Wie man als Programmierer Farbmusik macht

Einmal habe ich einen Film gesehen und der Moment war mir in Erinnerung geblieben, als eine der Hauptfiguren des Films ihr Bein und ihren Arm taktvoll im Rhythmus der Melodie anhob. Also: Nicht nur der Held ist bemerkenswert, sondern auch die Monitore hinter ihm.


In letzter Zeit hat, wie bei vielen anderen, die Zeit, die in vier Wänden verbracht wird, stark zugenommen, und es kam die Idee auf: "Und was ist der seltsamste Weg, eine solche Szene zu realisieren?"


Mit Blick auf die Zukunft werde ich sagen, dass die Wahl auf die Verwendung eines Webbrowsers fiel, nämlich WebRTC und WebAudio API.


Also dachte ich mir und ging dann sofort zu den Optionen über, von einfach (nimm das Radio und finde einen Player mit einer solchen Visualisierung) bis zu langen (mache eine Client-Server-Anwendung, die Informationen über die Farbe über den Socket sendet). Und dann fing ich an zu denken: "Jetzt hat der Browser alle für die Implementierung erforderlichen Komponenten", also werde ich versuchen, es damit zu tun.


WebRTC ist eine Möglichkeit, Daten von Browser zu Browser (Peer-to-Peer) zu übertragen. Das bedeutet, dass ich zunächst keinen Server erstellen muss. Aber er hat sich ein bisschen geirrt. Um RTCPeerConnection herzustellen, benötigen Sie zwei Server: Signal und ICE. Zum zweiten können Sie eine vorgefertigte Lösung verwenden (STUN- oder TURN-Server befinden sich in den Repositorys vieler Linux-Distributionen). Mit dem ersten muss etwas getan werden.


Die Dokumentation besagt, dass ein beliebiges Zwei-Wege-Interaktionsprotokoll als Signal fungieren kann und dann sofort WebSockets, Long Pooling oder etwas anderes tun kann. Es scheint mir, dass die einfachste Möglichkeit darin besteht, die Hallo-Welt aus der Dokumentation einer Bibliothek zu entnehmen. Und hier ist so ein einfacher Signalserver:


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)

Ich habe nicht einmal die Verarbeitung von Client-WebSockets-Nachrichten implementiert, sondern einfach einen POST-Endpunkt erstellt, der die Nachricht an alle sendet. Der Ansatz zum Kopieren aus der Dokumentation gefällt mir.


Um eine WebRTC-Verbindung zwischen Browsern herzustellen, tritt ein unkompliziertes Hi-Hi auf, und Sie können - und ich kann. Das Diagramm ist sehr gut sichtbar:



(Diagramm von Seite genommen )


Zuerst müssen Sie die Verbindung selbst erstellen:


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

Aus dem gesamten Spektrum wird ein Band hervorgehoben, das von einem einzelnen Browser dargestellt wird, und durch Mittelwertbildung wird die Amplitude erhalten. Als nächstes stellen Sie einfach die Farbe zwischen Rot und Blau ein, abhängig von der gleichen Amplitude. Zeichne die Leinwand.


So etwas sieht aus wie das Ergebnis der Verwendung der Frucht der Fantasie:



All Articles