Samouczek MIDI: tworzenie aplikacji audio opartych na przeglądarce kontrolowanych przez sprzęt MIDI

Opublikowany: 2022-03-11

Podczas gdy interfejs API Web Audio zyskuje na popularności, zwłaszcza wśród twórców gier HTML5, interfejs API Web MIDI jest wciąż mało znany wśród twórców frontendu. Duża część tego prawdopodobnie ma związek z obecnym brakiem wsparcia i dostępnej dokumentacji; Web MIDI API jest obecnie obsługiwany tylko w Google Chrome, pod warunkiem, że włączysz dla niego specjalną flagę. Producenci przeglądarek obecnie kładą niewielki nacisk na to API, ponieważ planuje się, że będzie ono częścią standardu ES7.

Zaprojektowany na początku lat 80-tych przez kilku przedstawicieli przemysłu muzycznego, MIDI (skrót od Musical Instrument Digital Interface) jest standardowym protokołem komunikacyjnym dla elektronicznych urządzeń muzycznych. Mimo że od tego czasu opracowano inne protokoły, takie jak OSC; trzydzieści lat później MIDI nadal jest de facto protokołem komunikacyjnym dla producentów sprzętu audio. Trudno będzie znaleźć nowoczesnego producenta muzycznego, który nie posiada w swoim studiu przynajmniej jednego urządzenia MIDI.

Wraz z szybkim rozwojem i przyjęciem interfejsu Web Audio API, możemy teraz zacząć tworzyć aplikacje oparte na przeglądarce, które wypełniają lukę między chmurą a światem fizycznym. Web MIDI API nie tylko pozwala nam budować syntezatory i efekty dźwiękowe, ale możemy nawet zacząć budować oparte na przeglądarce DAW (Digital Audio Workstation) podobne pod względem funkcji i wydajności do ich obecnych odpowiedników opartych na flashu (sprawdź na przykład Audiotool ).

W tym samouczku MIDI przeprowadzę Cię przez podstawy interfejsu API Web MIDI i zbudujemy prosty monosynth, w który będziesz mógł grać na swoim ulubionym urządzeniu MIDI. Pełny kod źródłowy jest dostępny tutaj i możesz bezpośrednio przetestować demo na żywo. Jeśli nie posiadasz urządzenia MIDI, nadal możesz skorzystać z tego samouczka, sprawdzając gałąź „klawiatury” repozytorium GitHub, która zapewnia podstawową obsługę klawiatury komputera, dzięki czemu możesz grać nuty i zmieniać oktawy. Jest to również wersja dostępna jako demo na żywo. Jednakże, ze względu na ograniczenia sprzętu komputerowego, zarówno prędkość, jak i rozstrojenie są wyłączone za każdym razem, gdy używasz klawiatury komputera do sterowania syntezatorem. Zapoznaj się z plikiem readme na GitHub, aby przeczytać o mapowaniu klucza/notatki.

Samouczek midi Toptal

Wymagania wstępne samouczka Midi

Do tego samouczka MIDI potrzebne będą następujące elementy:

  • Google Chrome (wersja 38 lub nowsza) z włączoną flagą #enable-web-midi
  • (Opcjonalnie) Urządzenie MIDI, które może wyzwalać nuty, podłączone do komputera

Będziemy również używać Angular.js, aby wprowadzić trochę struktury do naszej aplikacji; dlatego niezbędna jest podstawowa znajomość frameworka.

Pierwsze kroki

Zmodularyzujemy naszą aplikację MIDI od podstaw, dzieląc ją na 3 moduły:

  • WebMIDI: obsługa różnych urządzeń MIDI podłączonych do komputera
  • WebAudio: dostarczanie źródła dźwięku dla naszego syntezatora
  • WebSynth: podłączanie interfejsu internetowego do silnika audio

Moduł App obsłuży interakcję użytkownika z internetowym interfejsem użytkownika. Nasza struktura aplikacji może wyglądać mniej więcej tak:

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

Powinieneś również zainstalować następujące biblioteki, które pomogą Ci zbudować aplikację: Angular.js, Bootstrap i jQuery. Prawdopodobnie najłatwiej je zainstalować przez Bower.

Moduł WebMIDI: Łączenie się ze światem rzeczywistym

