دروس MIDI: إنشاء تطبيقات صوتية مستندة إلى المستعرض يتم التحكم فيها بواسطة أجهزة MIDI

نشرت: 2022-03-11

بينما تزداد شعبية واجهة برمجة تطبيقات Web Audio ، خاصة بين مطوري ألعاب HTML5 ، لا تزال واجهة Web MIDI API قليلة الشهرة بين مطوري الواجهة الأمامية. ربما يتعلق جزء كبير من هذا بنقص الدعم الحالي والوثائق التي يمكن الوصول إليها ؛ واجهة برمجة تطبيقات Web MIDI مدعومة حاليًا في Google Chrome فقط ، مع منحك تمكين علامة خاصة لها. يضع مصنعو المتصفحات في الوقت الحالي القليل من التركيز على واجهة برمجة التطبيقات هذه ، حيث من المخطط أن تكون جزءًا من معيار ES7.

تم تصميم MIDI في أوائل الثمانينيات من قبل العديد من ممثلي صناعة الموسيقى (اختصارًا للواجهة الرقمية للآلات الموسيقية) ، وهو بروتوكول اتصال قياسي لأجهزة الموسيقى الإلكترونية. على الرغم من أن البروتوكولات الأخرى ، مثل OSC ، قد تم تطويرها منذ ذلك الحين ؛ بعد ثلاثين عامًا ، لا يزال MIDI هو بروتوكول الاتصال الفعلي لمصنعي أجهزة الصوت. ستتعرض لضغوط شديدة للعثور على منتج موسيقى حديث لا يمتلك جهاز MIDI واحدًا على الأقل في الاستوديو الخاص به.

من خلال التطوير السريع لواجهة برمجة تطبيقات Web Audio واعتمادها ، يمكننا الآن البدء في إنشاء تطبيقات قائمة على المستعرض تعمل على سد الفجوة بين السحابة والعالم المادي. لا تسمح لنا واجهة Web MIDI API ببناء آلات توليف وتأثيرات صوتية فحسب ، بل يمكننا أيضًا البدء في إنشاء DAW المستندة إلى المستعرض (محطة عمل الصوت الرقمي) المشابهة في الميزات والأداء لنظرائهم الحاليين المستندة إلى الفلاش (راجع Audiotool ، على سبيل المثال ).

في هذا البرنامج التعليمي MIDI ، سوف أرشدك خلال أساسيات Web MIDI API ، وسنقوم ببناء monosynth بسيط يمكنك اللعب به مع جهاز MIDI المفضل لديك. يتوفر كود المصدر الكامل هنا ، ويمكنك اختبار العرض التوضيحي المباشر مباشرة. إذا لم يكن لديك جهاز MIDI ، فلا يزال بإمكانك اتباع هذا البرنامج التعليمي عن طريق التحقق من فرع "لوحة المفاتيح" في مستودع GitHub ، والذي يتيح الدعم الأساسي للوحة مفاتيح الكمبيوتر ، بحيث يمكنك تشغيل الملاحظات وتغيير الأوكتافات. هذا أيضًا هو الإصدار المتاح كعرض مباشر. ومع ذلك ، نظرًا لقيود أجهزة الكمبيوتر ، يتم تعطيل كل من السرعة و detune كلما استخدمت لوحة مفاتيح الكمبيوتر للتحكم في جهاز المزج. يرجى الرجوع إلى الملف التمهيدي على GitHub للقراءة حول تعيين المفتاح / الملاحظة.

برنامج تعليمي ميدي Toptal

المتطلبات الأساسية لبرنامج Midi التعليمي

ستحتاج إلى ما يلي من أجل هذا البرنامج التعليمي MIDI:

  • Google Chrome (الإصدار 38 أو أعلى) مع تمكين علامة #enable-web-midi
  • (اختياريًا) جهاز MIDI ، يمكنه تشغيل الملاحظات ، متصل بجهاز الكمبيوتر الخاص بك

سنستخدم أيضًا Angular.js لإضفاء القليل من البنية على تطبيقنا ؛ لذلك ، فإن المعرفة الأساسية للإطار هي شرط أساسي.

