كيف تصنع موسيقى ملونة عندما تكون مبرمجًا

بمجرد أن شاهدت فيلمًا واحدًا ، وتم طبع اللحظة في ذاكرتي عندما قامت إحدى الشخصيات الرئيسية في الفيلم برفع ساقها وذراعها بلباقة إلى إيقاع اللحن. لذا: ليس فقط البطل الجدير بالملاحظة بل مجموعة المراقبين وراءه.


في الآونة الأخيرة ، مثل العديد من الجدران الأخرى ، ازداد مقدار الوقت الذي يقضيه في أربعة جدران بشكل كبير ، وظهرت الفكرة: "وما هي الطريقة الأكثر غرابة لتحقيق مثل هذا المشهد؟"


وبالنظر إلى المستقبل ، سأقول أن الاختيار وقع على استخدام متصفح الويب ، أي WebRTC و WebAudio API.


لذا فكرت ، ثم انتقلت على الفور إلى الخيارات ، من بسيطة (التقط الراديو واعثر على لاعب لديه مثل هذا التصور) ، إلى طويلة (قم بإنشاء تطبيق خادم العميل الذي سيرسل معلومات حول اللون من خلال المقبس). ثم أدركت نفسي أفكر: "يحتوي المتصفح الآن على جميع المكونات اللازمة للتنفيذ" ، لذلك سأحاول القيام بذلك.


WebRTC هي طريقة لنقل البيانات من متصفح إلى متصفح (نظير إلى نظير) ، مما يعني أنني لن أضطر إلى إنشاء خادم ، في البداية اعتقدت. لكنه كان مخطئا قليلا. من أجل جعل RTCPeerConnection ، تحتاج إلى خادمين: إشارة و ICE. ثانيًا ، يمكنك استخدام حل جاهز (خوادم STUN أو TURN موجودة في مستودعات العديد من توزيعات لينكس). شيء ما يجب القيام به مع الأول.


تقول الوثائق أن بروتوكول التفاعل التعسفي ثنائي الاتجاه يمكن أن يعمل كإشارة ، ومن ثم على الفور WebSockets أو تجميع طويل أو القيام بشيء مختلف. يبدو لي أن الخيار الأسهل هو أخذ عالم مرحبًا من توثيق بعض المكتبة. وهنا خادم إشارة بسيط مثل:


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 بين المتصفحات ، يحدث hi-hi غير معقد ، ويمكنك - وأنا أستطيع. الرسم البياني واضح للغاية:



(مخطط مأخوذ من الصفحة )


تحتاج أولاً إلى إنشاء الاتصال نفسه:


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

من النطاق بأكمله ، يتم تمييز النطاق الذي سيتم تمثيله بواسطة متصفح واحد ، ويتم الحصول على متوسط ​​السعة. بعد ذلك ، ما عليك سوى جعل اللون بين الأحمر والأزرق ، اعتمادًا على نفس السعة. ارسم اللوحة.


شيء من هذا القبيل يبدو وكأنه نتيجة استخدام ثمار الخيال:



All Articles