04. HĽADAČ PIESNÍ – ARDUINO ROZPOZNÁVA SKLADBY (Tomáš Dobos)

04. HĽADAČ PIESNÍ – ARDUINO ROZPOZNÁVA SKLADBY (Tomáš Dobos)

Cieľom tohto projektu je postaviť zariadenie, ktoré:

  • počúva hudbu zvukovým senzorom,
  • rozpozná skladbu cez internet pomocou služby AudD,
  • zobrazí názov a interpreta na LCD displeji,
  • v dolnom riadku displeja ukazuje živé spektrum zvuku (equalizer).

💡 Princíp projektu

  • Stlačíš tlačidlo a Arduino päť sekúnd nahráva zvuk z okolia.
  • Nahrávku pošle cez USB do počítača.
  • Počítač ju odošle online službe AudD, ktorá rozpoznáva hudbu.
  • AudD vráti názov skladby a interpreta, ktoré sa zobrazia v hornom riadku displeja. Dlhý text sa posúva, aby sa dalo prečítať každé slovo.
  • V dolnom riadku je celý čas živé spektrum: 16 stĺpcov, každý pre jedno frekvenčné pásmo, od basov vľavo po výšky vpravo.

Je to podobné ako aplikácie na rozpoznávanie hudby v mobile, ale celé je postavené z jednoduchých súčiastok. Arduino Mega nemá internet ani dosť výkonu na rozpoznávanie skladieb, preto sa o prácu delí s počítačom:

ZariadenieČo robí
Arduino Megačíta tlačidlo, vzorkuje zvukový senzor, kreslí na LCD a počíta spektrum
Počítačprijme nahrávku, pošle ju službe AudD a výsledok vráti na displej

📸 Ako to vyzerá

Arduino Mega s LCD displejom, zvukovým senzorom a tlačidlom. Na displeji sa posúva názov skladby SuperStar
Hotové zariadenie. Po rozpoznaní sa názov posúva v hornom riadku.
Zapojenie na nepájivom poli, displej hlási Listening
Zariadenie počúva: displej hlási „Listening…“.
Video ukážka: stlačenie tlačidla, počúvanie a rozpoznanie skladby.

🧰 Potrebný hardvér

  • Arduino Mega 2560 s USB káblom (1 ks)
  • zvukový senzor KS0035, analógový mikrofón (1 ks)
  • LCD displej 16×2 s čipom HD44780, 16 pinov (1 ks)
  • tlačidlo so 4 nožičkami (1 ks)
  • potenciometer 10 kΩ na kontrast displeja (1 ks)
  • rezistor 220 Ω na podsvietenie displeja (1 ks)
  • nepájivé pole a prepojovacie vodiče

Softvér a účty

  • Arduino IDE (knižnica LiquidCrystal je jeho súčasťou)
  • Python 3 s balíkmi pyserial, numpy a requests
  • počítač s Windowsom a pripojením na internet
  • bezplatný účet na audd.io a API kľúč

🔌 Zapojenie

LCD displej 16×2

Displej beží v 4-bitovom režime, takže piny 7 až 10 (D0 až D3) ostávajú nezapojené.

Pin LCDOznačenieZapojiť na
1VSSGND
2VDD5V
3V0stredný vývod potenciometra (kontrast)
4RSMega pin 22
5RWGND
6EMega pin 23
7 až 10D0 až D3nezapojené
11D4Mega pin 24
12D5Mega pin 25
13D6Mega pin 26
14D7Mega pin 27
15A (podsvietenie +)cez rezistor 220 Ω na Mega pin 9
16K (podsvietenie −)GND

Zvukový senzor KS0035

Pin senzoraZapojiť na
GGND
V5V
SMega pin A4

Malý trimer na senzore nastavuje zosilnenie. Pri hlasnej skladbe by hodnota max vo výpise programu mala byť aspoň okolo 50.

Tlačidlo

  • jedna nožička tlačidla → Mega pin 2
  • protiľahlá (uhlopriečna) nožička → GND

Štvornohé tlačidlo má nožičky spojené po dvojiciach, preto ho zapájame uhlopriečne. Rezistor netreba, program zapína vnútorný pull-up (INPUT_PULLUP).

Podsvietenie a kontrast

Rezistor 220 Ω vedie z vývodu A displeja na pin 9, ktorý vie meniť jas (PWM). Kontrast nastavíme potenciometrom medzi 5V a GND so stredným vývodom na V0.