Zacznijmy zastanawiać się, jak korzystać z MIDI, podłączając nasze urządzenia MIDI do naszej aplikacji. W tym celu stworzymy prostą fabrykę zwracającą jedną metodę. Aby połączyć się z naszymi urządzeniami MIDI poprzez Web MIDI API, musimy wywołać metodę 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 }; }]);

I to prawie wszystko!

Metoda requestMIDIAccess zwraca obietnicę, więc możemy ją po prostu zwrócić bezpośrednio i obsłużyć wynik obietnicy w kontrolerze naszej aplikacji:

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

Jak wspomniano, metoda requestMIDIAccess zwraca obietnicę, przekazując obiekt do metody then , z dwiema właściwościami: wejściami i wyjściami.

We wcześniejszych wersjach przeglądarki Chrome te dwie właściwości były metodami umożliwiającymi bezpośrednie pobieranie szeregu urządzeń wejściowych i wyjściowych. Jednak w najnowszych aktualizacjach te właściwości są teraz obiektami. To robi dużą różnicę, ponieważ teraz musimy wywołać metodę values na obiekcie wejściowym lub wyjściowym, aby pobrać odpowiednią listę urządzeń. Ta metoda działa jak funkcja generatora i zwraca iterator. Ponownie, ten interfejs API ma być częścią ES7; dlatego implementacja zachowania generatora ma sens, nawet jeśli nie jest tak prosta, jak oryginalna implementacja.

Na koniec możemy pobrać liczbę urządzeń za pomocą właściwości size obiektu iteratora. Jeśli istnieje co najmniej jedno urządzenie, po prostu iterujemy wynik, wywołując next metodę obiektu iteratora i umieszczając każde urządzenie w tablicy zdefiniowanej w $scope. W interfejsie użytkownika możemy zaimplementować proste pole wyboru, które wyświetli listę wszystkich dostępnych urządzeń wejściowych i pozwoli nam wybrać urządzenie, którego chcemy użyć jako aktywnego urządzenia do sterowania syntezatorem sieciowym:

 <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>

Powiązaliśmy to pole wyboru ze zmienną $scope o nazwie activeDevice , której użyjemy później do połączenia tego aktywnego urządzenia z syntezatorem.

podłącz to aktywne urządzenie do syntezatora

Moduł WebAudio: Wytwarzanie hałasu

WebAudio API pozwala nam nie tylko odtwarzać pliki dźwiękowe, ale także generować dźwięki, odtwarzając podstawowe komponenty syntezatorów, takie jak między innymi oscylatory, filtry i węzły wzmocnienia.

Utwórz oscylator

Rolą oscylatorów jest wyprowadzenie kształtu fali. Istnieją różne rodzaje przebiegów, z których cztery są obsługiwane w interfejsie API WebAudio: sinusoidalny, kwadratowy, trójkątny i piłokształtny. Mówi się, że kształty fal „oscylują” z określoną częstotliwością, ale w razie potrzeby można również zdefiniować własną niestandardową tabelę fal. Pewien zakres częstotliwości jest słyszalny przez ludzi - są one znane jako dźwięki. Alternatywnie, gdy oscylują na niskich częstotliwościach, oscylatory mogą również pomóc nam zbudować LFO („oscylator niskich częstotliwości”), abyśmy mogli modulować nasze dźwięki (ale to wykracza poza zakres tego samouczka).

Pierwszą rzeczą, którą musimy zrobić, aby stworzyć dźwięk, jest utworzenie nowego AudioContext :

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

Stamtąd możemy utworzyć instancję dowolnego komponentu udostępnionego przez WebAudio API. Ponieważ możemy utworzyć wiele instancji każdego komponentu, sensowne jest tworzenie usług, aby móc tworzyć nowe, unikalne instancje potrzebnych komponentów. Zacznijmy od stworzenia usługi do generowania nowego oscylatora:

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

Możemy teraz tworzyć instancje nowych oscylatorów według naszej woli, przekazując jako argument utworzoną wcześniej instancję AudioContext. Aby ułatwić sobie zadanie, dodamy kilka metod opakowujących — zwykły cukier składniowy — i zwrócimy funkcję 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;

Utwórz filtr wieloprzebiegowy i regulację głośności

