MIDI-Tutorial: Erstellen von browserbasierten Audioanwendungen, die von MIDI-Hardware gesteuert werden
Veröffentlicht: 2022-03-11Während die Web Audio API immer beliebter wird, insbesondere bei HTML5-Spieleentwicklern, ist die Web MIDI API bei Frontend-Entwicklern noch wenig bekannt. Ein großer Teil davon hat wahrscheinlich mit dem derzeitigen Mangel an Unterstützung und zugänglicher Dokumentation zu tun; Die Web-MIDI-API wird derzeit nur in Google Chrome unterstützt, vorausgesetzt, Sie aktivieren ein spezielles Flag dafür. Browserhersteller legen derzeit wenig Wert auf diese API, da sie Teil des ES7-Standards sein soll.
MIDI (kurz für Musical Instrument Digital Interface) wurde Anfang der 80er Jahre von mehreren Vertretern der Musikindustrie entwickelt und ist ein Standard-Kommunikationsprotokoll für elektronische Musikgeräte. Auch wenn seitdem andere Protokolle wie OSC entwickelt wurden; dreißig Jahre später ist MIDI immer noch das De-facto-Kommunikationsprotokoll für Hersteller von Audiohardware. Sie werden kaum einen modernen Musikproduzenten finden, der nicht mindestens ein MIDI-Gerät in seinem Studio besitzt.
Mit der schnellen Entwicklung und Einführung der Web Audio API können wir jetzt damit beginnen, browserbasierte Anwendungen zu entwickeln, die die Lücke zwischen der Cloud und der physischen Welt schließen. Mit der Web-MIDI-API können wir nicht nur Synthesizer und Audioeffekte erstellen, sondern wir können sogar damit beginnen, eine browserbasierte DAW (Digital Audio Workstation) zu erstellen, die in Funktion und Leistung ihren aktuellen Flash-basierten Gegenstücken ähnelt (sehen Sie sich zum Beispiel Audiotool an ).
In diesem MIDI-Tutorial werde ich Sie durch die Grundlagen der Web-MIDI-API führen, und wir werden einen einfachen Monosynth bauen, den Sie mit Ihrem bevorzugten MIDI-Gerät spielen können. Der vollständige Quellcode ist hier verfügbar, und Sie können die Live-Demo direkt testen. Wenn Sie kein MIDI-Gerät besitzen, können Sie diesem Tutorial dennoch folgen, indem Sie sich den „Keyboard“-Zweig des GitHub-Repositorys ansehen, der grundlegende Unterstützung für Ihre Computertastatur ermöglicht, sodass Sie Noten spielen und Oktaven ändern können. Dies ist auch die Version, die als Live-Demo verfügbar ist. Aufgrund von Beschränkungen der Computerhardware sind Velocity und Detune jedoch beide deaktiviert, wenn Sie Ihre Computertastatur zur Steuerung des Synthesizers verwenden. Bitte lesen Sie die Readme-Datei auf GitHub, um mehr über die Tasten-/Notenzuordnung zu erfahren.
Voraussetzungen für das Midi-Tutorial
Für dieses MIDI-Tutorial benötigen Sie Folgendes:
- Google Chrome (Version 38 oder höher) mit
#enable-web-midi
Flag - (Optional) Ein an Ihren Computer angeschlossenes MIDI-Gerät, das Noten triggern kann
Wir werden auch Angular.js verwenden, um unserer Anwendung ein wenig Struktur zu verleihen. Grundkenntnisse des Frameworks sind daher Voraussetzung.
Einstieg
Wir werden unsere MIDI-Anwendung von Grund auf modularisieren, indem wir sie in 3 Module aufteilen:
- WebMIDI: Umgang mit den verschiedenen an Ihren Computer angeschlossenen MIDI-Geräten
- WebAudio: Bereitstellung der Audioquelle für unseren Synthesizer
- WebSynth: Verbinden der Weboberfläche mit der Audio-Engine
Ein App
-Modul behandelt die Benutzerinteraktion mit der Web-Benutzeroberfläche. Unsere Bewerbungsstruktur könnte in etwa so aussehen:
|- app |-- js |--- midi.js |--- audio.js |--- synth.js |--- app.js |- index.html
Sie sollten auch die folgenden Bibliotheken installieren, die Ihnen beim Erstellen Ihrer Anwendung helfen: Angular.js, Bootstrap und jQuery. Der wahrscheinlich einfachste Weg, diese zu installieren, ist über Bower.
Das WebMIDI-Modul: Verbindung mit der realen Welt
Beginnen wir damit, herauszufinden, wie MIDI verwendet wird, indem wir unsere MIDI-Geräte mit unserer Anwendung verbinden. Dazu erstellen wir eine einfache Factory, die eine einzelne Methode zurückgibt. Um eine Verbindung zu unseren MIDI-Geräten über die Web-MIDI-API herzustellen, müssen wir die Methode navigator.requestMIDIAccess
aufrufen:
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 }; }]);
Und das ist so ziemlich alles!
Die Methode „ requestMIDIAccess
“ gibt ein Versprechen zurück, sodass wir es einfach direkt zurückgeben und das Ergebnis des Versprechens im Controller unserer App verarbeiten können:
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); }); }]);
Wie bereits erwähnt, gibt die Methode „ requestMIDIAccess
“ ein Promise zurück, indem sie ein Objekt mit zwei Eigenschaften an die Methode „ then
“ übergibt: Eingänge und Ausgänge.
In früheren Versionen von Chrome waren diese beiden Eigenschaften Methoden, mit denen Sie eine Reihe von Eingabe- und Ausgabegeräten direkt abrufen konnten. In den neuesten Updates sind diese Eigenschaften jetzt jedoch Objekte. Das macht einen großen Unterschied, da wir jetzt die Methode values
entweder für das input- oder das output-Objekt aufrufen müssen, um die entsprechende Geräteliste abzurufen. Diese Methode fungiert als Generatorfunktion und gibt einen Iterator zurück. Auch diese API soll Teil von ES7 sein; Daher ist die Implementierung eines generatorähnlichen Verhaltens sinnvoll, auch wenn es nicht so einfach ist wie die ursprüngliche Implementierung.
Schließlich können wir die Anzahl der Geräte über die Eigenschaft size
des Iterator-Objekts abrufen. Wenn es mindestens ein Gerät gibt, iterieren wir einfach über das Ergebnis, indem wir die next
-Methode des Iterator-Objekts aufrufen und jedes Gerät in ein Array verschieben, das im $scope definiert ist. Am Frontend können wir ein einfaches Auswahlfeld implementieren, das alle verfügbaren Eingabegeräte auflistet und uns auswählen lässt, welches Gerät wir als aktives Gerät zur Steuerung des Websynthesizers verwenden möchten:
<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>
Wir haben dieses Auswahlfeld an eine $scope-Variable namens activeDevice
, die wir später verwenden werden, um dieses aktive Gerät mit dem Synthesizer zu verbinden.
Das WebAudio-Modul: Lärm machen
Mit der WebAudio-API können wir nicht nur Sounddateien abspielen, sondern auch Sounds erzeugen, indem wir unter anderem die wesentlichen Komponenten von Synthesizern wie Oszillatoren, Filter und Gain-Knoten nachbilden.
Erstellen Sie einen Oszillator
Die Rolle von Oszillatoren besteht darin, eine Wellenform auszugeben. Es gibt verschiedene Arten von Wellenformen, von denen vier in der WebAudio-API unterstützt werden: Sinus, Rechteck, Dreieck und Sägezahn. Es wird gesagt, dass Wellenformen mit einer bestimmten Frequenz „schwingen“, aber es ist auch möglich, bei Bedarf eine eigene benutzerdefinierte Wavetable zu definieren. Ein bestimmter Bereich von Frequenzen ist für den Menschen hörbar – sie werden als Töne bezeichnet. Wenn sie bei niedrigen Frequenzen schwingen, können uns Oszillatoren alternativ auch dabei helfen, LFOs („Low Frequency Oscillator“) zu bauen, damit wir unsere Sounds modulieren können (aber das würde den Rahmen dieses Tutorials sprengen).
Das erste, was wir tun müssen, um einen Sound zu erzeugen, ist, einen neuen AudioContext
zu instanziieren:
function _createContext() { self.ctx = new $window.AudioContext(); }
Von dort aus können wir alle Komponenten instanziieren, die von der WebAudio-API bereitgestellt werden. Da wir möglicherweise mehrere Instanzen jeder Komponente erstellen, ist es sinnvoll, Dienste zu erstellen, um neue, eindeutige Instanzen der benötigten Komponenten erstellen zu können. Beginnen wir mit dem Erstellen des Dienstes zum Generieren eines neuen Oszillators:
angular .module('WebAudio', []) .service('OSC', function() { var self; function Oscillator(ctx) { self = this; self.osc = ctx.createOscillator(); return self; } });
Wir können jetzt nach Belieben neue Oszillatoren instanziieren, indem wir die zuvor erstellte AudioContext-Instanz als Argument übergeben. Um die Dinge später einfacher zu machen, werden wir einige Wrapper-Methoden hinzufügen – reiner syntaktischer Zucker – und die Oszillator-Funktion zurückgeben:
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;
Erstellen Sie einen Multipass-Filter und eine Lautstärkeregelung
Wir brauchen zwei weitere Komponenten, um unsere grundlegende Audio-Engine zu vervollständigen: einen Multipass-Filter, um unserem Sound ein wenig Form zu geben, und einen Gain-Knoten, um die Lautstärke unseres Sounds zu steuern und die Lautstärke ein- und auszuschalten. Dazu können wir genauso vorgehen wie beim Oszillator: Erstellen Sie Dienste, die eine Funktion mit einigen Wrapper-Methoden zurückgeben. Wir müssen lediglich die AudioContext-Instanz bereitstellen und die entsprechende Methode aufrufen.
Wir erstellen einen Filter, indem wir die createBiquadFilter
Methode der AudioContext-Instanz aufrufen:
ctx.createBiquadFilter();
In ähnlicher Weise rufen wir für einen Gain-Knoten die createGain
Methode auf:
ctx.createGain();
Das WebSynth-Modul: Dinge verkabeln
Jetzt sind wir fast bereit, unser Synth-Interface zu bauen und MIDI-Geräte an unsere Audioquelle anzuschließen. Zuerst müssen wir unsere Audio-Engine miteinander verbinden und sie für den Empfang von MIDI-Noten vorbereiten. Um die Audio-Engine zu verbinden, erstellen wir einfach neue Instanzen der benötigten Komponenten und „verbinden“ sie dann miteinander, indem wir die connect
verwenden, die für die Instanzen der einzelnen Komponenten verfügbar ist. Die connect
-Methode akzeptiert ein Argument, das einfach die Komponente ist, mit der Sie die aktuelle Instanz verbinden möchten. Es ist möglich, eine ausgefeiltere Kette von Komponenten zu orchestrieren, da die connect
einen Knoten mit mehreren Modulatoren verbinden kann (wodurch Dinge wie Überblendung und mehr implementiert werden können).
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); }
Wir haben gerade die interne Verkabelung unserer Audio-Engine gebaut. Sie können ein bisschen herumspielen und verschiedene Verkabelungskombinationen ausprobieren, aber denken Sie daran, die Lautstärke zu verringern, um nicht taub zu werden. Jetzt können wir die MIDI-Schnittstelle mit unserer Anwendung verbinden und MIDI-Nachrichten an die Audio-Engine senden. Wir richten einen Watcher in der Geräteauswahlbox ein, um ihn virtuell in unseren Synthesizer zu „stecken“. Wir werden dann MIDI-Nachrichten hören, die vom Gerät kommen, und die Informationen an die Audio-Engine weitergeben:
// 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; }
Hier hören wir MIDI-Events vom Gerät, analysieren die Daten vom MidiEvent-Objekt und übergeben sie an die entsprechende Methode; entweder noteOn
oder noteOff
, basierend auf dem Ereigniscode (144 für noteOn, 128 für noteOff). Wir können jetzt die Logik in den jeweiligen Methoden im Audiomodul hinzufügen, um tatsächlich einen Sound zu erzeugen:
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); } }
Hier tut sich einiges. In der Methode noteOn
schieben wir zuerst die aktuelle Note in ein Array von Noten. Obwohl wir einen Monosynthesizer bauen (d.h. wir können nur eine Note gleichzeitig spielen), können wir immer noch mehrere Finger gleichzeitig auf der Tastatur haben. Wir müssen also alle diese Noten in eine Warteschlange stellen, damit beim Loslassen einer Note die nächste gespielt wird. Wir müssen dann den Oszillator stoppen, um die neue Frequenz zuzuweisen, die wir mit ein bisschen Mathematik von einer MIDI-Note (Skala von 0 bis 127) in einen tatsächlichen Frequenzwert umwandeln:
function _mtof(note) { return 440 * Math.pow(2, (note - 69) / 12); }
In der Methode noteOff
beginnen wir damit, die Note im Array der aktiven Noten zu finden und zu entfernen. Wenn es dann die einzige Note im Array war, schalten wir einfach die Lautstärke aus.
Das zweite Argument der setVolume
Methode ist die Übergangszeit, d. h. wie lange es dauert, bis die Verstärkung den neuen Lautstärkewert erreicht. In musikalischer Hinsicht entspricht eine eingeschaltete Note der Attack-Zeit, und eine ausgeschaltete Note entspricht der Release-Zeit.