ابدء

سننظم تطبيق MIDI الخاص بنا من الألف إلى الياء عن طريق فصله إلى 3 وحدات:

  • WebMIDI: التعامل مع أجهزة MIDI المختلفة المتصلة بجهاز الكمبيوتر الخاص بك
  • WebAudio: توفير مصدر الصوت الخاص بنا
  • WebSynth: توصيل واجهة الويب بمحرك الصوت

ستتعامل وحدة App مع تفاعل المستخدم مع واجهة مستخدم الويب. يمكن أن تبدو بنية التطبيق لدينا مثل هذا قليلاً:

 |- app |-- js |--- midi.js |--- audio.js |--- synth.js |--- app.js |- index.html

يجب عليك أيضًا تثبيت المكتبات التالية لمساعدتك في إنشاء تطبيقك: Angular.js و Bootstrap و jQuery. ربما تكون أسهل طريقة لتثبيتها هي عبر Bower.

وحدة WebMIDI: الاتصال بالعالم الحقيقي

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

 angular .module('WebMIDI', []) .factory('Devices', ['$window', function($window) { function _connect() { if($window.navigator && 'function' === typeof $window.navigator.requestMIDIAccess) { $window.navigator.requestMIDIAccess(); } else { throw 'No Web MIDI support'; } } return { connect: _connect }; }]);

وهذا كل شيء إلى حد كبير!

