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!
Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More
Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More
Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More
Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More
Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More
Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More