Das WebAnalyser-Modul: Visualisierung unseres Sounds
Eine weitere interessante Funktion, die wir unserem Synthesizer hinzufügen können, ist ein Analysatorknoten, der es uns ermöglicht, die Wellenform unseres Sounds anzuzeigen, indem wir Canvas verwenden, um ihn zu rendern. Das Erstellen eines Analyseknotens ist etwas komplizierter als bei anderen AudioContext-Objekten, da auch ein scriptProcessor-Knoten erstellt werden muss, um die Analyse tatsächlich durchzuführen. Wir beginnen mit der Auswahl des Canvas-Elements im 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; }
Dann fügen wir eine connect
hinzu, in der wir sowohl den Analysator als auch den Skriptprozessor erstellen:
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); } };
Zuerst erstellen wir ein scriptProcessor-Objekt und verbinden es mit dem Ziel. Dann erstellen wir den Analysator selbst, den wir mit dem Audioausgang des Oszillators oder Filters speisen. Beachten Sie, dass wir den Audioausgang noch mit dem Ziel verbinden müssen, damit wir es hören können! Wir müssen auch die Verlaufsfarben unseres Diagramms definieren – dies geschieht durch Aufrufen der createLinearGradient
Methode des Canvas-Elements.
Schließlich löst der scriptProcessor in einem Intervall ein 'audioprocess'-Ereignis aus; Wenn dieses Ereignis ausgelöst wird, berechnen wir die vom Analysator erfassten durchschnittlichen Frequenzen, löschen die Leinwand und zeichnen das neue Frequenzdiagramm neu, indem wir die Methode drawSpectrum
aufrufen:
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)); } }
Zu guter Letzt müssen wir die Verkabelung unserer Audio-Engine ein wenig ändern, um diese neue Komponente aufzunehmen:
// 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); }
Wir haben jetzt einen schönen Visualizer, der es uns ermöglicht, die Wellenform unseres Synthesizers in Echtzeit anzuzeigen! Dies erfordert ein wenig Arbeit bei der Einrichtung, ist aber sehr interessant und aufschlussreich, insbesondere bei der Verwendung von Filtern.
Aufbauend auf unserem Synthesizer: Hinzufügen von Velocity & Detune
An dieser Stelle in unserem MIDI-Tutorial haben wir einen ziemlich coolen Synthesizer – aber er spielt jede Note mit der gleichen Lautstärke. Das liegt daran, dass wir die Velocity-Daten nicht richtig handhaben, sondern einfach die Lautstärke auf einen festen Wert von 1,0 setzen. Beginnen wir damit, das zu beheben, und dann werden wir sehen, wie wir das Detune-Rad aktivieren können, das Sie auf den meisten gängigen MIDI-Keyboards finden.
Geschwindigkeit aktivieren
Wenn Sie damit nicht vertraut sind, bezieht sich die „Geschwindigkeit“ darauf, wie stark Sie die Taste auf Ihrer Tastatur drücken. Basierend auf diesem Wert erscheint der erzeugte Klang entweder leiser oder lauter.
In unserem MIDI-Tutorial-Synthesizer können wir dieses Verhalten emulieren, indem wir einfach mit der Lautstärke des Gain-Knotens spielen. Dazu müssen wir zunächst ein bisschen rechnen, um die MIDI-Daten in einen Float-Wert zwischen 0,0 und 1,0 umzuwandeln, um sie an den Gain-Knoten zu übergeben:
function _vtov (velocity) { return (velocity / 127).toFixed(2); }
Der Velocity-Bereich eines MIDI-Geräts reicht von 0 bis 127, also teilen wir diesen Wert einfach durch 127 und geben einen Float-Wert mit zwei Dezimalstellen zurück. Dann können wir die _noteOn
Methode aktualisieren, um den berechneten Wert an den Verstärkungsknoten zu übergeben:
self.amp.setVolume(_vtov(velocity), self.settings.attack);
Und das ist es! Wenn wir jetzt unseren Synthesizer spielen, werden wir feststellen, dass die Lautstärke variiert, je nachdem, wie hart wir die Tasten auf unserer Tastatur anschlagen.
Aktivieren des Detune-Rads auf Ihrem MIDI-Keyboard
Die meisten MIDI-Keyboards verfügen über ein Detune-Rad; Mit dem Rad können Sie die Frequenz der aktuell gespielten Note leicht verändern, wodurch ein interessanter Effekt entsteht, der als „Detune“ bekannt ist. Dies ist ziemlich einfach zu implementieren, wenn Sie lernen, MIDI zu verwenden, da das Detune-Rad auch ein MidiMessage-Ereignis mit seinem eigenen Ereigniscode (224) auslöst, den wir abhören und darauf reagieren können, indem wir den Frequenzwert neu berechnen und den Oszillator aktualisieren.
Zuerst müssen wir das Ereignis in unserem Synthesizer abfangen. Dazu fügen wir der switch-Anweisung, die wir im _onmidimessage
Callback erstellt haben, einen zusätzlichen Fall hinzu:
case 224: // the detune value is the third argument of the MidiEvent.data array Engine.detune(e.data[2]); break;
Dann definieren wir die detune
Methode auf der Audio-Engine:
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; } } }
Der voreingestellte Verstimmungswert ist 64, was bedeutet, dass keine Verstimmung angewendet wird, also geben wir in diesem Fall einfach die aktuelle Frequenz an den Oszillator weiter.
Schließlich müssen wir auch die Methode _noteOff
aktualisieren, um die Verstimmung zu berücksichtigen, falls eine andere Note in die Warteschlange gestellt wird:
self.osc1.setFrequency(self.currentFreq + self.detuneAmount, self.settings.portamento);
Erstellen der Schnittstelle
Bisher haben wir nur eine Auswahlbox erstellt, um unser MIDI-Gerät und einen Wellenform-Visualizer auswählen zu können, aber wir haben keine Möglichkeit, den Sound direkt durch Interaktion mit der Webseite zu ändern. Lassen Sie uns eine sehr einfache Schnittstelle mit gängigen Formularelementen erstellen und sie an unsere Audio-Engine binden.
Erstellen eines Layouts für die Benutzeroberfläche
Wir werden verschiedene Formularelemente erstellen, um den Klang unseres Synthesizers zu steuern:
- Eine Funkgruppe zur Auswahl des Oszillatortyps
- Ein Kontrollkästchen zum Aktivieren / Deaktivieren des Filters
- Eine Optionsgruppe zur Auswahl des Filtertyps
- Zwei Bereiche zur Steuerung von Frequenz und Resonanz des Filters
- Zwei Bereiche zur Steuerung von Attack und Release des Gain-Knotens
Wenn wir ein HTML-Dokument für unsere Schnittstelle erstellen, sollten wir ungefähr so aussehen:
<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>
Das Dekorieren der Benutzeroberfläche, um schick auszusehen, ist nichts, was ich in diesem grundlegenden MIDI-Tutorial behandeln werde; Stattdessen können wir es uns als Übung für später aufheben, um die Benutzeroberfläche zu polieren, vielleicht so, dass sie ungefähr so aussieht:
Binden der Schnittstelle an die Audio-Engine
Wir sollten einige Methoden definieren, um diese Steuerelemente an unsere Audio-Engine zu binden.
Steuerung des Oszillators
Für den Oszillator benötigen wir nur eine Methode, mit der wir den Oszillatortyp einstellen können:
Oscillator.prototype.setOscType = function(type) { if(type) { self.osc.type = type; } }
Steuerung des Filters
Für den Filter benötigen wir drei Regler: einen für den Filtertyp, einen für die Frequenz und einen für die Resonanz. Wir können auch die Methoden _connectFilter
und _disconnectFilter
mit dem Wert des Kontrollkästchens verbinden.
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; } }
Kontrolle von Attack und Resonanz
Um unseren Sound ein wenig zu formen, können wir die Attack- und Release-Parameter des Gain-Knotens verändern. Dazu brauchen wir zwei Methoden:
function _setAttack(a) { if(a) { self.settings.attack = a / 1000; } } function _setRelease(r) { if(r) { self.settings.release = r / 1000; } }
Beobachter einrichten
Schließlich müssen wir im Controller unserer App nur ein paar Beobachter einrichten und sie an die verschiedenen Methoden binden, die wir gerade erstellt haben:
$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);
Fazit
In diesem MIDI-Tutorial wurden viele Konzepte behandelt; Meistens entdeckten wir, wie man die WebMIDI-API verwendet, die abgesehen von der offiziellen Spezifikation des W3C ziemlich undokumentiert ist. Die Google Chrome-Implementierung ist ziemlich einfach, obwohl der Wechsel zu einem Iterator-Objekt für die Eingabe- und Ausgabegeräte ein wenig Refactoring für Legacy-Code erfordert, der die alte Implementierung verwendet.
Die WebAudio-API ist eine sehr reichhaltige API, und wir haben in diesem Tutorial nur einige ihrer Funktionen behandelt. Im Gegensatz zur WebMIDI-API ist die WebAudio-API sehr gut dokumentiert, insbesondere im Mozilla Developer Network. Das Mozilla Developer Network enthält eine Fülle von Codebeispielen und detaillierte Listen der verschiedenen Argumente und Ereignisse für jede Komponente, die Ihnen bei der Implementierung Ihrer eigenen benutzerdefinierten browserbasierten Audioanwendungen helfen werden.
Da beide APIs weiter wachsen, wird dies einige sehr interessante Möglichkeiten für JavaScript-Entwickler eröffnen; Dadurch können wir voll funktionsfähige, browserbasierte DAWs entwickeln, die mit ihren Flash-Äquivalenten konkurrieren können. Und für Desktop-Entwickler können Sie mit Tools wie Node-Webkit auch mit der Erstellung Ihrer eigenen plattformübergreifenden Anwendungen beginnen. Hoffentlich wird dies eine neue Generation von Musik-Tools für Audiophile hervorbringen, die Benutzer stärken, indem sie die Lücke zwischen der physischen Welt und der Cloud schließen.