บทช่วยสอน MIDI: การสร้างแอปพลิเคชันเสียงบนเบราว์เซอร์ที่ควบคุมโดยฮาร์ดแวร์ MIDI

เผยแพร่แล้ว: 2022-03-11

แม้ว่า Web Audio API จะได้รับความนิยมเพิ่มขึ้น โดยเฉพาะในหมู่นักพัฒนาเกม HTML5 แต่ Web MIDI API ยังไม่ค่อยเป็นที่รู้จักในหมู่นักพัฒนาฟรอนท์เอนด์ ส่วนใหญ่อาจเกี่ยวข้องกับการขาดการสนับสนุนและเอกสารที่เข้าถึงได้ในปัจจุบัน ขณะนี้ Web MIDI API รองรับเฉพาะใน Google Chrome โดยที่คุณเปิดใช้งานการตั้งค่าสถานะพิเศษ ปัจจุบันผู้ผลิตเบราว์เซอร์ให้ความสำคัญกับ API นี้เพียงเล็กน้อย เนื่องจากมีแผนจะเป็นส่วนหนึ่งของมาตรฐาน ES7

MIDI (ย่อมาจาก Musical Instrument Digital Interface) ได้รับการออกแบบในช่วงต้นยุค 80 โดยตัวแทนในวงการเพลงหลายคน เป็นโปรโตคอลการสื่อสารมาตรฐานสำหรับอุปกรณ์ดนตรีอิเล็กทรอนิกส์ แม้ว่าโปรโตคอลอื่นๆ เช่น OSC จะได้รับการพัฒนาตั้งแต่นั้นเป็นต้นมา สามสิบปีต่อมา MIDI ยังคงเป็นโปรโตคอลการสื่อสารโดยพฤตินัยสำหรับผู้ผลิตฮาร์ดแวร์ด้านเสียง คุณจะลำบากใจที่จะหาผู้ผลิตเพลงสมัยใหม่ที่ไม่มีอุปกรณ์ MIDI อย่างน้อยหนึ่งเครื่องในสตูดิโอของเขา

ด้วยการพัฒนาอย่างรวดเร็วและการนำ Web Audio API มาใช้ ตอนนี้เราสามารถเริ่มสร้างแอปพลิเคชันบนเบราว์เซอร์ที่เชื่อมช่องว่างระหว่างคลาวด์และโลกทางกายภาพ Web MIDI API ไม่เพียงแต่ช่วยให้เราสร้างซินธิไซเซอร์และเอฟเฟกต์เสียงเท่านั้น แต่เรายังสามารถเริ่มสร้าง DAW (Digital Audio Workstation) บนเบราว์เซอร์ได้ ซึ่งมีฟีเจอร์และประสิทธิภาพที่คล้ายคลึงกันกับอุปกรณ์ที่ใช้แฟลชในปัจจุบัน (ดูตัวอย่าง Audiotool เป็นต้น ).

ในบทช่วยสอน MIDI นี้ ฉันจะแนะนำคุณเกี่ยวกับพื้นฐานของ Web MIDI API และเราจะสร้าง monosynth ง่ายๆ ที่คุณจะสามารถเล่นกับอุปกรณ์ MIDI ที่คุณชื่นชอบได้ ซอร์สโค้ดแบบเต็มมีให้ที่นี่ และคุณสามารถทดสอบการสาธิตสดได้โดยตรง หากคุณไม่ได้เป็นเจ้าของอุปกรณ์ MIDI คุณยังสามารถทำตามบทช่วยสอนนี้ได้โดยดูที่สาขา "แป้นพิมพ์" ของที่เก็บ GitHub ซึ่งรองรับการรองรับพื้นฐานสำหรับแป้นพิมพ์คอมพิวเตอร์ของคุณ คุณจึงสามารถเล่นโน้ตและเปลี่ยนอ็อกเทฟได้ นี่เป็นเวอร์ชันที่มีให้ในการสาธิตสดด้วย อย่างไรก็ตาม เนื่องจากข้อจำกัดของฮาร์ดแวร์คอมพิวเตอร์ ความเร็วและ detune จะถูกปิดใช้งานเมื่อคุณใช้แป้นพิมพ์คอมพิวเตอร์เพื่อควบคุมซินธิไซเซอร์ โปรดดูไฟล์ readme บน GitHub เพื่ออ่านเกี่ยวกับการแมปคีย์/โน้ต

บทช่วยสอน midi ของ Toptal

ข้อกำหนดเบื้องต้นของการสอน Midi