Dôležité: V0 priamo na GND je maximálny kontrast. Písmená sa potom strácajú v tmavých štvorčekoch, preto použi potenciometer.

🧩 Schéma zapojenia (textová)

Arduino Mega            Zariadenie
------------            ----------
5V   -------------->    + lišta
GND  -------------->    - lišta

Zvukový senzor KS0035:
V    -------------->    + lišta (5V)
G    -------------->    - lišta (GND)
S    -------------->    A4

LCD 16x2 (4-bitový režim):
pin 22 -------------->  RS  (pin 4 LCD)
pin 23 -------------->  E   (pin 6 LCD)
pin 24..27 ---------->  D4..D7 (piny 11 až 14 LCD)
VSS, RW, K ---------->  GND
VDD ----------------->  5V
V0  ----------------->  stred potenciometra (kontrast)

Podsvietenie LCD:
D9  --> [220 Ω] --> A (pin 15 LCD),  K --> GND

Tlačidlo:
D2  --> tlačidlo --> GND (uhlopriečne)

🔥 Dôležité upozornenie

API kľúč služby AudD je tajný. Nesmie sa dostať do kódu na GitHube ani na webstránku. Ukladáme ho do súboru host/audd_token.txt, ktorý Git ignoruje.

Pieseň musí byť hlasná a blízko senzora. Náš senzor je jednoduchý, takže pri tichej hudbe alebo zo vzdialenosti rozpoznávanie často nevyjde.

Port môže mať otvorený len jeden program. Pred nahrávaním kódu do Arduina treba zavrieť Python program aj Serial Monitor.

👉 Toto je výborný moment na vysvetlenie, že reálne senzory a online služby nie sú dokonalé: treba ich vedieť nastaviť a otestovať.


⚙️ Ako to funguje

  1. Stlačenie tlačidla: Arduino zobrazí „Listening…“ a päť sekúnd vzorkuje senzor na pine A4: 8 000 vzoriek za sekundu, 8 bitov na vzorku, spolu 40 000 bajtov.
  2. Prenos do počítača: Arduino pošle hlavičku AUDIO 40000 a hneď za ňou surové bajty cez USB rýchlosťou 250 000 baudov.
  3. Úprava zvuku: počítač odpočíta jednosmernú zložku (senzor je v pokoji blízko nuly), zosilní signál na plnú hlasitosť a uloží ho ako súbor WAV.
  4. Rozpoznanie: počítač pošle WAV službe AudD a čaká na odpoveď.
  5. Zobrazenie: počítač pošle text „názov – interpret“ späť na Arduino. Písmená s diakritikou nahradí obyčajnými, lebo font displeja ich nemá.

Živé spektrum v dolnom riadku

  1. Arduino odmeria 128 vzoriek pri 8 kHz (trvá to 16 ms) a odpočíta ich priemer.
  2. Pre každé z 16 pásiem spustí Goertzelov filter, čo je zjednodušená FFT, ktorá počíta len jednu frekvenciu naraz. Preto sa zmestí do pamäte a výkonu Arduina Mega. Pásma sú rozložené od 110 Hz do 3 500 Hz.
  3. Z výsledku sa vypočíta amplitúda každého pásma. Automatické zosilnenie sa prispôsobuje najhlasnejšiemu pásmu, takže stĺpce poskakujú pri tichej aj hlasnej hudbe.
  4. Šumová brána skryje pásma, ktoré sú slabšie ako šum senzora.
  5. Výška stĺpca sa zaokrúhli na jednu z 8 úrovní. Každá úroveň je vlastný znak displeja, ktorý Arduino nahrá do pamäte LCD.
  6. Výsledkom je približne 20 snímok za sekundu.

🎵 Služba AudD

AudD je online služba na rozpoznávanie hudby. Pošleme jej krátku nahrávku a vráti názov skladby, interpreta, album a odkaz. Nahrávku porovnáva so zvukovými odtlačkami skladieb vo svojej databáze.

Ako získať kľúč

  1. zaregistrovať sa na dashboard.audd.io
  2. na hlavnej stránke dashboardu skopírovať api_token
  3. po registrácii je skúšobné obdobie, v ktorom sa dá služba vyskúšať zadarmo

Odpoveď služby

{
  "status": "success",
  "result": {
    "artist": "Tears For Fears",
    "title": "Everybody Wants To Rule The World",
    "album": "Songs From The Big Chair",
    "timecode": "00:56"
  }
}

