Kawałek Kodu

Jedna z teorii budowy wszechświata mówi, że wszystko jest falą. A ponieważ wszystkiego jest nieskończenie dużo, to i fal jest bardzo dużo. Te rozchodząc się w przestrzeni znoszą się i nakładają. Tam gdzie jest ich więcej, tam jest więcej materii. A materia może wydawać dźwięki, które są... falami. Wiemy jak wygląda materia, a jak wygląda dźwięk?

Czerwony pasek nie tylko na świadectwie.

Mając do czynienia z wymarzoną wieżą czy pierwszymi głośnikami i skórkami do WinAmp'a, zapewne pamiętasz efekt skaczących w rytm muzyki pasków. Fajnie to wyglądało i oczywiste było, że paski z lewej strony skaczą w rytm basów, a te po prawej, w rytm słów piosenki czy rytmicznie uderzanych talerzy perkusji. Tu już narzuca się oczywista rzecz, że każdy pasek reprezentował inną częstotliwość (wysokość) dźwięku. Brak paska to brak "tego" dźwięku w aktualnym momencie, połowa to 50% głośności, a lewy pasek sięgający do samej góry oznaczał, że porządnie podkręciłeś basy. Żeby się dowiedzieć jakie paski w danym momencie występują i jaką mają wysokość, można się posłużyć szybką transformacją Fourier'a. Wynikiem jej jest transformata Fouriera, a reprezentacją wizualną widmo sygnału (właśnie te paski). Transformacja Fourier'a potrafi rozdzielić nasz dźwięk (sygnał) na poszczególne składowe - odrębne fale sinusoidalne. W zależności od złożoności transformacji uzyskujemy różną ilość pasków. Tzn. czym większa złożoność tym bardziej szczegółowa (gęściejsza) analiza i więcej pasków.

Raz na wozie, raz pod wozem.

Jak wspomniałem, transformacja Fourier'a pokazuje o jakiej częstotliwości i o jakiej amplitudzie występują fale sinusoidalne w sygnale. Bo każdy sygnał da się sprowadzić do sumy sygnałów sinusoidalnych. Ten jest podstawowy. Mamy również do czynienia z sygnałem prostokątnym, piłokształtnym czy też trójkątnym. Jeśli nie wierzysz, to zróbmy rzecz odwrotną, która przy okazji pomoże Ci zrozumieć skąd się biorą paski. Na przykładzie poniżej zbudujemy falę prostokątną. Ta jest sumą sygnałów sinusoidalnych o określonych częstotliwościach. Aby osiągnąć idealną falę prostokątną z sygnałów sinusoidalnych, musiałoby ich być nieskończenie wiele. No, dobra, ale buduj! Klikając w wykres poniżej, z każdym krokiem będzie dodawana sinusoida o innej częstotliwości (niebieska). W miarę napływu tych sinusoid, fala prostokątna (czerwona) będzie nabierać kształtu zbliżonego do docelowego.

Efektownie i efektywnie.

Myśląc nad tym wpisem, początkowo chciałem zabrać się za FFT (Fast Fourier Transformation) od podstaw. Okazuje się jednak, że wraz z transformacją przeglądarek, przyszła do nas i transformacja FFT. Mamy ją pod reką jako jedną z metod w WebAudio API. Aby móc analizować dane audio (w naszym przypadku będzie to odtwarzany plik mp3) należy stworzyć swego rodzaju scenę muzyczną, jest nią obiekt AudioContext. Zza kulis wypychamy naszą mp3 na scenę i przystawiamy do mikrofonu. Mikrofon będzie naszym źródłem (MediaElementAudioSourceNode). Oczywiście nie ma sensu tworzyć całego kontekstu, aby odwtorzyć mp3. My podłączamy pod źródło "plugin". Będzie nim analizator (AnalyserNode). To trochę taki pan dźwiękowiec pilnujący czy... gra muzyka. Ten pan też może poprawiać fałszującyh śpiewaków z pomocą swojej konsoli, ale już korzystając z innego "pluginu". To co przetworzy lub przeanalizuje wypuszcza do głośników (AudioDestinationNode).

Chcąc zobaczyć skaczące kreski na naszej konsolecie należy odpowiednio zaprogramować plugin. A robi się to bardzo prost, bo wystarczy ustawić dla niego właściwość fftSize, która to mówi ile słupków chcemy widzieć na naszej konsoli. Przy czym fftSize ustawiamy na dwukrotną wartość żądanej ilości - jeśli chcemy zobaczyć 64 słupki, to ustawiamy wartość 128. Te słupki to oczywiście wybrane częstotliwości z analizowanego dźwięku - a dokładnie te składowe sinusoidalne, które w danym momencie przyczyniają się do powstania próbki dźwięku.

