Cómo hacer música en color cuando eres programador

Una vez vi una película , y el momento quedó grabado en mi memoria cuando uno de los personajes principales de la película levanta con tacto la pierna y el brazo al ritmo de la melodía. Entonces: no solo el héroe es notable, sino el conjunto de monitores detrás de él.


Recientemente, como muchos otros, la cantidad de tiempo que pasó en cuatro paredes ha aumentado considerablemente, y surgió la idea: "¿Y cuál es la forma más extraña de realizar tal escena?"


Mirando hacia el futuro, diré que la elección recayó en el uso de un navegador web, es decir, WebRTC y WebAudio API.


Así que pensé, y luego fui inmediatamente a las opciones, desde simples (coger la radio y encontrar un reproductor que tenga esa visualización), hasta las largas (crear una aplicación cliente-servidor que envíe información sobre el color a través del zócalo). Y luego me sorprendí pensando: "ahora el navegador tiene todos los componentes necesarios para la implementación", así que intentaré hacerlo con él.


WebRTC es una forma de transferir datos de un navegador a otro (punto a punto), lo que significa que no tendré que crear un servidor, al principio pensé. Pero estaba un poco equivocado. Para hacer RTCPeerConnection, necesita dos servidores: señal e ICE. Para el segundo, puede usar una solución preparada (los servidores STUN o TURN están en los repositorios de muchas distribuciones de Linux). Algo debe hacerse con el primero.


La documentación dice que un protocolo de interacción bidireccional arbitrario puede actuar como una señal, y luego inmediatamente WebSockets, Long pooling o hacer algo diferente. Me parece que la opción más fácil es tomar hola mundo de la documentación de alguna biblioteca. Y aquí hay un servidor de señal tan simple:


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)

Ni siquiera implementé el procesamiento de los mensajes de WebSockets del cliente, sino que simplemente hice un punto final POST, que envía el mensaje a todos. El enfoque para copiar de la documentación es de mi agrado.


Además, para establecer una conexión WebRTC entre navegadores, se produce un hi-hi sin complicaciones, y usted puede, y yo puedo. El diagrama es muy claramente visible:



(Gráfico tomado de la página )


Primero debe crear la conexión en sí:


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;

  });
  }

}

WebAudio API. , , , .


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));
}

De todo el espectro, se resalta una banda que estará representada por un solo navegador, y promediando, se obtiene la amplitud. Luego, solo haga el color entre rojo y azul, dependiendo de la misma amplitud. Dibuja el lienzo.


Algo así se ve como el resultado de usar el fruto de la imaginación:



All Articles