Keď AudD skladbu nepozná, pole result je null a program zobrazí „No match, try again“.

Pozor: API kľúč je ako heslo. Nesmie sa dostať do kódu na GitHube ani na webstránku. V našom projekte je v súbore host/audd_token.txt, ktorý Git ignoruje.

💻 Kód Arduino

Nahráva sa do Arduina Mega cez Arduino IDE (doska „Arduino Mega or Mega 2560“). Súbor song_finder.ino.

// Song finder: mic + 16x2 LCD + button.
//   button on pin 2 (or 'g') -> stream 5 s of 8 kHz 8-bit audio from the sound sensor on A4 to the PC
//   "L<text>"                -> top LCD row shows <text> (scrolls when longer than 16 characters)
//   "B<0-255>"               -> backlight brightness
//   "S<1-9>"                 -> analyzer sensitivity (1 = bars stay low, 9 = bars jump easily)
//   "D1" / "D0"              -> debug: print the band amplitudes over serial / stop
// The bottom LCD row is a live 16-band spectrum analyzer of the mic (Goertzel filters, no FFT library).
// Wiring: sensor G -> GND, V -> 5V, S -> A4.  LCD RS 22, E 23, D4-D7 24-27, backlight A -> 220 ohm -> pin 9.
// Button: leg -> pin 2, diagonal leg -> GND.
#include <LiquidCrystal.h>
#include <math.h>

const uint8_t PIN_MIC = A4;
const uint8_t PIN_BTN = 2;
const uint8_t PIN_BACKLIGHT = 9;       // PWM pin: LCD pin 15 (A) connects through the 220 ohm resistor to this pin
const uint8_t BACKLIGHT_DEFAULT = 255; // 0 = off ... 255 = full brightness; the PC can change it with "B<0-255>"
const uint32_t BAUD = 250000;
const uint16_t SAMPLE_RATE = 8000;
const uint8_t SECONDS = 5;
const uint32_t TOTAL = (uint32_t)SAMPLE_RATE * SECONDS;
const uint32_t PERIOD_US = 1000000UL / SAMPLE_RATE;

const uint8_t LCD_COLS = 16;
const uint8_t TEXT_MAX = 120;          // longest text for the top row
const uint8_t SCROLL_GAP = 4;          // blank columns between the end of the text and its restart
const uint32_t SCROLL_MS = 350;        // time per scroll step
const uint8_t HOLD_TICKS = 4;          // scroll steps to pause with the start of the text in view
const uint32_t REDRAW_MS = 10000;      // full LCD re-init, so a glitched display recovers by itself

// Spectrum analyzer (bottom row)
const uint8_t BANDS = LCD_COLS;        // one band per LCD column
const uint8_t VIS_N = 128;             // samples per analysis block: 16 ms at 8 kHz
const float BAND_LOW_HZ = 110.0f;      // left column
const float BAND_HIGH_HZ = 3500.0f;    // right column, bands are spaced logarithmically in between
const float GATE_AMP = 12.0f;          // band amplitude (ADC counts) below this shows nothing: keeps noise off the bars
const float MIN_REF = 48.0f;           // the auto-gain never goes more sensitive than this amplitude = full bar
const uint8_t SENS_DEFAULT = 6;        // 1 (bars stay low) ... 9 (bars jump easily); the PC can change it with "S<1-9>"
const float REF_DECAY = 0.97f;         // how fast the auto-gain relaxes after a loud moment, per frame
const uint8_t BAR_PIXELS = 0b01110;    // bar width inside a character cell (0b11111 = full width)

LiquidCrystal lcd(22, 23, 24, 25, 26, 27);  // RS, E, D4, D5, D6, D7

char line[TEXT_MAX + 4];  // "L" + text + NUL
uint8_t len = 0;
uint32_t lastByte = 0;    // when the last serial byte arrived
char title[TEXT_MAX + 1] = "Song finder";
uint8_t pos = 0;          // scroll position of the top row
uint8_t hold = HOLD_TICKS;
uint32_t lastDraw = 0;
uint32_t lastScroll = 0;
bool btnDown = false;
uint32_t btnChange = 0;

int16_t coeff[BANDS];     // Goertzel coefficients, 2*cos(w) in Q10
int16_t block[VIS_N];     // one block of mic samples
float amp[BANDS];         // measured amplitude per band, in ADC counts
uint8_t level[BANDS];     // bar heights currently shown, 0..8
float ref = MIN_REF;      // auto-gain reference: the amplitude that fills a bar
float thr[8];             // thr[k] = fraction of ref a band needs to reach bar height k + 1
bool debug = false;

