Shazam este un program ce se bazează pe înregistrarea și analiza sunetelor pentru a recunoaște cântece folosind microfonul telefonului mobil.
Proiectul constă în realizarea unui astfel de program ce poate rula pe un mediu constrâns cum ar fi Raspberry Pico, programat folosind framework-ul embassy-rs în Rust C. Am decis să folosesc C deoarece am întâmpinat multe probleme cu embassy deoarece nu aveam un debug probe.
Algoritmul de identificare al cântecelor se folosește de FFT, algoritm ce convertește un semnal din domeniul timpului în domeniul frecvenței, astfel obținându-se frecvențele specifice ale fiecărui cântec.
La apăsarea unui buton, dispozitivul începe să înregistreze 10 secunde de audio pe care apoi le analizează folosind FFT.
Ideea de bază este că undele ce formează un cântec sunt funcții ce depind de timp. Folosind FFT putem să le transformăm în funcții ce depind de frecvențe – astfel, fiecare undă devine indentificabilă. Pentru oameni, aceste frecvențe sunt vizibile folosind o spectrogramă. Aceasta se poate obține împărțind melodia în secvențe scurte, aplicând FFT pe fiecare, obținând “slice-uri” ce pot fi apoi concatenate pentru a obține acest al varianței frecvențelor în funcție de timp. Culorile deschise reprezintă o intensitate mai mare a anumitor frecvențe.
Din spectrograma cântecului Never Gonna Give You Up putem deduce câteva lucruri interesante:
- deși spectrul auditiv este considerat 20-20000Hz+, majoritatea cântecelor nu vor conține frecvențe mai mari de 16000Hz.
- frecvențele joase au o intensitate mai mare în cântece, deoarece ele sunt percepute ca fiind mai puțin intense ca sunetele înalte.
Listă de piese:
Schematic:
Descriere pini:
Dispozitiv | Funcție | Pin Pico W | Observații |
---|---|---|---|
LCD I2C (PCF8574) | GND | GND | GND comun |
LCD I2C (PCF8574) | VCC | 3V3 (OUT) | Alimentare 3.3V |
LCD I2C (PCF8574) | SDA | GP0 | I2C0 SDA (folosește pull-up intern) |
LCD I2C (PCF8574) | SCL | GP1 | I2C0 SCL (folosește pull-up intern) |
MAX4466 (microfon) | GND | GND | GND comun |
MAX4466 (microfon) | VCC | 3V3 (OUT) | Alimentare 3.3V |
MAX4466 (microfon) | OUT | GP26 (ADC0) | Conectat la un pin ADC pentru citirea semnalului |
Buton | GND | GND | |
Buton | GPIO | GP15 |
[ Buton ] | v [ Pico W detectează apăsarea ] | v [ MAX4466 captează sunetul ambiental ] | v [ Semnal analogic intră pe GP26 (ADC) ] | v [ Pico W face sampling și FFT ] | v [ Căutare în hashmap a hashului fiecărui sample din înregistare ] | v [ Rezultat afișat pe LCD 16x2 via I2C ]
LCD-ul este programat folosind interfața I2C. Am făcut un fișier ce expune o interfață pentru scris pe ecran.
Microfonul MAX4466 este conectat prin interfața ADC. Pico dispune de un ADC pe 12 biți, așa că am decis să folosesc 8 biți pentru înregistrare(formatele audio pe 12 biți nu sunt un standard).
Cea mai complexă parte a codului reprezintă înregistrarea și prelucrarea input-ului audio pentru recunoașterea cântecului. Aplicând Nyquist–Shannon sampling theorem, aflăm că pentru a obține frecvențe din intervalul 0-x Hz, trebuie să facem sampling în intervalul 0-2*x Hz. Fiecare sample înregistrat este apoi împărțit în mai multe linii de frecvențe din care selectăm cele mai puternice sunete din fiecare bandă de frecvență. Am ales să aleg 4 cele mai puternice frecvențe pentru a crea un hash de 4 bytes pentru fiecare sample.
FFT-ul este implementat folosind biblioteca KissFFT.
Prima problemă pe care o întâmpinăm este faptul că PICO are doar 512KB de SRAM si 4 MB flash. Dacă am folosi tot acest spațiu și un cântec ar avea în medie 4000 de samples(un sample la 50 de milisecunde pentru un cântec de 200 de secunde), am avea nevoie de 16K de memorie pentru un singur cântec, deci am putea stoca maxim 256 de cântece în flash / 32 de cântece în SRAM. În plus, am vrea să putem înregistra 10 secunde dintr-un cântec unde aplicăm sampling la 16000Hz, am avea nevoie de 160KB, în plus doar pentru a înregistra un sample.
Planul devine în mod rapid nefezabil așa că avem nevoie de tactici pentru a reduce consumul mare de memorie. Ce am făcut a fost să scad samplerate-ul până la 800Hz. Semnăturile digitale sunt precomputate și puse în memoria microcontrollerului ca apoi să fie matched.
* Inițializează periferice: * I2C pentru LCD * ADC pentru microfon (ex: GP26 / ADC0) * GPIO pentru buton while true: dacă butonul este apăsat: afișează pe LCD "Ascult..." // 1. Eșantionare semnal audio colectează N mostre de la ADC (durată: ~5 secunde) salvează datele într-un buffer // 2. Preprocesare aplică fereastră Hanning (sau altă fereastră) peste date // 3. Aplică FFT execută transformata Fourier rapidă (FFT) pe buffer obține spectrul de frecvență // 4. Generează semnătură extrage caracteristici dominante (peak-uri, forma generală, etc.) generează un hash/structură cu semnătura audio // 5. Compară cu baza de date pentru fiecare semnătură salvată în baza de date: dacă semnătura se potrivește: afișează pe LCD: "Melodie găsită:" afișează numele melodiei break dacă nicio potrivire: afișează "Melodie necunoscută"
Pentru a obține hash-urile am scris un program în C++ care, folosind aceeași algoritmi ca și codul pus pe Pico, generează hash-urile cântecelor ce sunt apoi încărcate pe pico înainte de rulare.
Pentru a obține cântecele le-am descărcat de pe youtubeyt-dlp, le-am convertit într-un format necompresat (.wav) pe 1 byte la acelasi sample rate ca cel înregistrat de microcontroller folosind ffmpeg și le-am dat ca date de intrare pentru program.
Proiectul este dezvoltat folosind VSCode. Pentru compilarea proiectului am folosit ninja și cmake. Pentru flashing am folosit picotool.
Dispozitivul este capabil de a recunoaște cântece, deși cu o precizie destul de slabă. Sunt multe motive pentru care acesta ar putea eșua: zgomote puternice în timpul înregistrării, fidelitatea înregistrării scăzute, coliziuni de hash tabela ce ține frecvențele, etc.
Proiectul funcționează, deși nu la fel de bine precum am sperat.
Consider mai multe îmbunătățiri pe care le-aș putea face(micșorarea benzilor de frecvență, modificarea relației pentru semnătura cântecului, etc.) dar acestea necesită o perioada îndelungată de cercetare și testare pentru a ajunge la rezultatul dorit.