Как Сделать Цветную Музыку, Если Ты Программист

Однажды я смотрел один фильм и отпечатывается в памяти момент когда один из главных героев фильма тактично поднимает ногу и руку в ритм мелодии.

Так вот: примечателен не столько герой, сколько множество мониторов, стоящих за его спиной.

В последнее время у меня, как и у многих других, сильно увеличилось количество времени, проводимого в четырех стенах, и мне пришла в голову идея: «Какой самый странный способ реализовать такую сценуЭ» Забегая вперед скажу, что выбор пал на использование веб-браузера, а именно WebRTC и WebAudio API. Вот я и подумал, и тут сразу начались варианты, от простых (поднять магнитолу и найти плеер, у которого есть такая визуализация), до длинных (сделать клиент-серверное приложение, которое будет отправлять информацию о цвете через сокет).

И тут я поймал себя на мысли: «теперь в браузере есть все необходимые для реализации компоненты», вот и попробую сделать это с его помощью.

WebRTC — это способ передачи данных из браузера в браузер (peer-to-peer), а значит, мне не придется делать сервер, подумал я сначала.

Но я немного ошибся.

Для того, чтобы сделать RTCPeerConnection, нужны два сервера: сигнальный и ICE. Для второго можно использовать готовое решение (серверы STUN или TURN есть в репозиториях многих дистрибутивов Linux).

С первым нужно что-то делать.

В документации сказано, что в качестве сигнала может выступать произвольный протокол двусторонней связи, и здесь можно сразу использовать WebSockets, Longpooling или сделать что-то свое.

Мне кажется, самый простой вариант — взять hello world из документации какой-нибудь библиотеки.

А вот этот простой сервер сигнализации:

  
  
  
  
  
  
  
  
   