// Top row: text that fits is static, longer text shows a 16-column window that wraps around.
void drawTitle() {
  uint8_t n = strlen(title);
  lcd.setCursor(0, 0);
  for (uint8_t i = 0; i < LCD_COLS; i++) {
    char c = ' ';
    if (n <= LCD_COLS) {
      if (i < n) c = title[i];
    } else {
      uint8_t k = (pos + i) % (n + SCROLL_GAP);
      if (k < n) c = title[k];
    }
    lcd.write(c);
  }
}

// Bottom row: one custom character per bar height, 1..8 pixels tall. Level 0 is a blank.
void createBarChars() {
  for (uint8_t h = 1; h <= 8; h++) {
    uint8_t rows[8];
    for (uint8_t r = 0; r < 8; r++) rows[r] = (r >= 8 - h) ? BAR_PIXELS : 0;
    lcd.createChar(h - 1, rows);
  }
}

void drawBars() {
  lcd.setCursor(0, 1);
  for (uint8_t b = 0; b < BANDS; b++) {
    lcd.write(level[b] ? (uint8_t)(level[b] - 1) : (uint8_t)' ');
  }
}

// Full init + redraw. The HD44780 can lose its 4-bit alignment after a glitch (power dip, bumped
// wire, noise on E) and only the init sequence recovers it, so this runs every REDRAW_MS, not once.
// The custom bar characters live in the LCD's own memory, so they are rebuilt here as well.
void draw() {
  lcd.begin(16, 2);
  createBarChars();
  drawTitle();
  drawBars();
  lastDraw = millis();
}

void scrollStep() {
  uint8_t n = strlen(title);
  if (n <= LCD_COLS) return;
  if (hold) {
    hold--;
    return;
  }
  pos = (pos + 1) % (n + SCROLL_GAP);
  if (pos == 0) hold = HOLD_TICKS;
  drawTitle();
}

void setText(const char* text) {
  strncpy(title, text, TEXT_MAX);
  title[TEXT_MAX] = 0;
  pos = 0;
  hold = HOLD_TICKS;
  draw();
  lastScroll = millis();
}

void setupBands() {
  float ratio = powf(BAND_HIGH_HZ / BAND_LOW_HZ, 1.0f / (BANDS - 1));
  float f = BAND_LOW_HZ;
  for (uint8_t b = 0; b < BANDS; b++) {
    coeff[b] = (int16_t)lroundf(2.0f * cosf(2.0f * M_PI * f / SAMPLE_RATE) * 1024.0f);
    f *= ratio;
  }
}

// Sensitivity shapes the curve from signal strength to bar height: height = 8 * (amp/ref)^gamma.
// gamma 0.5 (level 9) boosts weak bands so most bars are tall; gamma 2 (level 1) only lets strong peaks reach the top.
void setSensitivity(uint8_t s) {
  s = constrain(s, 1, 9);
  float gamma = 0.5f + (9 - s) * 0.19f;
  for (uint8_t k = 1; k < 8; k++) thr[k] = powf((k + 0.5f) / 8.0f, 1.0f / gamma);
}

// Sample VIS_N points at SAMPLE_RATE and remove the DC offset (the sensor idles near 0 and swings up).
void sampleBlock() {
  uint32_t next = micros();
  int32_t sum = 0;
  for (uint8_t i = 0; i < VIS_N; i++) {
    while ((int32_t)(micros() - next) < 0) {}
    next += PERIOD_US;
    int16_t v = analogRead(PIN_MIC);
    block[i] = v;
    sum += v;
  }
  int16_t mean = sum / VIS_N;
  for (uint8_t i = 0; i < VIS_N; i++) {
    int16_t x = block[i] - mean;
    block[i] = constrain(x, -500, 500);  // the clamp keeps the fixed-point filter below inside 32 bits
  }
}