คุณจะต้องมีสิ่งต่อไปนี้สำหรับบทช่วยสอน MIDI นี้:

  • Google Chrome (เวอร์ชัน 38 ขึ้นไป) พร้อม #enable-web-midi flag ที่เปิดใช้งาน
  • (ทางเลือก) อุปกรณ์ MIDI ที่สามารถเรียกโน้ต เชื่อมต่อกับคอมพิวเตอร์ของคุณ

เราจะใช้ Angular.js เพื่อนำโครงสร้างเล็กน้อยมาสู่แอปพลิเคชันของเรา ดังนั้นความรู้พื้นฐานของกรอบงานจึงเป็นข้อกำหนดเบื้องต้น

เริ่มต้น

เราจะปรับแอปพลิเคชัน MIDI ของเราให้เป็นโมดูลโดยแยกออกเป็น 3 โมดูล:

  • WebMIDI: จัดการอุปกรณ์ MIDI ต่างๆ ที่เชื่อมต่อกับคอมพิวเตอร์ของคุณ
  • WebAudio: จัดหาแหล่งเสียงสำหรับ synth . ของเรา
  • 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 บนอ็อบเจ็กต์อินพุตหรือเอาต์พุต เพื่อดึงข้อมูลรายการอุปกรณ์ที่เกี่ยวข้อง เมธอดนี้ทำหน้าที่เป็นฟังก์ชันตัวสร้าง และส่งคืนตัววนซ้ำ อีกครั้ง API นี้มีขึ้นเพื่อเป็นส่วนหนึ่งของ ES7; ดังนั้นการนำพฤติกรรมเหมือนตัวสร้างมาใช้จึงสมเหตุสมผล แม้ว่าจะไม่ได้ตรงไปตรงมาเหมือนการใช้งานดั้งเดิมก็ตาม

สุดท้าย เราสามารถดึงข้อมูลจำนวนอุปกรณ์ผ่านคุณสมบัติ size ของวัตถุ iterator หากมีอุปกรณ์อย่างน้อยหนึ่งเครื่อง เราเพียงแค่วนซ้ำผลลัพธ์โดยเรียกใช้เมธอด next ของอ็อบเจ็กต์ iterator และผลักแต่ละอุปกรณ์ไปยังอาร์เรย์ที่กำหนดไว้ในขอบเขต $ ที่ส่วนหน้า เราสามารถใช้กล่องเลือกอย่างง่าย ซึ่งจะแสดงรายการอุปกรณ์อินพุตที่มีอยู่ทั้งหมด และให้เราเลือกอุปกรณ์ที่เราต้องการใช้เป็นอุปกรณ์ที่ใช้งานอยู่เพื่อควบคุมการสังเคราะห์เว็บ:

 <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 ซึ่งเราจะใช้ในการเชื่อมต่ออุปกรณ์ที่ใช้งานอยู่นี้กับซินธ์ในภายหลัง

เชื่อมต่ออุปกรณ์ที่ใช้งานนี้กับ synth

โมดูล WebAudio: สร้างเสียงรบกวน

WebAudio API ไม่เพียงแต่ช่วยให้เราเล่นไฟล์เสียงได้เท่านั้น แต่ยังสร้างเสียงโดยการสร้างส่วนประกอบสำคัญของซินธิไซเซอร์ขึ้นใหม่ เช่น ออสซิลเลเตอร์ ฟิลเตอร์ และเกนโหนด เป็นต้น

สร้าง Oscillator

บทบาทของออสซิลเลเตอร์คือการส่งสัญญาณรูปคลื่น รูปคลื่นมีหลายประเภท โดยใน WebAudio API รองรับสี่รูปแบบ: ไซน์ สี่เหลี่ยม สามเหลี่ยม และฟันเลื่อย รูปแบบคลื่นมีการกล่าวกันว่า "สั่น" ที่ความถี่หนึ่ง แต่ก็เป็นไปได้ที่รูปแบบหนึ่งจะกำหนดตารางคลื่นที่กำหนดเองได้หากจำเป็น มนุษย์สามารถได้ยินช่วงความถี่ต่างๆ ได้ ซึ่งเรียกว่าเสียง อีกทางหนึ่ง เมื่อพวกเขาสั่นที่ความถี่ต่ำ ออสซิลเลเตอร์ยังสามารถช่วยเราสร้าง LFO (“ออสซิลเลเตอร์ความถี่ต่ำ”) เพื่อให้เราสามารถมอดูเลตเสียงของเรา (แต่นั่นอยู่นอกเหนือขอบเขตของบทช่วยสอนนี้)

สิ่งแรกที่เราต้องทำเพื่อสร้างเสียงคือสร้างอินสแตนซ์ 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:

 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 และ Volume Control