requestMIDIAccess وعدًا ، لذا يمكننا فقط إعادته مباشرة والتعامل مع نتيجة الوعد في وحدة التحكم في التطبيق لدينا:

 angular .module('DemoApp', ['WebMIDI']) .controller('AppCtrl', ['$scope', 'Devices', function($scope, devices) { $scope.devices = []; devices .connect() .then(function(access) { if('function' === typeof access.inputs) { // deprecated $scope.devices = access.inputs(); console.error('Update your Chrome version!'); } else { if(access.inputs && access.inputs.size > 0) { var inputs = access.inputs.values(), input = null; // iterate through the devices for (input = inputs.next(); input && !input.done; input = inputs.next()) { $scope.devices.push(input.value); } } else { console.error('No devices detected!'); } } }) .catch(function(e) { console.error(e); }); }]);

كما ذكرنا سابقًا ، يُرجع التابع requestMIDIAccess وعدًا ، ويمرر كائنًا إلى التابع then ، بخاصيتين: المدخلات والمخرجات.

في الإصدارات السابقة من Chrome ، كانت هاتان الخاصيتان طريقتين تسمحان لك باسترداد مجموعة من أجهزة الإدخال والإخراج مباشرة. ومع ذلك ، في التحديثات الأخيرة ، أصبحت هذه الخصائص كائنات الآن. هذا يحدث فرقًا كبيرًا ، لأننا نحتاج الآن إلى استدعاء طريقة values على كائن المدخلات أو المخرجات لاسترداد قائمة الأجهزة المقابلة. تعمل هذه الطريقة كوظيفة مولد ، وتقوم بإرجاع مكرر. مرة أخرى ، من المفترض أن تكون واجهة برمجة التطبيقات هذه جزءًا من ES7 ؛ لذلك ، فإن تنفيذ السلوك الشبيه بالمولد أمر منطقي ، على الرغم من أنه ليس مباشرًا مثل التنفيذ الأصلي.

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

 <select ng-model="activeDevice" class="form-control" ng-options="device.manufacturer + ' ' + device.name for device in devices"> <option value="" disabled>Choose a MIDI device...</option> </select>

لقد ربطنا مربع التحديد هذا بمتغير نطاق $ يسمى activeDevice والذي سنستخدمه لاحقًا لتوصيل هذا الجهاز النشط بالمُزامنة.

قم بتوصيل هذا الجهاز النشط بالمزامنة

وحدة WebAudio: إحداث ضوضاء

لا تسمح لنا واجهة برمجة تطبيقات WebAudio بتشغيل ملفات الصوت فحسب ، بل تتيح لنا أيضًا إنشاء أصوات عن طريق إعادة إنشاء المكونات الأساسية لأجهزة المزج مثل المذبذبات والمرشحات وعقد الكسب وغيرها.

قم بإنشاء مذبذب

دور المذبذبات هو إخراج شكل موجة. هناك أنواع مختلفة من أشكال الموجة ، من بينها أربعة مدعومة في WebAudio API: الجيب والمربع والمثلث وسن المنشار. يُقال أن أشكال الموجة "تتأرجح" عند تردد معين ، ولكن من الممكن أيضًا أن يحدد المرء جدول الموجة المخصص الخاص به إذا لزم الأمر. مجموعة معينة من الترددات مسموعة من قبل البشر - تُعرف بالأصوات. بدلاً من ذلك ، عندما تتأرجح عند ترددات منخفضة ، يمكن أن تساعدنا المذبذبات أيضًا في بناء LFO's ("مذبذب التردد المنخفض") حتى نتمكن من تعديل أصواتنا (ولكن هذا خارج نطاق هذا البرنامج التعليمي).

أول شيء يتعين علينا القيام به لإنشاء بعض الأصوات هو إنشاء مثيل نص AudioContext جديد:

 function _createContext() { self.ctx = new $window.AudioContext(); }

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

 angular .module('WebAudio', []) .service('OSC', function() { var self; function Oscillator(ctx) { self = this; self.osc = ctx.createOscillator(); return self; } });

يمكننا الآن إنشاء مثيل لمذبذبات جديدة بإرادتنا ، لتمرير مثيل AudioContext الذي أنشأناه سابقًا كوسيطة. لتسهيل الأمور في المستقبل ، سنضيف بعض طرق الغلاف - مجرد سكر نحوي - ونعيد وظيفة المذبذب:

 Oscillator.prototype.setOscType = function(type) { if(type) { self.osc.type = type } } Oscillator.prototype.setFrequency = function(freq, time) { self.osc.frequency.setTargetAtTime(freq, 0, time); }; Oscillator.prototype.start = function(pos) { self.osc.start(pos); } Oscillator.prototype.stop = function(pos) { self.osc.stop(pos); } Oscillator.prototype.connect = function(i) { self.osc.connect(i); } Oscillator.prototype.cancel = function() { self.osc.frequency.cancelScheduledValues(0); } return Oscillator;

قم بإنشاء مرشح Multipass والتحكم في مستوى الصوت

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

نقوم بإنشاء مرشح عن طريق استدعاء طريقة createBiquadFilter لمثيل AudioContext:

 ctx.createBiquadFilter();

وبالمثل ، بالنسبة لعقدة الكسب ، نسمي طريقة createGain :

 ctx.createGain();

وحدة WebSynth: توصيل الأشياء

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

 self.osc1 = new Oscillator(self.ctx); self.osc1.setOscType('sine'); self.amp = new Amp(self.ctx); self.osc1.connect(self.amp.gain); self.amp.connect(self.ctx.destination); self.amp.setVolume(0.0, 0); //mute the sound self.filter1.disconnect(); self.amp.disconnect(); self.amp.connect(self.ctx.destination); }

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

 // in the app's controller $scope.$watch('activeDevice', DSP.plug); // in the synth module function _onmidimessage(e) { /** * e.data is an array * e.data[0] = on (144) / off (128) / detune (224) * e.data[1] = midi note * e.data[2] = velocity || detune */ switch(e.data[0]) { case 144: Engine.noteOn(e.data[1], e.data[2]); break; case 128: Engine.noteOff(e.data[1]); break; } } function _plug(device) { self.device = device; self.device.onmidimessage = _onmidimessage; }

هنا ، نستمع إلى أحداث MIDI من الجهاز ، ونحلل البيانات من كائن MidiEvent ، ونمررها إلى الطريقة المناسبة ؛ إما noteOn أو noteOff ، بناءً على رمز الحدث (144 لـ noteOn ، و 128 لـ noteOff). يمكننا الآن إضافة المنطق بالطرق المعنية في وحدة الصوت لتوليد صوت فعليًا:

 function _noteOn(note, velocity) { self.activeNotes.push(note); self.osc1.cancel(); self.currentFreq = _mtof(note); self.osc1.setFrequency(self.currentFreq, self.settings.portamento); self.amp.cancel(); self.amp.setVolume(1.0, self.settings.attack); } function _noteOff(note) { var position = self.activeNotes.indexOf(note); if (position !== -1) { self.activeNotes.splice(position, 1); } if (self.activeNotes.length === 0) { // shut off the envelope self.amp.cancel(); self.currentFreq = null; self.amp.setVolume(0.0, self.settings.release); } else { // in case another note is pressed, we set that one as the new active note self.osc1.cancel(); self.currentFreq = _mtof(self.activeNotes[self.activeNotes.length - 1]); self.osc1.setFrequency(self.currentFreq, self.settings.portamento); } }

هناك أشياء قليلة تحدث هنا. في طريقة noteOn ، نقوم أولاً بدفع الملاحظة الحالية إلى مجموعة من الملاحظات. على الرغم من أننا نقوم ببناء monosynth (مما يعني أنه لا يمكننا تشغيل سوى ملاحظة واحدة في كل مرة) ، لا يزال بإمكاننا استخدام عدة أصابع في نفس الوقت على لوحة المفاتيح. لذلك ، نحتاج إلى ترتيب جميع ملاحظات الأطروحات في قائمة الانتظار بحيث عندما نقوم بإصدار ملاحظة واحدة ، يتم تشغيل الملاحظة التالية. نحتاج بعد ذلك إلى إيقاف المذبذب لتعيين التردد الجديد ، والذي نقوم بتحويله من ملاحظة MIDI (مقياس من 0 إلى 127) إلى قيمة تردد فعلية مع القليل من الرياضيات:

 function _mtof(note) { return 440 * Math.pow(2, (note - 69) / 12); }

في طريقة noteOff ، نبدأ أولاً بإيجاد الملاحظة في مصفوفة الملاحظات النشطة وإزالتها. بعد ذلك ، إذا كانت هذه هي الملاحظة الوحيدة في المصفوفة ، فإننا ببساطة نوقف تشغيل الصوت.

الوسيطة الثانية لطريقة setVolume هي وقت الانتقال ، مما يعني المدة التي يستغرقها الكسب للوصول إلى قيمة الحجم الجديدة. من الناحية الموسيقية ، إذا كانت النوتة الموسيقية قيد التشغيل ، فستكون معادلة لوقت الهجوم ، وإذا كانت النوتة الموسيقية معطلة ، فإنها تعادل وقت الإصدار.

وحدة WebAnalyser: تصور صوتنا

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

 function Analyser(canvas) { self = this; self.canvas = angular.element(canvas) || null; self.view = self.canvas[0].getContext('2d') || null; self.javascriptNode = null; self.analyser = null; return self; }

بعد ذلك ، نضيف طريقة connect ، حيث سننشئ كلاً من المحلل ومعالج البرنامج النصي:

 Analyser.prototype.connect = function(ctx, output) { // setup a javascript node self.javascriptNode = ctx.createScriptProcessor(2048, 1, 1); // connect to destination, else it isn't called self.javascriptNode.connect(ctx.destination); // setup an analyzer self.analyser = ctx.createAnalyser(); self.analyser.smoothingTimeConstant = 0.3; self.analyser.fftSize = 512; // connect the output to the destination for sound output.connect(ctx.destination); // connect the output to the analyser for processing output.connect(self.analyser); self.analyser.connect(self.javascriptNode); // define the colors for the graph var gradient = self.view.createLinearGradient(0, 0, 0, 200); gradient.addColorStop(1, '#000000'); gradient.addColorStop(0.75, '#ff0000'); gradient.addColorStop(0.25, '#ffff00'); gradient.addColorStop(0, '#ffffff'); // when the audio process event is fired on the script processor // we get the frequency data into an array // and pass it to the drawSpectrum method to render it in the canvas self.javascriptNode.onaudioprocess = function() { // get the average for the first channel var array = new Uint8Array(self.analyser.frequencyBinCount); self.analyser.getByteFrequencyData(array); // clear the current state self.view.clearRect(0, 0, 1000, 325); // set the fill style self.view.fillStyle = gradient; drawSpectrum(array); } };

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

أخيرًا ، سيقوم scriptProcessor بإطلاق حدث "معالجة صوتية" على فاصل زمني ؛ عند إطلاق هذا الحدث ، نحسب متوسط ​​الترددات الملتقطة بواسطة المحلل ، ونمسح اللوحة القماشية ، ونعيد رسم الرسم البياني للتردد الجديد عن طريق استدعاء طريقة drawSpectrum :

 function drawSpectrum(array) { for (var i = 0; i < (array.length); i++) { var v = array[i], h = self.canvas.height(); self.view.fillRect(i * 2, h - (v - (h / 4)), 1, v + (h / 4)); } }

أخيرًا وليس آخرًا ، سنحتاج إلى تعديل أسلاك محرك الصوت لدينا قليلاً لاستيعاب هذا المكون الجديد:

 // in the _connectFilter() method if(self.analyser) { self.analyser.connect(self.ctx, self.filter1); } else { self.filter1.connect(self.ctx.destination); } // in the _disconnectFilter() method if(self.analyser) { self.analyser.connect(self.ctx, self.amp); } else { self.amp.connect(self.ctx.destination); }

لدينا الآن مصور مرئي رائع يسمح لنا بعرض شكل موجة موالفةنا في الوقت الفعلي! يتضمن هذا القليل من العمل للإعداد ، لكنه مثير للاهتمام وثاقب البصيرة ، خاصة عند استخدام الفلاتر.

بناء على توليفنا: إضافة السرعة و Detune

في هذه المرحلة من برنامج MIDI التعليمي لدينا ، لدينا توليفة رائعة جدًا - لكنها تلعب كل نغمة بنفس الحجم. هذا لأنه بدلاً من التعامل مع بيانات السرعة بشكل صحيح ، نقوم ببساطة بتعيين الحجم على قيمة ثابتة تبلغ 1.0. لنبدأ بإصلاح ذلك ، وبعد ذلك سنرى كيف يمكننا تمكين عجلة التفريغ التي تجدها في لوحات مفاتيح MIDI الأكثر شيوعًا.

سرعة التمكين

إذا لم تكن معتادًا على ذلك ، فإن "السرعة" تتعلق بمدى صعوبة الضغط على المفتاح الموجود على لوحة المفاتيح. بناءً على هذه القيمة ، يبدو الصوت الناتج إما أنعم أو أعلى.

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

 function _vtov (velocity) { return (velocity / 127).toFixed(2); }

يتراوح نطاق سرعة جهاز MIDI من 0 إلى 127 ، لذلك نقسم هذه القيمة ببساطة على 127 ونعيد قيمة عائمة مع رقمين عشريين. بعد ذلك ، يمكننا تحديث طريقة _noteOn لتمرير القيمة المحسوبة إلى عقدة الكسب:

 self.amp.setVolume(_vtov(velocity), self.settings.attack);

وهذا كل شيء! الآن ، عندما نلعب المزج الخاص بنا ، سنلاحظ اختلاف الأحجام بناءً على مدى صعوبة الضغط على المفاتيح الموجودة على لوحة المفاتيح الخاصة بنا.

تمكين عجلة Detune على لوحة مفاتيح MIDI الخاصة بك

تتميز معظم لوحات مفاتيح MIDI بعجلة detune ؛ تسمح لك العجلة بتغيير تردد النغمة التي يتم تشغيلها بشكل طفيف ، مما يخلق تأثيرًا مثيرًا يُعرف باسم "detune". هذا سهل التنفيذ إلى حد ما عندما تتعلم كيفية استخدام MIDI ، حيث تقوم عجلة detune أيضًا بإطلاق حدث MidiMessage برمز الحدث الخاص به (224) ، والذي يمكننا الاستماع إليه والعمل على أساسه من خلال إعادة حساب قيمة التردد وتحديث المذبذب.

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

 case 224: // the detune value is the third argument of the MidiEvent.data array Engine.detune(e.data[2]); break;

ثم نحدد طريقة detune على محرك الصوت:

 function _detune(d) { if(self.currentFreq) { //64 = no detune if(64 === d) { self.osc1.setFrequency(self.currentFreq, self.settings.portamento); self.detuneAmount = 0; } else { var detuneFreq = Math.pow(2, 1 / 12) * (d - 64); self.osc1.setFrequency(self.currentFreq + detuneFreq, self.settings.portamento); self.detuneAmount = detuneFreq; } } }

قيمة detune الافتراضية هي 64 ، مما يعني أنه لا يوجد detune مطبق ، لذلك في هذه الحالة نقوم ببساطة بتمرير التردد الحالي إلى المذبذب.

أخيرًا ، نحتاج أيضًا إلى تحديث طريقة _noteOff ، لأخذ detune في الاعتبار في حالة وضع ملاحظة أخرى في قائمة الانتظار:

 self.osc1.setFrequency(self.currentFreq + self.detuneAmount, self.settings.portamento);

إنشاء الواجهة

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

إنشاء تخطيط للواجهة

سننشئ عناصر شكل مختلفة للتحكم في صوت الموالفة الخاصة بنا:

  • مجموعة راديو لتحديد نوع المذبذب
  • مربع اختيار لتمكين / تعطيل عامل التصفية
  • مجموعة راديو لتحديد نوع المرشح
  • نطاقان للتحكم في تردد ورنين المرشح
  • نطاقان للتحكم في الهجوم وتحرير عقدة الكسب

عند إنشاء مستند HTML لواجهتنا ، يجب أن ينتهي بنا الأمر بشيء مثل هذا:

 <div class="synth container" ng-controller="WebSynthCtrl"> <h1>webaudio synth</h1> <div class="form-group"> <select ng-model="activeDevice" class="form-control" ng-options="device.manufacturer + ' ' + device.name for device in devices"> <option value="" disabled>Choose a MIDI device...</option> </select> </div> <div class="col-lg-6 col-md-6 col-sm-6"> <h2>Oscillator</h2> <div class="form-group"> <h3>Oscillator Type</h3> <label ng-repeat="t in oscTypes"> <input type="radio" name="oscType" ng-model="synth.oscType" value="{{t}}" ng-checked="'{{t}}' === synth.oscType" /> {{t}} </label> </div> <h2>Filter</h2> <div class="form-group"> <label> <input type="checkbox" ng-model="synth.filterOn" /> enable filter </label> </div> <div class="form-group"> <h3>Filter Type</h3> <label ng-repeat="t in filterTypes"> <input type="radio" name="filterType" ng-model="synth.filterType" value="{{t}}" ng-disabled="!synth.filterOn" ng-checked="synth.filterOn && '{{t}}' === synth.filterType" /> {{t}} </label> </div> <div class="form-group"> <!-- frequency --> <label>filter frequency:</label> <input type="range" class="form-control" min="50" max="10000" ng-model="synth.filterFreq" ng-disabled="!synth.filterOn" /> </div> <div class="form-group"> <!-- resonance --> <label>filter resonance:</label> <input type="range" class="form-control" min="0" max="150" ng-model="synth.filterRes" ng-disabled="!synth.filterOn" /> </div> </div> <div class="col-lg-6 col-md-6 col-sm-6"> <div class="panel panel-default"> <div class="panel-heading">Analyser</div> <div class="panel-body"> <!-- frequency analyser --> <canvas></canvas> </div> </div> <div class="form-group"> <!-- attack --> <label>attack:</label> <input type="range" class="form-control" min="50" max="2500" ng-model="synth.attack" /> </div> <div class="form-group"> <!-- release --> <label>release:</label> <input type="range" class="form-control" min="50" max="1000" ng-model="synth.release" /> </div> </div> </div>

تزيين واجهة المستخدم لتبدو خيالية ليس شيئًا سأقوم بتغطيته في هذا البرنامج التعليمي الأساسي MIDI ؛ بدلاً من ذلك ، يمكننا حفظه كتمرين لوقت لاحق لتحسين واجهة المستخدم ، ربما لتبدو شيئًا كالتالي:

واجهة مستخدم ميدي مصقولة

ربط الواجهة بمحرك الصوت

يجب أن نحدد بعض الطرق لربط عناصر التحكم هذه بمحرك الصوت لدينا.

التحكم في المذبذب

بالنسبة للمذبذب ، نحتاج فقط إلى طريقة تسمح لنا بضبط نوع المذبذب:

 Oscillator.prototype.setOscType = function(type) { if(type) { self.osc.type = type; } }

التحكم في الفلتر

بالنسبة للمرشح ، نحتاج إلى ثلاثة عناصر تحكم: واحدة لنوع المرشح ، وواحدة للتردد وواحدة للرنين. يمكننا أيضًا توصيل _connectFilter و _disconnectFilter بقيمة مربع الاختيار.

 Filter.prototype.setFilterType = function(type) { if(type) { self.filter.type = type; } } Filter.prototype.setFilterFrequency = function(freq) { if(freq) { self.filter.frequency.value = freq; } } Filter.prototype.setFilterResonance = function(res) { if(res) { self.filter.Q.value = res; } }

السيطرة على الهجوم والرنين

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

 function _setAttack(a) { if(a) { self.settings.attack = a / 1000; } } function _setRelease(r) { if(r) { self.settings.release = r / 1000; } }

إعداد المراقبين

أخيرًا ، في وحدة تحكم التطبيق لدينا ، نحتاج فقط إلى إعداد عدد قليل من المراقبين وربطهم بالطرق المختلفة التي أنشأناها للتو:

 $scope.$watch('synth.oscType', DSP.setOscType); $scope.$watch('synth.filterOn', DSP.enableFilter); $scope.$watch('synth.filterType', DSP.setFilterType); $scope.$watch('synth.filterFreq', DSP.setFilterFrequency); $scope.$watch('synth.filterRes', DSP.setFilterResonance); $scope.$watch('synth.attack', DSP.setAttack); $scope.$watch('synth.release', DSP.setRelease);

خاتمة

تمت تغطية الكثير من المفاهيم في هذا البرنامج التعليمي MIDI ؛ في الغالب ، اكتشفنا كيفية استخدام WebMIDI API ، وهو غير موثق إلى حد ما بصرف النظر عن المواصفات الرسمية من W3C. يعد تطبيق Google Chrome أمرًا بسيطًا إلى الأمام ، على الرغم من أن التبديل إلى كائن مكرر لأجهزة الإدخال والإخراج يتطلب القليل من إعادة بناء الكود القديم باستخدام التطبيق القديم.

بالنسبة لواجهة برمجة تطبيقات WebAudio ، فهذه واجهة برمجة تطبيقات غنية جدًا ، ولم نغطي سوى القليل من إمكاناتها في هذا البرنامج التعليمي. على عكس WebMIDI API ، فإن WebAudio API موثقة جيدًا ، لا سيما على شبكة مطوري Mozilla. تحتوي شبكة مطوري Mozilla على عدد كبير من أمثلة التعليمات البرمجية والقوائم التفصيلية للحجج والأحداث المختلفة لكل مكون ، مما سيساعدك على تنفيذ تطبيقاتك الصوتية المخصصة المستندة إلى المستعرض.

مع استمرار نمو كلٍّ من واجهتي برمجة التطبيقات ، ستفتح بعض الاحتمالات المثيرة جدًا لمطوري جافا سكريبت ؛ مما يسمح لنا بتطوير منصة عمل صوتية (DAW) كاملة الميزات ، قائمة على المستعرض ، والتي ستكون قادرة على التنافس مع مكافئات الفلاش الخاصة بهم. وبالنسبة لمطوري سطح المكتب ، يمكنك أيضًا البدء في إنشاء تطبيقاتك الخاصة عبر الأنظمة الأساسية باستخدام أدوات مثل node-webkit. نأمل أن يؤدي هذا إلى إنتاج جيل جديد من أدوات الموسيقى لعشاق الموسيقى التي ستمكّن المستخدمين من خلال سد الفجوة بين العالم المادي والسحابة.