// One Goertzel filter per band, then map the amplitudes to bar heights.
void analyze() {
  float top = 0;
  for (uint8_t b = 0; b < BANDS; b++) {
    const int32_t c = coeff[b];
    int32_t s1 = 0, s2 = 0;
    for (uint8_t i = 0; i < VIS_N; i++) {
      int32_t s = block[i] + ((c * s1) >> 10) - s2;
      s2 = s1;
      s1 = s;
    }
    float p = (float)s1 * s1 + (float)s2 * s2 - (c / 1024.0f) * (float)s1 * s2;
    amp[b] = sqrtf(p > 0 ? p : 0) * (2.0f / VIS_N);
    if (amp[b] > top) top = amp[b];
  }
  ref = max(top, ref * REF_DECAY);
  if (ref < MIN_REF) ref = MIN_REF;
  for (uint8_t b = 0; b < BANDS; b++) {
    uint8_t lv = 0;
    if (amp[b] >= GATE_AMP) {
      float v = amp[b] / ref;
      lv = 1;
      for (uint8_t k = 1; k < 8; k++) {
        if (v >= thr[k]) lv = k + 1;
      }
    }
    if (level[b] > 0 && lv + 1 < level[b]) lv = level[b] - 1;  // bars fall one step per frame
    level[b] = lv;
  }
}

void printDebug(uint32_t frameUs) {
  Serial.print('V');
  for (uint8_t b = 0; b < BANDS; b++) {
    Serial.print(' ');
    Serial.print((int)amp[b]);
  }
  Serial.print(' ');
  Serial.print((int)ref);
  Serial.print(' ');
  Serial.println(frameUs / 1000);
}

void handleLine() {
  if (len > 1 && line[0] == 'L') {
    setText(line + 1);
  } else if (len > 1 && line[0] == 'B') {
    analogWrite(PIN_BACKLIGHT, constrain(atoi(line + 1), 0, 255));
  } else if (len > 1 && line[0] == 'S') {
    setSensitivity(atoi(line + 1));
  } else if (len > 1 && line[0] == 'D') {
    debug = line[1] == '1';
  }
}

void capture() {
  Serial.print(F("AUDIO "));
  Serial.println(TOTAL);
  uint32_t next = micros();
  for (uint32_t i = 0; i < TOTAL; i++) {
    while ((int32_t)(micros() - next) < 0) {}
    next += PERIOD_US;
    Serial.write((uint8_t)(analogRead(PIN_MIC) >> 2));  // 10-bit -> 8-bit
  }
}

// True once per press (debounced). Internal pull-up: the pin reads LOW while the button is down.
bool buttonPressed() {
  bool down = digitalRead(PIN_BTN) == LOW;
  if (down != btnDown && millis() - btnChange > 30) {
    btnDown = down;
    btnChange = millis();
    return down;
  }
  return false;
}

// Show feedback on the LCD right away, then stream the audio. The PC script answers with the result.
void listen() {
  memset(level, 0, sizeof(level));
  ref = MIN_REF;
  setText("Listening...");
  capture();
  btnDown = digitalRead(PIN_BTN) == LOW;  // a button still held after the capture must not retrigger
  btnChange = millis();
}

void setup() {
  Serial.begin(BAUD);
  pinMode(PIN_BTN, INPUT_PULLUP);
  pinMode(PIN_BACKLIGHT, OUTPUT);
  analogWrite(PIN_BACKLIGHT, BACKLIGHT_DEFAULT);
  ADCSRA = (ADCSRA & ~0x07) | 0x04;  // ADC clock /16: a read takes ~16 us instead of ~112 us
  setupBands();
  setSensitivity(SENS_DEFAULT);
  delay(1000);  // let the LCD settle after power-up/reset before the init sequence
  draw();
  Serial.println(F("BOOT"));
}

void loop() {
  uint32_t frameStart = micros();
  if (buttonPressed()) listen();
  if (len > 0 && millis() - lastByte > 500) len = 0;  // drop a half-received line so it can't swallow the next command
  while (Serial.available()) {
    lastByte = millis();
    char c = Serial.read();
    if (len == 0 && c == 'g') {
      listen();
    } else if (c == '\n') {
      line[len] = 0;
      handleLine();
      len = 0;
    } else if (c != '\r' && len < sizeof(line) - 1) {
      line[len++] = c;
    }
  }
  uint32_t now = millis();
  if (now - lastDraw >= REDRAW_MS) {
    draw();
  } else if (now - lastScroll >= SCROLL_MS) {
    scrollStep();
    lastScroll = now;
  }
  sampleBlock();
  analyze();
  drawBars();
  if (debug) printDebug(micros() - frameStart);
}

🐍 Kód Python