เราต้องการส่วนประกอบอีกสององค์ประกอบเพื่อทำให้เอ็นจิ้นเสียงพื้นฐานของเราสมบูรณ์: ฟิลเตอร์มัลติพาส เพื่อให้เสียงของเรามีรูปทรง และเกนโหนดเพื่อควบคุมระดับเสียงของเราและเปิดและปิดระดับเสียง ในการทำเช่นนั้น เราสามารถดำเนินการในลักษณะเดียวกับที่เราทำกับออสซิลเลเตอร์: สร้างบริการที่ส่งคืนฟังก์ชันด้วยเมธอดของแรปเปอร์ สิ่งที่เราต้องทำคือจัดเตรียมอินสแตนซ์ AudioContext และเรียกใช้เมธอดที่เหมาะสม

เราสร้างตัวกรองโดยการเรียกเมธอด createBiquadFilter ของอินสแตนซ์ AudioContext:

 ctx.createBiquadFilter();

ในทำนองเดียวกัน สำหรับโหนดเกน เราเรียกเมธอด createGain :

 ctx.createGain();

โมดูล WebSynth: การเดินสายไฟขึ้น

ตอนนี้เราเกือบจะพร้อมแล้วที่จะสร้างอินเทอร์เฟซ synth และเชื่อมต่ออุปกรณ์ MIDI กับแหล่งกำเนิดเสียงของเรา ขั้นแรก เราต้องเชื่อมต่อเอ็นจิ้นเสียงของเราเข้าด้วยกัน และเตรียมพร้อมเพื่อรับโน้ต MIDI ในการเชื่อมต่อเอ็นจิ้นเสียง เราเพียงแค่สร้างอินสแตนซ์ใหม่ของส่วนประกอบที่เราต้องการ จากนั้น "เชื่อมต่อ" เข้าด้วยกันโดยใช้วิธีการ connect ที่มีให้สำหรับอินสแตนซ์ของส่วนประกอบแต่ละส่วน วิธีการ connect ใช้อาร์กิวเมนต์เดียว ซึ่งเป็นส่วนประกอบที่คุณต้องการเชื่อมต่ออินสแตนซ์ปัจจุบัน เป็นไปได้ที่จะจัดกลุ่มส่วนประกอบที่ซับซ้อนมากขึ้นเนื่องจากวิธีการ connect สามารถเชื่อมต่อโหนดหนึ่งกับโมดูเลเตอร์หลายตัว (ทำให้สามารถใช้สิ่งต่าง ๆ เช่น cross-fading และอื่น ๆ ได้)

 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 ไปยังเอ็นจิ้นเสียง เราจะตั้งค่าผู้เฝ้าดูบนกล่องเลือกอุปกรณ์เพื่อ "เสียบ" เข้ากับ synth ของเรา จากนั้นเราจะฟังข้อความ 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 Object และส่งผ่านไปยังวิธีการที่เหมาะสม 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: การแสดงภาพเสียงของเรา

คุณลักษณะที่น่าสนใจอีกประการหนึ่งที่เราสามารถเพิ่มลงใน synth ของเราคือโหนดตัววิเคราะห์ ซึ่งช่วยให้เราสามารถแสดงรูปคลื่นของเสียงของเราโดยใช้ผ้าใบเพื่อแสดงผล การสร้างโหนดตัววิเคราะห์นั้นซับซ้อนกว่าออบเจ็กต์ AudioContext อื่นๆ เล็กน้อย เนื่องจากจำเป็นต้องสร้างโหนด scriptProcessor เพื่อทำการวิเคราะห์จริงๆ เราเริ่มต้นด้วยการเลือกองค์ประกอบผ้าใบบน 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 จะเริ่มเหตุการณ์ 'audioprocess' ในช่วงเวลาหนึ่ง เมื่อเหตุการณ์นี้เริ่มทำงาน เราจะคำนวณความถี่เฉลี่ยที่ตัววิเคราะห์จับ เคลียร์พื้นที่ และวาดกราฟความถี่ใหม่โดยเรียกใช้เมธอด 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); }

ตอนนี้เรามีวิชวลไลเซอร์ที่ดี ซึ่งช่วยให้เราแสดงรูปคลื่นของซินธ์ของเราได้แบบเรียลไทม์! สิ่งนี้เกี่ยวข้องกับงานเล็กน้อยในการตั้งค่า แต่ก็น่าสนใจและลึกซึ้งมาก โดยเฉพาะอย่างยิ่งเมื่อใช้ตัวกรอง

สร้าง Synth ของเรา: การเพิ่ม Velocity & Detune

