Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Algorytmika Programowanie Programowanie webowe

Manipulacja Audio w HTML5

Cześć! Cieszę się, że mnie odwiedziłeś/aś. Zanim przejdziesz do artykułu chciałbym zwrocić Ci uwagę na to, że ten artykuł był pisany kilka lat temu (2016-02-19) miej więc proszę na uwadzę że rozwiązania i przemyślenia które tu znajdziesz nie muszą być aktualne. Niemniej jednak zachęcam do przeczytania.

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

DEMO

Dzięki!

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

0 komentarzy
Inline Feedbacks
View all comments