Beží na počítači. Inštalácia balíkov: pip install pyserial numpy requests. API kľúč sa vkladá do súboru host/audd_token.txt. Spustenie: python host/recognize.py COM9 (číslo portu sa zisťuje v Správcovi zariadení).

"""Hľadač piesní (PC strana): čaká na stlačenie tlačidla na Arduine Mega, rozpozná nahrávku cez AudD a ukáže ju na LCD.

Použitie: python recognize.py COM9 [jas%]   (stlač tlačidlo na Arduine; Ctrl+C ukončí program)

API kľúč sa hľadá v tomto poradí:
  1. súbor host/audd_token.txt (jeden riadok, súbor je v .gitignore, nikdy sa nenahrá na GitHub)
  2. premenná prostredia AUDD_API_TOKEN
"""
import io
import msvcrt  # len Windows: číta klávesy + a -
import os
import pathlib
import sys
import time
import unicodedata
import wave

import numpy as np
import requests
import serial

BAUD = 250000
SAMPLE_RATE = 8000
LCD_MAX = 120  # longest text the Mega accepts (TEXT_MAX in the sketch)
SENS_DEFAULT = 6  # bar sensitivity at startup, same scale as SENS_DEFAULT in the sketch


def load_token() -> str:
    path = pathlib.Path(__file__).with_name("audd_token.txt")
    token = path.read_text(encoding="utf8").strip() if path.exists() else os.environ.get("AUDD_API_TOKEN", "").strip()
    if not token or token == "SEM_VLOZ_SVOJ_TOKEN":
        sys.exit("Chýba AudD kľúč. Vlož ho do súboru host/audd_token.txt (pozri audd_token.example.txt).")
    return token


AUDD_TOKEN = load_token()

PUNCTUATION = {"–": "-", "—": "-", "‘": "'", "’": "'", "“": '"', "”": '"', "…": "..."}
EXTRA_LETTERS = {"ł": "l", "Ł": "L", "đ": "d", "Đ": "D", "ø": "o", "Ø": "O", "ß": "ss", "æ": "ae", "Æ": "AE", "œ": "oe", "Œ": "OE", "ı": "i"}


def lcd_safe(text: str) -> str:
    """Plain ASCII for the LCD, whose font has no accented letters: é -> e, š -> s, ť -> t, ď -> d, and so on."""
    text = unicodedata.normalize("NFKD", text)  # splits "š" into "s" + a combining caron
    text = "".join(c for c in text if not unicodedata.combining(c))
    return text.translate(str.maketrans({**PUNCTUATION, **EXTRA_LETTERS}))


def lcd(ser: serial.Serial, text: str) -> None:
    """Show text on the top LCD row (the bottom row is the Mega's live spectrum analyzer).

    The Mega scrolls text longer than 16 characters. Non-ASCII becomes '?'. The Mega's serial buffer is
    64 bytes and it is busy while it redraws the LCD, so the message goes out in small chunks with a
    short pause between them.
    """
    # The leading newline ends any half-received line the Mega might still be holding, so it can't swallow ours.
    data = f"\nL{lcd_safe(text).replace(chr(10), ' ')[:LCD_MAX]}\n".encode("ascii", errors="replace")
    for i in range(0, len(data), 24):
        ser.write(data[i:i + 24])
        ser.flush()
        time.sleep(0.05)


def to_wav(u8: np.ndarray) -> bytes:
    s = u8.astype(np.float32)
    print(f"min {u8.min()}  max {u8.max()}  mean {s.mean():.0f}  clipped {np.mean((u8 >= 250) | (u8 <= 5)):.0%}")
    s -= s.mean()
    peak = np.abs(s).max()
    if peak < 2:
        raise RuntimeError("audio is silent: check the sensor wiring (S to A4)")
    pcm = (s / peak * 30000).astype(np.int16)
    buf = io.BytesIO()
    with wave.open(buf, "wb") as w:
        w.setnchannels(1)
        w.setsampwidth(2)
        w.setframerate(SAMPLE_RATE)
        w.writeframes(pcm.tobytes())
    return buf.getvalue()