ณ จุดนี้ในบทช่วยสอน MIDI ของเรา เรามีซินธ์ที่เจ๋งมาก - แต่มันเล่นโน้ตทุกตัวในโวลุ่มเดียวกัน เนื่องจากแทนที่จะจัดการข้อมูลความเร็วอย่างเหมาะสม เราเพียงแค่ตั้งค่าระดับเสียงเป็นค่าคงที่ 1.0 มาเริ่มด้วยการแก้ไขปัญหากัน จากนั้นเราจะมาดูกันว่าเราจะสามารถเปิดใช้งานวงล้อ detune ที่คุณพบบนคีย์บอร์ด MIDI ทั่วไปได้อย่างไร

เปิดใช้งาน Velocity

หากคุณไม่คุ้นเคย 'ความเร็ว' จะสัมพันธ์กับความแรงที่คุณกดแป้นบนแป้นพิมพ์ของคุณ จากค่านี้ เสียงที่สร้างขึ้นดูเหมือนจะเบาลงหรือดังขึ้น

ในการสังเคราะห์บทช่วยสอน 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);

และนั่นแหล่ะ! เมื่อเราเล่น synth เราจะสังเกตเห็นความดังที่แตกต่างกันไปตามความแรงที่เรากดแป้นบนแป้นพิมพ์

เปิดใช้งาน Detune Wheel บนคีย์บอร์ด MIDI ของคุณ

คีย์บอร์ด MIDI ส่วนใหญ่มีล้อเลื่อน วงล้อช่วยให้คุณปรับเปลี่ยนความถี่ของโน้ตที่กำลังเล่นอยู่ได้เล็กน้อย สร้างเอฟเฟกต์ที่น่าสนใจที่เรียกว่า 'detune' การดำเนินการนี้ค่อนข้างง่ายเมื่อคุณเรียนรู้วิธีใช้ MIDI เนื่องจากวงล้อ detune ยังเริ่มเหตุการณ์ MidiMessage ด้วยรหัสเหตุการณ์ (224) ซึ่งเราสามารถรับฟังและดำเนินการโดยการคำนวณค่าความถี่ใหม่และอัปเดตออสซิลเลเตอร์

อันดับแรก เราต้องจับเหตุการณ์ใน synth ของเรา ในการทำเช่นนั้น เราเพิ่มกรณีพิเศษให้กับคำสั่ง switch ที่เราสร้างขึ้นในการเรียกกลับ _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 พื้นฐานนี้ แต่เราสามารถบันทึกเป็นแบบฝึกหัดเพื่อขัดส่วนต่อประสานผู้ใช้ในภายหลัง ซึ่งอาจมีลักษณะดังนี้:

ส่วนต่อประสานผู้ใช้ midi ขัดเงา

การผูกส่วนต่อประสานกับ Audio Engine

เราควรกำหนดวิธีการสองสามวิธีในการผูกการควบคุมเหล่านี้กับเอ็นจิ้นเสียงของเรา

การควบคุมออสซิลเลเตอร์

สำหรับออสซิลเลเตอร์ เราต้องการเพียงวิธีการที่ทำให้เราตั้งค่าประเภทออสซิลเลเตอร์ได้:

 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 API นี่เป็น API ที่สมบูรณ์มาก และเราครอบคลุมความสามารถเพียงไม่กี่อย่างในบทช่วยสอนนี้ WebAudio API ไม่เหมือนกับ WebMIDI API ตรงที่ WebAudio API ได้รับการจัดทำเป็นเอกสารไว้เป็นอย่างดี โดยเฉพาะใน Mozilla Developer Network Mozilla Developer Network มีตัวอย่างโค้ดมากมายและรายการรายละเอียดของอาร์กิวเมนต์และเหตุการณ์ต่างๆ สำหรับแต่ละส่วนประกอบ ซึ่งจะช่วยให้คุณใช้แอปพลิเคชันเสียงบนเบราว์เซอร์ที่คุณกำหนดเองได้

ในขณะที่ API ทั้งสองเติบโตอย่างต่อเนื่อง จะเป็นการเปิดโอกาสที่น่าสนใจสำหรับนักพัฒนา JavaScript ทำให้เราสามารถพัฒนา DAW ที่มีคุณสมบัติครบถ้วนบนเบราว์เซอร์ซึ่งสามารถแข่งขันกับ Flash ที่เทียบเท่าได้ และสำหรับนักพัฒนาเดสก์ท็อป คุณยังสามารถเริ่มสร้างแอปพลิเคชันข้ามแพลตฟอร์มโดยใช้เครื่องมือต่างๆ เช่น node-webkit หวังว่านี่จะทำให้เกิดเครื่องมือดนตรีรุ่นใหม่สำหรับออดิโอไฟล์ที่จะให้อำนาจแก่ผู้ใช้โดยเชื่อมช่องว่างระหว่างโลกทางกายภาพกับคลาวด์