Potrzebujemy jeszcze dwóch komponentów, aby uzupełnić nasz podstawowy silnik audio: filtra wieloprzepustowego, aby nadać nieco kształt naszemu dźwiękowi, oraz węzła wzmocnienia do kontrolowania głośności naszego dźwięku oraz włączania i wyłączania głośności. Aby to zrobić, możemy postępować w ten sam sposób, co w przypadku oscylatora: utworzyć usługi zwracające funkcję z kilkoma metodami opakowującymi. Wystarczy, że dostarczymy instancję AudioContext i wywołamy odpowiednią metodę.

Filtr tworzymy wywołując metodę createBiquadFilter instancji AudioContext:

 ctx.createBiquadFilter();

Podobnie dla węzła wzmocnienia wywołujemy metodę createGain :

 ctx.createGain();

Moduł WebSynth: Podłączanie rzeczy

Teraz jesteśmy prawie gotowi do zbudowania naszego interfejsu syntezatora i podłączenia urządzeń MIDI do naszego źródła dźwięku. Najpierw musimy połączyć nasz silnik audio i przygotować go do odbioru nut MIDI. Aby połączyć silnik audio, po prostu tworzymy nowe instancje potrzebnych komponentów, a następnie „łączymy” je ze sobą za pomocą metody connect dostępnej dla instancji każdego komponentu. Metoda connect przyjmuje jeden argument, który jest po prostu komponentem, z którym chcesz połączyć bieżącą instancję. Możliwe jest zorganizowanie bardziej złożonego łańcucha komponentów, ponieważ metoda connect może łączyć jeden węzeł z wieloma modulatorami (umożliwiając implementację takich rzeczy jak cross-fading i więcej).

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

Właśnie zbudowaliśmy wewnętrzne okablowanie naszego silnika audio. Możesz trochę pobawić się i wypróbować różne kombinacje okablowania, ale pamiętaj o ściszeniu głośności, aby uniknąć głuchoty. Teraz możemy podłączyć interfejs MIDI do naszej aplikacji i wysyłać komunikaty MIDI do silnika audio. Skonfigurujemy obserwatora na polu wyboru urządzenia, aby wirtualnie „podłączyć” go do naszego syntezatora. Następnie będziemy słuchać komunikatów MIDI pochodzących z urządzenia i przekazywać informacje do silnika audio:

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

Tutaj słuchamy zdarzeń MIDI z urządzenia, analizujemy dane z obiektu MidiEvent i przekazujemy je do odpowiedniej metody; noteOn lub noteOff , na podstawie kodu zdarzenia (144 dla noteOn, 128 dla noteOff). Możemy teraz dodać logikę w odpowiednich metodach w module audio, aby faktycznie wygenerować dźwięk:

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

Dzieje się tutaj kilka rzeczy. W metodzie noteOn najpierw wstawiamy bieżącą notatkę do tablicy notatek. Nawet jeśli budujemy monosyntezę (co oznacza, że ​​możemy grać tylko jedną nutę na raz), wciąż możemy mieć kilka palców na klawiaturze. Musimy więc ustawić wszystkie te nuty w kolejce, aby po wydaniu jednej nuty zagrana została następna. Następnie musimy zatrzymać oscylator, aby przypisać nową częstotliwość, którą konwertujemy z nuty MIDI (skala od 0 do 127) na rzeczywistą wartość częstotliwości z odrobiną matematyki:

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

W metodzie noteOff zaczynamy od znalezienia notatki w tablicy aktywnych notatek i jej usunięcia. Następnie, jeśli była to jedyna nuta w tablicy, po prostu wyłączamy głośność.

Drugim argumentem metody setVolume jest czas przejścia, czyli czas potrzebny na osiągnięcie przez wzmocnienie nowej wartości głośności. W kategoriach muzycznych, jeśli nuta jest włączona, będzie to odpowiednik czasu ataku, a jeśli nuta jest wyłączona, jest to odpowiednik czasu zwolnienia.

Moduł WebAnalyser: Wizualizacja naszego dźwięku

Inną interesującą funkcją, którą możemy dodać do naszego syntezatora, jest węzeł analizatora, który pozwala nam wyświetlać przebieg naszego dźwięku za pomocą płótna do jego renderowania. Tworzenie węzła analizatora jest nieco bardziej skomplikowane niż w przypadku innych obiektów AudioContext, ponieważ wymaga również utworzenia węzła scriptProcessor, aby faktycznie przeprowadzić analizę. Zaczynamy od wybrania elementu canvas na 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; }