def identify(ser: serial.Serial, n: int) -> None:
    """Read the n audio bytes that follow an AUDIO header, look the song up, and show it on the LCD."""
    # Never change ser.timeout here: reconfiguring a Windows port while samples stream in can drop a byte.
    raw = bytearray()
    deadline = time.time() + 15
    while len(raw) < n and time.time() < deadline:
        raw += ser.read(n - len(raw))
    if len(raw) < n * 0.98:
        raise RuntimeError(f"got {len(raw)} of {n} bytes")
    if len(raw) != n:
        print(f"note: {n - len(raw)} sample(s) missing, carrying on")
    wav = to_wav(np.frombuffer(bytes(raw), dtype=np.uint8))
    with open("last_capture.wav", "wb") as f:
        f.write(wav)
    lcd(ser, "Searching...")
    r = requests.post(
        "https://api.audd.io/",
        data={"api_token": AUDD_TOKEN},
        files={"file": ("capture.wav", wav, "audio/wav")},
        timeout=30,
    )
    result = r.json()
    song = result.get("result")
    if song:
        print(f"=> {song['artist']} - {song['title']}")
        lcd(ser, f"{song['title']} - {song['artist']}")
    else:
        print(f"=> no match ({result.get('error', {}).get('error_message', 'unknown song')})")
        lcd(ser, "No match, try again")


def main() -> None:
    if len(sys.argv) < 2:
        sys.exit("Použitie: python recognize.py COM9 [jas%]")
    ser = serial.Serial()
    ser.port = sys.argv[1]
    ser.baudrate = BAUD
    ser.timeout = 1
    ser.dtr = False  # keep DTR low so opening/closing the port does not reset the Mega (and wipe the LCD)
    ser.rts = False
    ser.open()
    time.sleep(0.3)
    ser.reset_input_buffer()
    if len(sys.argv) > 2:  # optional backlight brightness in percent, e.g. "python recognize.py COM9 40"
        ser.write(f"\nB{round(int(sys.argv[2]) * 255 / 100)}\n".encode("ascii"))
    sens = SENS_DEFAULT
    ser.write(f"\nS{sens}\n".encode("ascii"))
    lcd(ser, "Press button to listen")
    print("Ready. Press the button on the Mega, then play the song near the mic. Ctrl+C to quit.")
    print("Click this window, then press + or - to change how sensitive the bars are (1-9).")
    ser.timeout = 0.2  # short, so key presses are noticed quickly (never changed again while audio streams)
    try:
        while True:
            while msvcrt.kbhit():
                key = msvcrt.getwch()
                if key in "+=" and sens < 9 or key in "-_" and sens > 1:
                    sens += 1 if key in "+=" else -1
                    ser.write(f"\nS{sens}\n".encode("ascii"))
                    print(f"bar sensitivity {sens}/9")
            header = ser.readline().decode("ascii", errors="replace").strip()
            if not header.startswith("AUDIO "):
                continue
            print("Listening...")
            try:
                identify(ser, int(header.split()[1]))
            except Exception as e:
                print(f"error: {e}")
                lcd(ser, f"Error: {e}")
            time.sleep(0.2)
            ser.reset_input_buffer()  # drop anything sent while we were busy
            print("Ready for the next press.")
    except KeyboardInterrupt:
        pass
    finally:
        time.sleep(0.3)  # let the last LCD line reach the Mega before the port closes
        ser.close()


if __name__ == "__main__":
    main()

🖥️ Príklad výstupu v termináli

Po stlačení tlačidla program vypíše úroveň zvuku a výsledok:

Výpis programu v termináli s rozpoznanou skladbou SuperStar
Výpis programu na počítači: úroveň zvuku a rozpoznaná skladba.
Listening...
min 0  max 46  mean 4  clipped 69%
=> Calin - SuperStar (feat. Dollar Prync)
Ready for the next press.

Keď skladbu nepozná:

Listening...
min 0  max 49  mean 2  clipped 86%
=> no match (unknown song)
Ready for the next press.

Čo ukazuje horný riadok displeja

TextVýznam
Song finderúvodný stav po štarte
Press button to listenprogram beží a čaká na tlačidlo
Listening…Arduino práve nahráva
Searching…čaká sa na odpoveď AudD
názov – interpretvýsledok (dlhý text sa posúva)
No match, try againskladba sa nerozpoznala

🎓 Prečo je to dobré pre výučbu

Projekt učí:

  • čítanie analógového vstupu (analogRead) a vzorkovanie zvuku
  • vzorkovaciu frekvenciu a prečo sa dá zachytiť len polovica z nej
  • spracovanie signálu (odpočítanie priemeru, Goertzelov filter, automatické zosilnenie)
  • sériovú komunikáciu medzi Arduinom a počítačom a návrh vlastného protokolu
  • prácu s LCD displejom HD44780 a vlastnými znakmi
  • PWM (zmena jasu podsvietenia)
  • ošetrenie tlačidla (debouncing, vnútorný pull-up)
  • volanie internetového API (HTTP POST, JSON) z Pythonu
  • cykly, podmienky, polia, funkcie, premenné a časovanie
  • zverejnenie projektu (Git, GitHub, webstránka)