/* tworzymy element Audio
*/
const audio = new Audio();
audio.src = "muzyka.mp3";
audio.controls = true;
audio.loop = true;

/* nasza scena muzyczna
*/
const context = new AudioContext();

/* pan dźwiękowiec
   lubiący 16 kresek
*/
const analyser = context.createAnalyser();
analyser.fftSize = 32;

/* tworzymy tablicę
   złożoną z bezznakowych bajtów
   o długości 16
*/
const freqByteData = new Uint8Array(analyser.frequencyBinCount);

/* wypychamy śpiewaka
   na scenę
*/
const source = context.createMediaElementSource(audio);

/* pan dźwiękowiec
   wpina się w mikrofon
*/
source.connect(analyser);

/* podłączamy głośniki
*/
analyser.connect(context.destination);

audio.play();

/* jeśli muzyka gra,
   to możemy pobrać
   wartości kresek,
   czyli wynik analizy FFT
*/
if (audio.played) analyser.getByteFrequencyData(freqByteData);

Aby zajrzeć do konsoli pana dźwiękowca należy wykorzystać ten kod w praktyce. Zwizualizujemy nasze słupki.

<!-- to będzie przycisk
     uruchamiający muzykę
-->
<button>Graj</button>

<!-- a to nasz konsola
-->
<div></div>

+

/* słupki w konsoli
   mocujemy u dołu
*/
div {
  width: 100%;
  max-width: 320px;
  height: 320px;
  position: relative;
  display: flex;
  align-items: flex-end;
}

/* szerokość słupka
   jest proporcjonalna
   do ilości słupków,
   wysokość również,
   no i kolor też
*/
span {
  width: calc(100% / var(--barsCount));
  height: calc(100% * (var(--height) / 255));
  background: hsl(calc(120 - 360 *  (var(--height) / 255) / 3),  100%, 50%);
}

+

/* 16 słupków
*/
const barsCount = 16;

/* kontenerem będzie
   <div>, w którym przechowamy
   ilość słupków
*/
const container = document.querySelector('div');
container.style.setProperty('--barsCount', barsCount);

/* tworzymy słupki
   w kodzie
*/
for (let i = 0; i < barsCount; i++) {
  container.appendChild(document.createElement('span'));
}

/* inicjalizujemy
   nasz element <audio>
*/
const audio = new Audio();
audio.src = "Quixotic Dust to dust.mp3";
audio.controls = true;
audio.loop = true;

let play = false;

let context;
let analyser;
let freqByteData;
let source;

/* inicjalizację kontekstu
   musimy przeprowadzić
   na kliknięcie, bo polityka
   Chrome nie pozwala startować
   ot tak
*/
document.querySelector("button").addEventListener("click", function(e) {
  e.preventDefault();

/* nasza scena
*/
  context = new AudioContext();

/* pan dźwiękowiec
*/
  analyser = context.createAnalyser();
  analyser.fftSize = barsCount * 2;
  freqByteData = new Uint8Array(analyser.frequencyBinCount);

/* dajemy śpiewakowi
   mikrofon do ręki
*/
  source = context.createMediaElementSource(audio);

/* pan dźwiękowiec
   podpina się pod
   kable
*/
  source.connect(analyser);

/* i podłączamy głośniki
*/
  analyser.connect(context.destination);

  audio.play();
  play = true;
});

/* co 50 ms
   kreślimy słupki
*/
setInterval(function() {
  if (play) updateBars();
}, 50);

function updateBars(time) {
/* jeśli dźwięk jest
   odtwarzany
*/
  if (audio.played){

/* to pobieramy amplitudy
   sinusoid
*/
    analyser.getByteFrequencyData(freqByteData);

/* i podstawiamy je
   jako wartości potrzebne
   do obliczenia wysokości
   słupków
*/
    for (let i = 0; i < barsCount; i++) {
      container.querySelector('span:nth-child(' + (i + 1) + ')').style.setProperty('--height', freqByteData[i]);
    }
  }
}

Wysokość słupka obliczamy z proporcji: 100%*(aktualna_wartosc/maksymalna_wartosc). Maksymalna wartość to 255, tak więc i słupek może mieć maksymalnie 100% wysokości kontenera.

Chyba się nie pomylę, że nawet nie będąc audiofilem przyjemnie popatrzeć jak wygląda dźwięk. Do przeczytania!

 

Przydatne linki:
Dokumentacja WebAudio.
Próbka dźwięku.