Cześć,
HTML 5 poza rzeczami znanymi, takimi jak WebRTC, localStorage czy sessionStorage daje możliwość manipulacji elementami Audio. Pokażę krótki kawałek kodu którym możemy łatwo przeanalizować dźwięk który „słyszy” mikrofon ;)
Jak zwykle kod pisany jest pod Chrome, ale łatwo możecie go sobie przepisać min. na FireFoxa.
Zacznijmy od uzyskania dostępu do mikrofonu. UserMedia w Chrome wymagają aby strona żądająca do nich dostępu była serwowana przez HTTPs, stąd demo które zamieszczę, ma darmowy, zaufany certyfikat, wygenerowany zgodnie z poprzednim wpisem na tym blogu – Darmowy certyfikat TLS (HTTPS) od Lets Encrypt
Więc owy dostęp możemy uzyskać dzięki linijce takiego kodu:
1 2 3 4 5 | navigator.webkitGetUserMedia( {audio:true, video:false}, callbackWithMedia, function(){} ); |
Gdzie jako parametry podajemy, kolejno, o jaki dostęp prosimy, callback do funkcji która ma się wykonać gdy uzyskamy dostęp i callback do funkcji gdy pojawi się błąd.
Funkcja przekazana jako drugi argument przyjmuje jeden argument którym jest strumień, w tym przypadku audio.
Aby móc manipulować i analizować dźwięk musimy skorzystać z klasy AudioContext która reprezentuje całościowy kontekst dźwiękowy komputera. Poza tym dostarcza kilka metod fabrykujących inne obiekty klas które mogą operować na dźwięku, My użyjemy klasy Analyser.
1 2 3 4 5 | var callbackWithMedia = function (stream) { var context = new AudioContext(); var microphone = context.createMediaStreamSource(stream); var analyser = context.createAnalyser(); }; |
Tak jak napisałem wyżej, pierw tworzymy kontekst. Następnie funkcją createMediaStreamSource tworzymy, przekazując w jej argumencie nasz surowy strumień, obiekt klasy MediaStreamAudioSourceNode który może być np. odtwarzany. Następnie używając aktualnego kontekstu tworzymy nasz Analyser.
Klasa Analyser pozwala analizować strumień audio w dziedzinie częstotliwości lub czasu. My będziemy opierać się o częstotliwość dźwięku. Aby uzyskać takie dane od Analyser’a należy podać mu argument FFT – czyli Szybka Transformata Fouriera.
Transformacja Fouriera rozkłada funkcję okresową na szereg funkcji okresowych tak, że uzyskana transformata podaje w jaki sposób poszczególne częstotliwości składają się na pierwotną funkcję.
A sam argument który podajemy do Analyser’a to rozmiar bloku danych – jest to zwykle liczba która jest potęgą liczby 2.
Rozbudujmy nasz kod:
1 2 3 4 5 6 7 8 | var callbackWithMedia = function (stream) { var context = new AudioContext(); var microphone = context.createMediaStreamSource(stream); var analyser = context.createAnalyser(); analyser.fftSize = 1024; microphone.connect(analyser); element.src = URL.createObjectURL(stream); }; |
Poza ustawieniem fftSize łączymy jeszcze naszego Analyser’a z naszym strumieniem mediów i przypisujemy do pola src zmiennej element wartość naszego surowego strumienia przepuszczonego przez funkcję URL.createObjectURL. Zmienna element została zdefiniowana jako:
1 | var element = document.getElementById("audioElement"); |
A w pliku HTML w którym osadzamy nasz plik JS mamy element audio:
1 | <audio controls="controls" style="width:100%;height:100px" id="audioElement"></audio> |
który renderuje się do takiego odtwarzacza:
Uzyskujemy dzięki temu efekt w którym jeśli otworzymy naszą stronę i klikniemy na odtwarzaczu „Play” to usłyszymy dźwięk z mikrofonu.
No i fajnie – ale nadal nie korzystamy z Analyser’a. Napiszmy kawałek kodu który jeśli wykryje „przemowę” mówioną do mikrofonu to odtworzy nam inny plik audio – niech to będą oklaski ;) czyli nasza przemowa będzie nagradzana oklaskami. A więc zaczynajmy:
1 2 3 4 5 6 7 8 9 10 11 12 | var callbackWithMedia = function (stream) { var context = new AudioContext(); var microphone = context.createMediaStreamSource(stream); var analyser = context.createAnalyser(); analyser.fftSize = 1024; microphone.connect(analyser); window.setInterval(function() { var freqByteData = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(freqByteData); }, 50); element.src = URL.createObjectURL(stream); }; |
Dodaliśmy tutaj setInterval – czyli oznaczyliśmy funkcję przekazaną jako pierwszy argument jako taką która ma się uruchamiać co tyle ms ile jest przekazane w drugim argumencie. Czyli u nas co 50ms.
W niej natomiast tworzymy obiekt klasy Uint8Array, czyli tak naprawdę tablicy o długości podanej w argumencie – frequencyBinCount. I wypełniamy ją danymi od Analyser’a.
Załóżmy że jako wykrywaną „przemowę” będziemy rozumieć utrzymującą się przez co najmniej 2 sekundy średnią częstotliwość na poziomie 70Hz. Dla ułatwienia będziemy rozpatrywać tylko 3 pierwsze wartości dostarczone od Analyser’a.
Możemy to zaimplementować np. tak
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | var callbackWithMedia = function (stream) { var context = new AudioContext(); var microphone = context.createMediaStreamSource(stream); var analyser = context.createAnalyser(); analyser.fftSize = 1024; microphone.connect(analyser); window.setInterval(function() { var freqByteData = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(freqByteData); if((freqByteData[0] + freqByteData[1] +freqByteData[2])/3 >= noiseLvl){ currentDuration++; } else { if(currentDuration> 0) currentDuration--; } if(currentDuration >= duration){ clap(); currentDuration= 0; } console.log(((freqByteData[0] + freqByteData[1] +freqByteData[2])/3) + '... '+currentDuration); }, 50); element.src = URL.createObjectURL(stream); }; |
Czyli jeśli średnia częstotliwość jest na poziomie większym niż noiseLvl (70), to zwiększamy zmienną currentDuration o jeden, w przeciwnym przypadku, jeśli zmienna ta jest większa niż 0, to zmniejszamy ją o 1. To zmniejszanie pozwoli nam wykrywać i korygować ewentualną sytuację w której przemowa nie jest ciągła.. Czyli jeśli np. zrobimy pauzę w mówieniu to będzie spadać wymagana długość mówienia, ale gdy zaczniemy mówić znów to nie zaczniemy zbierać jej „od zera”. Wymagana długość mówienia to zmienna duration i dla 2 sekund ma wartość 40. Wartość 40 wzięła się stąd że, skoro funkcja jest odpalana co 50ms to currentDuration ma szansę być raz na 50ms zwiększone o jeden. Jeśli przemowa będzie ciągła to currentDuration będzie miało wartość 40 po 2 sekundach, ponieważ 40 * 50m = 2000ms = 2s.
I jeśli uzyskaliśmy wymagany próg to wywołujemy clap() i zerujemy currentDuration. Funkcja clap odtworzy nam plik MP3 z oklaskami:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var clapContext = new AudioContext(); var clap = function(){ console.log("Clap!!"); var request = new XMLHttpRequest(); request.open("GET", "clap.mp3", true); request.responseType = "arraybuffer"; request.onload = function() { var audioData = request.response; var soundSource = clapContext.createBufferSource(); clapContext.decodeAudioData(audioData, function(soundBuffer){ soundSource.buffer = soundBuffer; soundSource.connect(clapContext.destination); soundSource.start(clapContext.currentTime); }); }; request.send(); } |
Wysyłamy GET’a po plik, dekodujemy używając kolejnych metod z klasy AudioContext i odtwarzamy ;) zwróć uwagę na responseType oraz na sposób przekazywania pliku MP3 do kontekstu.
Cały kod wygląda tak (poza plikiem HTML):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | var element = document.getElementById("audioElement"); var duration = 40; var currentDuration = 0; var noiseLvl = 70; var clapContext = new AudioContext(); var clap = function(){ console.log("Clap!!"); var request = new XMLHttpRequest(); request.open("GET", "clap.mp3", true); request.responseType = "arraybuffer"; request.onload = function() { var audioData = request.response; var soundSource = clapContext.createBufferSource(); clapContext.decodeAudioData(audioData, function(soundBuffer){ soundSource.buffer = soundBuffer; soundSource.connect(clapContext.destination); soundSource.start(clapContext.currentTime); }); }; request.send(); } var callbackWithMedia = function (stream) { var context = new AudioContext(); var microphone = context.createMediaStreamSource(stream); var analyser = context.createAnalyser(); analyser.fftSize = 1024; microphone.connect(analyser); window.setInterval(function() { var freqByteData = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(freqByteData); if((freqByteData[0] + freqByteData[1] +freqByteData[2])/3 >= noiseLvl){ currentDuration++; } else { if(currentDuration > 0) currentDuration--; } if(currentDuration >= duration){ clap(); currentDuration = 0; } console.log(((freqByteData[0] + freqByteData[1] +freqByteData[2])/3) + '... '+currentDuration); }, 50); element.src = URL.createObjectURL(stream); }; navigator.webkitGetUserMedia( {audio:true, video:false}, callbackWithMedia, function(){} ); |
Dzięki!
Mateusz Mazurek