A hlavne: výsledok je vidieť a počuť, takže je to motivujúce.


📝 Úlohy pre žiakov

1. Jednoduché

  • zmeniť jas podsvietenia (BACKLIGHT_DEFAULT)
  • zmeniť rýchlosť posúvania textu (SCROLL_MS)
  • zmeniť texty, ktoré displej zobrazuje
  • predĺžiť alebo skrátiť nahrávanie (SECONDS)

2. Stredné

  • zmeniť frekvenčné pásma analyzátora (BAND_LOW_HZ, BAND_HIGH_HZ)
  • upraviť citlivosť a šumovú bránu (GATE_AMP, SENS_DEFAULT)
  • ukladať rozpoznané skladby do súboru a vytvoriť z nich playlist
  • pridať druhé tlačidlo (napr. na zmenu jasu)

🧠 Dôležitá myšlienka

Mikrofón nepočuje hudbu, len meria napätie. Až spracovaním signálu z neho vznikne spektrum a rozpoznanie skladby.

⭐ Rozšírenie (pre rýchlejších žiakov)

  • pridať LED pásik alebo maticu, ktoré reagujú na 16 pásiem spektra
  • zobraziť na displeji aj diakritiku pomocou vlastných znakov
  • nahradiť počítač doskou s Wi-Fi (napr. ESP32), ktorá pošle nahrávku službe sama
  • vyskúšať lepší mikrofón (MAX4466 alebo MAX9814) a porovnať úspešnosť rozpoznávania

⚠️ Obmedzenia

  • Treba zapnutý počítač s internetom, Arduino skladby samo nerozpoznáva.
  • Senzor KS0035 je jednoduchý: v pokoji je na nule a kmitá len jedným smerom, takže zvuk je skreslený a rozpoznávanie nevyjde vždy. Pomáha hlasná skladba tesne pri senzore.
  • Zvuk má 8 kHz a 8 bitov, teda kvalitu telefónu.
  • Displej zobrazuje len základné znaky, preto sa diakritika nahrádza obyčajnými písmenami.

🎯 Kompetencie + dôvod pre školu

Tento projekt rozvíja:

  • prácu s elektronikou a zapojenie podľa schémy
  • programovanie v C++ (Arduino) aj v Pythone
  • spracovanie signálu a základy digitálneho zvuku
  • prácu s internetovým API
  • ladenie chýb: hľadali sme, prečo displej nereaguje, prečo sa stráca bajt pri prenose alebo prečo sa rozpoznávanie nedarí
  • prezentáciu a dokumentáciu projektu

Pre informatiku navyše učí: analogRead(), PWM, polia, funkcie, sériový protokol, HTTP a JSON.


👥 Autori a ako projekt vznikol

Autori: Tomáš Dobos a Boris Kalmár, študenti CSOŠE Frasattiho III. GDMPS.

Nápad prišiel z našej hlavy a všetko ostatné sme vymysleli my: čo má zariadenie robiť, ako bude vyzerať displej, ktoré súčiastky použijeme a ako to celé zapojíme. Zapojenie, skúšanie a ladenie sme robili sami na reálnom Arduine.

Na písanie kódu a ako pomoc pri projekte sme využili Claude Code (AI asistent od firmy Anthropic). Pomáhal nám s programom pre Arduino a Python, s hľadaním chýb a s webstránkou. Rozhodnutia o tom, čo sa bude robiť, a testovanie na hardvéri boli naše.

Naše vlastné doplnky k základnej myšlienke:

  • živé spektrum v dolnom riadku displeja
  • posúvanie dlhého textu
  • tlačidlo na spustenie počúvania
  • nastaviteľný jas podsvietenia a citlivosť spektra
  • nahradenie diakritiky, aby sa text zobrazil čitateľne
  • webstránka a zverejnenie kódu na GitHube

Zdrojový kód: https://github.com/tomaskooooo/arduino-rozpoznanie-pesniciek

Webstránka projektu: https://arduino-rozpoznanie-pesniciek.vercel.app/

Pridaj komentár