Comment faire de la musique couleur lorsque vous êtes programmeur

Une fois j'ai regardé un film , et le moment a été imprimé dans ma mémoire quand l'un des personnages principaux du film lève avec tact sa jambe et son bras au rythme de la mélodie. Donc: non seulement le héros est remarquable, mais l'ensemble des moniteurs derrière lui.


Récemment, comme beaucoup d'autres, le temps passé sur quatre murs a considérablement augmenté, et l'idée est venue: "Et quelle est la façon la plus étrange de réaliser une telle scène?"


Pour l'avenir, je dirai que le choix s'est porté sur l'utilisation d'un navigateur Web, à savoir WebRTC et WebAudio API.


J'ai donc pensé, puis je suis immédiatement allé aux options, du simple (prenez la radio et trouvez un lecteur qui a une telle visualisation), aux longues (faites une application client-serveur qui enverra des informations sur la couleur via le socket). Et puis je me suis surpris à penser: "maintenant le navigateur a tous les composants nécessaires à l'implémentation", donc je vais essayer de le faire avec.


WebRTC est un moyen de transférer des données de navigateur à navigateur (peer-to-peer), ce qui signifie que je n'ai pas besoin de créer de serveur, au début, je pensais. Mais il avait un peu tort. Afin de faire RTCPeerConnection, vous avez besoin de deux serveurs: signal et ICE. Pour la seconde, vous pouvez utiliser une solution toute faite (les serveurs STUN ou TURN sont dans les référentiels de nombreuses distributions Linux). Il faut faire quelque chose avec le premier.


La documentation indique qu'un protocole d'interaction bidirectionnelle arbitraire peut agir comme un signal, puis immédiatement WebSockets, Long pooling ou faire quelque chose de différent. Il me semble que l'option la plus simple est de prendre bonjour dans la documentation d'une bibliothèque. Et voici un serveur de signaux si 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)

Je n'ai même pas implémenté le traitement des messages WebSockets client, mais simplement créé un point de terminaison POST, qui envoie le message à tout le monde. L'approche à copier à partir de la documentation est à mon goût.


De plus, pour établir une connexion WebRTC entre les navigateurs, un hi-hi simple se produit, et vous pouvez - et je peux. Le diagramme est très clairement visible:



(Graphique tiré de la page )


Vous devez d'abord créer la connexion elle-même:


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

A partir de l'ensemble du spectre, une bande est mise en évidence qui sera représentée par un seul navigateur, et en moyenne, l'amplitude est obtenue. Ensuite, faites juste la couleur entre le rouge et le bleu, selon la même amplitude. Dessinez la toile.


Quelque chose comme ça ressemble au résultat de l'utilisation du fruit de l'imagination:



All Articles