import os from aiohttp import web, WSMsgType routes = web.RouteTableDef() routes.static("/def", os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') ) @routes.post('/broadcast') async def word(request): for conn in list(ws_client_connections): data = await request.text() await conn.send_str(data) return web.Response(text="Hello, world") ws_client_connections = set() async def websocket_handler(request): ws = web.WebSocketResponse(autoping=True) await ws.prepare(request) ws_client_connections.add(ws) async for msg in ws: if msg.type == WSMsgType.TEXT: if msg.data == 'close': await ws.close() # del ws_client_connections[user_id] else: continue elif msg.type == WSMsgType.ERROR: print('conn lost') # del ws_client_connections[user_id] return ws if __name__ == '__main__': app = web.Application() app.add_routes(routes) app.router.add_get('/ws', websocket_handler) web.run_app(app)

Я даже не стал реализовывать обработку клиентских WebSockets-сообщений, а просто сделал POST-эндпойнт, который отправляет сообщение всем желающим.

Мне нравится подход копирования документации.

Далее, чтобы установить WebRTC-соединение между браузерами, происходит простое «привет-привет», и вы можете — и я могу.

На схеме это очень наглядно показано:

Как сделать цветную музыку, если ты программист

(Схема взята из страницы ) Для начала нужно создать само соединение:

function openConnection() { const servers = { iceServers: [ { urls: [`turn:${window.location.hostname}`], username: 'rtc', credential: 'demo' }, { urls: [`stun:${window.location.hostname}`] } ]}; let localConnection = new RTCPeerConnection(servers); console.log('Created local peer connection object localConnection'); dataChannelSend.placeholder = ''; localConnection.ondatachannel = receiveChannelCallback; localConnection.ontrack = e => { consumeRemoteStream(localConnection, e); } let sendChannel = localConnection.createDataChannel('sendDataChannel'); console.log('Created send data channel'); sendChannel.onopen = onSendChannelStateChange; sendChannel.onclose = onSendChannelStateChange; return localConnection; }

Здесь важно, что прежде чем устанавливать само соединение, нужно задать цель подключения.

Вызовы createDataChannel() и более поздние addTrack() делают именно это.



function createConnection() { if(!iAmHost){ alert('became host') return 0; } for (let listener of streamListeners){ streamListenersConnections[listener] = openConnection() streamListenersConnections[listener].

onicecandidate = e => { onIceCandidate(streamListenersConnections[listener], e, listener); }; audioStreamDestination.stream.getTracks().

forEach(track => { streamListenersConnections[listener].

addTrack(track.clone()) }) // localConnection.getStats().

then(it => console.log(it)) streamListenersConnections[listener].

createOffer().

then((offer) => gotDescription1(offer, listener), onCreateSessionDescriptionError ).

then( () => { startButton.disabled = true; closeButton.disabled = false; }); } }

На этом этапе пора вспомнить об API WebAudio. Поскольку в браузере есть возможность загружать двоичный файл в память и даже декодировать его, если формат поддерживается, вам необходимо использовать эту возможность.



function createAudioBufferFromFile(){ let fileToProcess = new FileReader(); fileToProcess.readAsArrayBuffer(selectedFile.files[0]); fileToProcess.onload = () => { let audioCont = new AudioContext(); audioCont.decodeAudioData(fileToProcess.result).

then(res => { //TODO: stream to webrtcnode let source = audioCont.createBufferSource() audioSource = source; source.buffer = res; let dest = audioCont.createMediaStreamDestination() source.connect(dest) audioStreamDestination =dest; source.loop = false; // source.start(0) iAmHost = true; }); } }

Допустим, в этой функции mp3-файл считывается в память.

После чтения создается AudioContext, затем декодируется и превращается в MediaStream. Вуаля, у нас есть аудиопоток, который можно передать через метод addTrack() соединения WebRTC. Чуть раньше функция createOffer() вызывалась при каждом соединении и отправлялась соответствующему клиенту.

Клиент, в свою очередь, принимает предложение и уведомляет инициатора:

function acceptDescription2(desc, broadcaster) { return localConnection.setRemoteDescription(desc) .

then( () => { return localConnection.createAnswer(); }) .

then(answ => { return localConnection.setLocalDescription(answ); }).

then(() => { postData(JSON.stringify({type: "accept", user:username.value, to:broadcaster, descr:localConnection.currentLocalDescription})); }) }

Далее инициатор обрабатывает ответ и инициирует обмен IceCandiates:

function finalizeCandidate(val, listener) { console.log('accepting connection') const a = new RTCSessionDescription(val); streamListenersConnections[listener].

setRemoteDescription(a).

then(() => { dataChannelSend.disabled = false; dataChannelSend.focus(); sendButton.disabled = false; processIceCandiates(listener) }); }

Выглядит предельно просто:

let conn = localConnection? localConnection: streamListenersConnections[data.user] conn.addIceCandidate(data.candidate).

then( onAddIceCandidateSuccess, onAddIceCandidateError); processIceCandiates(data.user)

В результате каждое соединение через addIceCandidate() изучает возможные транспорты противоположного конца соединения.

На данный момент уже есть один основной браузер и несколько браузеров, готовых его слушать.

Остаётся только принять аудиопоток и воспроизвести/(отобразить нужную часть спектра в цвете экрана).



function consumeRemoteStream(localConnection, event) { console.log('consume remote stream') const styles = { position: 'absolute', height: '100%', width: '100%', top: '0px', left: '0px', 'z-index': 1000 }; Object.keys(styles).

map(i => { canvas.style[i] = styles[i]; }) let audioCont = new AudioContext(); audioContext = audioCont; let stream = new MediaStream(); stream.addTrack(event.track) let source = audioCont.createMediaStreamSource(stream) let analyser = audioCont.createAnalyser(); analyser.smoothingTimeConstant = 0; analyser.fftSize = 2048; source.connect(analyser); audioAnalizer = analyser; audioSource = source; analyser.connect(audioCont.destination) render() }

Небольшая манипуляция со стилями, чтобы заполнить всю страницу холстом.

Затем мы создаем AudioContext, который поступает через соединение, и добавляем два узла-потребителя AudioNode. Один из них имеет возможность получить спектрограмму, не зря метод называется createAnalyser().

Завершающий штрих:

function render(){ var freq = new Uint8Array(audioAnalizer.frequencyBinCount); audioAnalizer.getByteFrequencyData(freq); let band = freq.slice( Math.floor(freqFrom.value/audioContext.sampleRate*2*freq.length), Math.floor(freqTo.value/audioContext.sampleRate*2*freq.length)); let avg = band.reduce((a,b) => a+b,0)/band.length; context.fillStyle = `rgb( ${Math.floor(255 - avg)}, 0, ${Math.floor(avg)})`; context.fillRect(0,0,200,200); requestAnimationFrame(render.bind(gl)); }

Из всего спектра выбирается полоса, которая будет представлена одним браузером, и усредняется, получается амплитуда.

Затем мы просто делаем цвет между красным и синим, в зависимости от одной и той же амплитуды.

Давайте нарисуем контур.

Результат использования плода фантазии выглядит примерно так: Теги: #JavaScript #Ненормальное программирование #webrtc #webaudio API

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.