Cara membuat musik berwarna ketika Anda seorang programmer

Suatu kali saya menonton satu film , dan momen itu tercetak dalam ingatan saya ketika salah satu karakter utama film dengan bijaksana mengangkat kaki dan lengannya ke irama melodi. Jadi: bukan hanya pahlawan yang patut diperhatikan, tetapi set monitor di belakangnya.


Baru-baru ini, seperti banyak yang lain, jumlah waktu yang dihabiskan di empat dinding telah sangat meningkat, dan muncul ide: "Dan apa cara paling aneh untuk mewujudkan adegan seperti itu?"


Ke depan, saya akan mengatakan bahwa pilihan jatuh pada penggunaan browser web, yaitu WebRTC dan WebAudio API.


Jadi saya pikir, dan kemudian langsung pergi opsi, dari yang sederhana (mengambil radio dan menemukan pemain yang memiliki visualisasi seperti itu), yang lama (membuat aplikasi client-server yang akan mengirim informasi tentang warna melalui soket). Dan kemudian saya mendapati diri saya berpikir: "sekarang browser memiliki semua komponen yang diperlukan untuk implementasi", jadi saya akan mencoba melakukannya dengan itu.


WebRTC adalah cara mentransfer data dari browser ke browser (peer-to-peer), yang berarti saya tidak akan harus membuat server, pada awalnya saya pikir. Tapi dia sedikit salah. Untuk membuat RTCPeerConnection, Anda memerlukan dua server: sinyal dan ICE. Untuk yang kedua, Anda dapat menggunakan solusi yang sudah jadi (server STUN atau TURN berada dalam repositori dari banyak distribusi linux). Sesuatu harus dilakukan dengan yang pertama.


Dokumentasi mengatakan bahwa protokol interaksi dua arah yang sewenang-wenang dapat bertindak sebagai sinyal, dan kemudian segera WebSockets, Long pooling atau melakukan sesuatu yang berbeda. Sepertinya saya bahwa pilihan termudah adalah mengambil hello world dari dokumentasi beberapa perpustakaan. Dan di sini adalah server sinyal sederhana:


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)

Saya bahkan tidak mengimplementasikan pemrosesan pesan WebSockets klien, tetapi hanya membuat titik akhir POST, yang mengirim pesan ke semua orang. Pendekatan untuk menyalin dari dokumentasi sesuai dengan keinginan saya.


Selanjutnya, untuk membuat koneksi WebRTC antara browser, hi-hi yang tidak rumit terjadi, dan Anda bisa - dan saya bisa. Diagram ini sangat jelas terlihat:



(Grafik diambil dari halaman )


Pertama, Anda perlu membuat koneksi itu sendiri:


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

Dari keseluruhan spektrum, sebuah band disorot yang akan diwakili oleh satu browser, dan rata-rata, amplitudo diperoleh. Selanjutnya, cukup buat warna antara merah dan biru, tergantung pada amplitudo yang sama. Gambar kanvas.


Sesuatu seperti ini terlihat seperti hasil dari menggunakan buah imajinasi:



All Articles