Następnie dodajemy metodę connect , w której stworzymy zarówno analizator, jak i procesor skryptów:

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

Najpierw tworzymy obiekt scriptProcessor i łączymy go z miejscem docelowym. Następnie tworzymy sam analizator, który zasilamy wyjściem audio z oscylatora lub filtra. Zwróć uwagę, jak nadal musimy podłączyć wyjście audio do miejsca docelowego, abyśmy mogli to usłyszeć! Musimy również zdefiniować kolory gradientu naszego wykresu - odbywa się to poprzez wywołanie metody createLinearGradient elementu canvas.

Na koniec scriptProcessor uruchomi zdarzenie 'audioprocess' w interwale; po uruchomieniu tego zdarzenia obliczamy średnie częstotliwości przechwycone przez analizator, czyścimy płótno i przerysowujemy nowy wykres częstotliwości, wywołując metodę 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)); } }

Na koniec będziemy musieli nieco zmodyfikować okablowanie naszego silnika audio, aby pomieścić ten nowy komponent:

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

Mamy teraz ładny wizualizator, który pozwala nam wyświetlać przebieg naszego syntezatora w czasie rzeczywistym! Wymaga to trochę pracy przy konfiguracji, ale jest bardzo interesujące i wnikliwe, zwłaszcza przy użyciu filtrów.

Budowanie na naszym syntezatorze: dodawanie prędkości i odstrojenia

W tym momencie naszego samouczka MIDI mamy całkiem fajny syntezator - ale odtwarza on każdą nutę z taką samą głośnością. Dzieje się tak, ponieważ zamiast prawidłowo obsługiwać dane dotyczące prędkości, po prostu ustawiamy głośność na stałą wartość 1,0. Zacznijmy od naprawienia tego, a potem zobaczymy, jak możemy włączyć koło detune, które można znaleźć na większości popularnych klawiatur MIDI.

Włączanie prędkości

Jeśli nie znasz tego, „szybkość” odnosi się do tego, jak mocno naciskasz klawisz na klawiaturze. W oparciu o tę wartość generowany dźwięk wydaje się albo cichszy, albo głośniejszy.

W naszym samouczku syntezatora MIDI możemy emulować to zachowanie, po prostu bawiąc się głośnością węzła wzmocnienia. Aby to zrobić, najpierw musimy wykonać trochę matematyki, aby przekonwertować dane MIDI na wartość zmiennoprzecinkową z zakresu od 0,0 do 1,0, która zostanie przekazana do węzła wzmocnienia:

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

Zakres prędkości urządzenia MIDI wynosi od 0 do 127, więc po prostu dzielimy tę wartość przez 127 i zwracamy wartość zmiennoprzecinkową z dwoma miejscami po przecinku. Następnie możemy zaktualizować metodę _noteOn , aby przekazać obliczoną wartość do węzła wzmocnienia:

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

I to wszystko! Teraz, kiedy gramy na naszym syntezatorze, zauważymy, że głośność różni się w zależności od tego, jak mocno naciskamy klawisze na naszej klawiaturze.

Włączanie koła Detune na klawiaturze MIDI

Większość klawiatur MIDI posiada pokrętło detune; koło pozwala na nieznaczną zmianę częstotliwości aktualnie granej nuty, tworząc ciekawy efekt znany jako „rozstrojenie”. Jest to dość łatwe do zaimplementowania, gdy nauczysz się korzystać z MIDI, ponieważ koło detune uruchamia również zdarzenie MidiMessage z własnym kodem zdarzenia (224), którego możemy słuchać i działać, przeliczając wartość częstotliwości i aktualizując oscylator.

Najpierw musimy złapać wydarzenie w naszym syntezatorze. Aby to zrobić, dodajemy dodatkową wielkość liter do instrukcji switch, którą utworzyliśmy w wywołaniu zwrotnym _onmidimessage :

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

Następnie definiujemy metodę detune na silniku audio:

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

Domyślna wartość rozstrojenia to 64, co oznacza, że ​​nie zastosowano rozstrojenia, więc w tym przypadku po prostu przekazujemy aktualną częstotliwość do oscylatora.

Na koniec musimy również zaktualizować metodę _noteOff , aby uwzględnić rozstrojenie na wypadek, gdyby kolejna notatka była w kolejce:

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

Tworzenie interfejsu

Do tej pory stworzyliśmy tylko pole wyboru, aby móc wybrać nasze urządzenie MIDI i wizualizator kształtu fali, ale nie mamy możliwości bezpośredniej modyfikacji dźwięku poprzez interakcję ze stroną internetową. Stwórzmy bardzo prosty interfejs używając wspólnych elementów formularza i powiążmy je z naszym silnikiem audio.

Tworzenie układu interfejsu

Stworzymy różne elementy formy, aby sterować dźwiękiem naszego syntezatora:

  • Grupa radiowa do wyboru typu oscylatora
  • Pole wyboru, aby włączyć / wyłączyć filtr
  • Grupa radiowa do wyboru typu filtra
  • Dwa zakresy do kontroli częstotliwości i rezonansu filtra
  • Dwa zakresy do kontrolowania ataku i zwolnienia węzła wzmocnienia

Tworząc dokument HTML dla naszego interfejsu, powinniśmy otrzymać coś takiego:

 <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>

Dekorowanie interfejsu użytkownika tak, aby wyglądał wymyślnie, nie jest czymś, co omówię w tym podstawowym samouczku MIDI; zamiast tego możemy zapisać to jako ćwiczenie na później, aby dopracować interfejs użytkownika, być może tak, aby wyglądał mniej więcej tak:

dopracowany interfejs użytkownika midi

Powiązanie interfejsu z silnikiem audio

Powinniśmy zdefiniować kilka metod powiązania tych kontrolek z naszym silnikiem audio.

Kontrolowanie oscylatora

W przypadku oscylatora potrzebujemy tylko metody pozwalającej na ustawienie typu oscylatora:

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

Kontrolowanie filtra

Do filtra potrzebujemy trzech regulatorów: jednego dla typu filtra, jednego dla częstotliwości i jednego dla rezonansu. Możemy również połączyć metody _connectFilter i _disconnectFilter z wartością checkboxa.

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

Kontrolowanie ataku i rezonansu

Aby nieco ukształtować nasze brzmienie, możemy zmienić parametry ataku i zwolnienia węzła wzmocnienia. Potrzebujemy do tego dwóch metod:

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

Konfigurowanie obserwatorów

Wreszcie, w kontrolerze naszej aplikacji, musimy tylko skonfigurować kilka obserwatorów i powiązać ich z różnymi właśnie utworzonymi metodami:

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

Wniosek

W tym samouczku MIDI omówiono wiele koncepcji; w większości odkryliśmy, jak korzystać z API WebMIDI, co jest dość nieudokumentowane, poza oficjalną specyfikacją W3C. Implementacja Google Chrome jest dość prosta, chociaż przejście na obiekt iteratora dla urządzeń wejściowych i wyjściowych wymaga trochę refaktoryzacji starszego kodu przy użyciu starej implementacji.

Jeśli chodzi o API WebAudio, jest to bardzo bogate API, a w tym samouczku omówiliśmy tylko kilka jego możliwości. W przeciwieństwie do WebMIDI API, WebAudio API jest bardzo dobrze udokumentowane, w szczególności w sieci Mozilla Developer Network. Sieć programistów Mozilla zawiera mnóstwo przykładów kodu i szczegółowe listy różnych argumentów i zdarzeń dla każdego komponentu, które pomogą Ci zaimplementować własne, niestandardowe aplikacje audio oparte na przeglądarce.

Ponieważ oba API nadal się rozwijają, otworzy to kilka bardzo interesujących możliwości dla programistów JavaScript; co pozwala nam na opracowanie w pełni funkcjonalnego, opartego na przeglądarce DAW, który będzie w stanie konkurować z ich odpowiednikami we Flashu. Programiści komputerów stacjonarnych mogą również zacząć tworzyć własne aplikacje wieloplatformowe, korzystając z narzędzi takich jak node-webkit. Mamy nadzieję, że stworzy to nową generację narzędzi muzycznych dla audiofilów, które wzmocnią użytkowników, wypełniając lukę między światem fizycznym a chmurą.