Hľadač piesní

Projekt pre Arduino Mega

Hľadač piesní

Malé zariadenie, ktoré päť sekúnd počúva hudbu, cez internet rozpozná skladbu a zobrazí jej názov a interpreta na LCD displeji. Dolný riadok ukazuje živé spektrum zvuku.

  • 16 pásiem
  • 20 snímok za sekundu
  • 5 s nahrávka
Ukážka displeja počas hrania. Hore sa posúva názov a interpret, dole poskakujú stĺpce podľa hudby.

Čo to je

Equalizer, ktorý vie pomenovať pieseň

Hľadač piesní spája zvukový senzor, Arduino Mega, LCD displej a online službu na rozpoznávanie hudby. Stlačíš tlačidlo a Arduino päť sekúnd nahráva zvuk z okolia. Nahrávku pošle počítaču, ten ju odošle službe AudD a tá vráti názov skladby a interpreta. Výsledok sa zobrazí v hornom riadku displeja. Dlhší text sa po riadku posúva, aby sa dalo prečítať každé slovo.

Dolný riadok displeja pracuje samostatne. Arduino v ňom stále zobrazuje 16 stĺpcov, každý pre jedno frekvenčné pásmo, od basov vľavo po výšky vpravo. Stĺpce poskakujú podľa toho, čo práve hrá, takže displej funguje aj ako malý equalizer.

Arduino Mega nemá internet ani dosť výkonu na rozpoznávanie skladieb, preto sa o prácu delí s počítačom.

Arduino MegaZbiera a zobrazuje

Číta tlačidlo, vzorkuje zvukový senzor, kreslí na LCD a počíta spektrum.

PočítačRozpoznáva

Prijme nahrávku, pošle ju službe AudD a výsledok vráti na displej.

Média

Ako to vyzerá naživo

Video ukážkaassets/video/demo.mp4
Arduino Mega s LCD displejom, zvukovým senzorom a tlačidlom. Na displeji sa posúva názov skladby SuperStar
Fotka 1assets/img/foto-1.jpg
Hotové zariadenie. Po rozpoznaní sa názov posúva v hornom riadku.
Zapojenie na nepájivom poli, displej hlási Listening
Fotka 2assets/img/foto-2.jpg
Zariadenie počúva: displej hlási „Listening...“.
Výpis programu v termináli s rozpoznanou skladbou SuperStar
Fotka 3assets/img/foto-3.jpg
Výpis programu na počítači: úroveň zvuku a rozpoznaná skladba.

Hardvér

Čo potrebuješ

1 ksArduino Mega 2560

Riadi všetko a komunikuje s počítačom cez USB.

1 ksZvukový senzor KS0035

Analógový mikrofón. Trimer na ňom mení zosilnenie.

1 ksLCD displej 16×2

Čip HD44780 s 16 pinmi, bez I2C modulu.

1 ksTlačidlo so 4 nožičkami

Spustí počúvanie.

1 ksPotenciometer 10 kΩ

Kontrast displeja. Alternatívou sú rezistory 4,7 kΩ a 1 kΩ.

1 ksRezistor 220 Ω

Obmedzuje prúd podsvietenia.

–Nepájivé pole a vodiče

Zapojenie bez spájkovania.

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. Ovládanie citlivosti klávesmi používa funkciu, ktorá je len vo Windowse.
  • Bezplatný účet na audd.io a API kľúč.

Hardvér

Zapojenie

Všetko sa zapája na nepájivom poli. Najdôležitejšie je správne poradie pinov displeja. Displej beží v 4-bitovom režime, takže piny 7 až 10 (D0 až D3) nechaj nezapojené.

LCD displej 16×2

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

Pin senzoraZapojiť na
GGND
V5V
SMega pin A4

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

Tlačidlo

Štvornohé tlačidlo má nožičky spojené po dvojiciach. Zapoj ho uhlopriečne: jednu nožičku na Mega pin 2 a protiľahlú na GND. Rezistor netreba, program zapína vnútorný pull-up.

Podsvietenie a kontrast

Rezistor 220 Ω vedie z vývodu A na pin 9, ktorý vie meniť jas. Kontrast nastavíš potenciometrom medzi 5V a GND so stredným vývodom na V0. Bez neho urob delič: 4,7 kΩ z 5V na V0 a 1 kΩ z V0 na GND. V0 priamo na GND je maximálny kontrast, pri ktorom sa písmená strácajú v štvorčekoch.

Softvér

Služba AudD

AudD je online služba na rozpoznávanie hudby. Pošleš jej krátku nahrávku, stačí pár sekúnd, 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. Zaregistruj sa na dashboard.audd.io.
  2. Na hlavnej stránke dashboardu nájdeš svoj api_token. Skopíruj ho.
  3. Po registrácii máš skúšobné obdobie, v ktorom môžeš API vyskúšať zadarmo. Aktuálne limity a ceny sú v dashboarde.

Ako vyzerá požiadavka

Stačí jeden HTTP POST na https://api.audd.io/ s poľom api_token a súborom v poli file.

import requests

r = requests.post(
    "https://api.audd.io/",
    data={"api_token": "SEM_VLOZ_SVOJ_TOKEN"},
    files={"file": open("nahravka.wav", "rb")},
)
print(r.json())
{
  "status": "success",
  "result": {
    "artist": "Tears For Fears",
    "title": "Everybody Wants To Rule The World",
    "album": "Songs From The Big Chair",
    "release_date": "2014-11-10",
    "timecode": "00:56"
  }
}

Keď AudD skladbu nepozná, pole result je null a program zobrazí „No match, try again“. Pri zlom kľúči vráti chybu authorization failed.

Pozor API kľúč je ako heslo. Nevkladaj ho do kódu, ktorý zverejňuješ, a nedávaj ho na webovú stránku. Kto ho uvidí, môže míňať tvoje dotazy. V tomto repozitári je kľúč v súbore host/audd_token.txt, ktorý Git ignoruje.

Princíp

Ako to funguje

  1. Zvukový senzormikrofón KS0035
  2. Arduino Megavzorky, LCD, tlačidlo
  3. Počítačprogram recognize.py
  4. AudDdatabáza skladieb

Jedno rozpoznanie krok za krokom

  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, pretože senzor je v pokoji blízko nuly. Potom signál zosilní na plnú hlasitosť a uloží ho ako 16-bitový súbor WAV.
  4. Rozpoznanie. Počítač odoš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á. Dlhý text sa v hornom riadku posúva.

Živé spektrum v dolnom riadku

Arduino meria zvuk neustále, aj keď práve nerozpoznáva. Jeden obrázok vzniká takto:

  1. Arduino odmeria 128 vzoriek pri 8 kHz, čo trvá 16 ms, a odpočíta ich priemer.
  2. Pre každé z 16 pásiem spustí Goertzelov filter. Je to zjednodušená FFT, ktorá počíta len jednu frekvenciu naraz, a preto sa zmestí do pamäte a výkonu Arduina Mega bez dodatočných knižníc. Pásma sú rozložené logaritmicky 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 posledných pár desatín sekundy, takže stĺpce poskakujú pri tichej aj hlasnej hudbe.
  4. Šumová brána skryje pásma slabšie ako šum senzora. V tichu sa displej nehýbe.
  5. Výška každého stĺpca sa zaokrúhli na jednu z 8 úrovní. Každá úroveň je vlastný znak displeja, ktorý Arduino nahrá do pamäte LCD. Stĺpec rastie okamžite a klesá o jeden stupeň za snímku.

Výsledkom je približne 20 snímok za sekundu. Počas päťsekundového nahrávania sa spektrum nekreslí a po nahraní sa obnoví samo.

Kód

Celý kód

Nahraj ho cez Arduino IDE. Vyber dosku „Arduino Mega or Mega 2560“ a správny port. Zavri pritom program v Pythone, lebo port môže mať otvorený naraz len jeden program. Súbor je firmware/song_finder/song_finder.ino.

Arduino C++ · 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);
}

Príkazy medzi Arduinom a počítačom

SmerSprávaVýznam
PC → ArduinoL<text>zobrazí text v hornom riadku (najviac 120 znakov)
PC → ArduinoB<0–255>jas podsvietenia
PC → ArduinoS<1–9>citlivosť spektra
PC → ArduinoD1 a D0ladiaci výpis zapnúť a vypnúť
Arduino → PCAUDIO 40000hlavička nahrávky, za ňou 40 000 bajtov vzoriek
Arduino → PCBOOTArduino sa práve spustilo

Program beží na počítači. Nainštaluj balíky, vlož AudD kľúč do súboru host/audd_token.txt a spusti ho s číslom portu Arduina.

pip install -r host/requirements.txt
python host/recognize.py COM9
python host/recognize.py COM9 50   # jas podsvietenia 50 %
Python 3 · host/recognize.py
"""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()

Spustenie

Ako to rozbehať

  1. Zapoj všetko podľa tabuliek a pripoj Arduino USB káblom.
  2. Nahraj Arduino kód.
  3. Vlož svoj AudD kľúč do súboru host/audd_token.txt.
  4. Spusti python host/recognize.py COM9. Číslo portu nájdeš v Správcovi zariadení v časti Porty (COM a LPT).
  5. Pusti hlasnú skladbu a daj reproduktor tesne k senzoru.
  6. Stlač tlačidlo. Výsledok sa zobrazí po piatich sekundách. Klávesmi + a - v okne programu meníš citlivosť spektra.

Čo ukazuje horný riadok

TextVýznam
Song finderúvodný stav po štarte Arduina
Press button to listenpočítačový program 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

Využitie

Na čo sa to dá použiť

Displej „čo hrá?“

V miestnosti, v dielni alebo na párty ukáže názov skladby bez telefónu.

Vizualizácia hudby

Dolný riadok funguje ako stolový equalizer aj bez rozpoznávania.

Učenie elektroniky

Analógové vstupy, vzorkovanie, spracovanie signálu, sériová komunikácia, displej a internetové API na jednom projekte.

Zoznam skladieb

Počítačový program môže každý výsledok uložiť do súboru a vytvoriť z neho playlist. Tento nápad tu zatiaľ nie je.

Svetelná show

Hodnoty 16 pásiem sa dajú poslať na LED pásik alebo maticu, ktoré budú reagovať na hudbu.

Krúžok a výstava

Princíp rozpoznávania hudby sa na malom zariadení dá ukázať názorne.

Obmedzenia a riešenie problémov

  • Potrebuješ 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, diakritika sa nahrádza obyčajnými písmenami.
Podsvietenie svieti, ale hore sú len štvorčeky
Displej nedostáva príkazy. Skontroluj E (pin 6 na Mega 23), RS (pin 4 na Mega 22) a RW (pin 5 na GND) a skús nové vodiče.
Displej je prázdny alebo text je slabý
Nastav kontrast potenciometrom na V0. Skontroluj aj VSS na GND a VDD na 5V.
Na displeji sú náhodné znaky
Dátové vodiče D4 až D7 sú v zlom poradí alebo uvoľnené. Nahraj tools/lcd_test/lcd_test.ino, ktorý skúša všetky poradia a čitateľný text ukáže správne.
Na displeji ostane „Listening...“
Nebeží Python program, preto nikto neodpovedá.
Stále „No match“
Pusti hlasnejšiu skladbu, daj reproduktor bližšie, skús trimer na senzore a zopakuj.
Chyba api_token is incorrect
Skontroluj kľúč v host/audd_token.txt a stav účtu na dashboard.audd.io.
Nahrávanie do Arduina zlyhá
Port drží iný program. Zavri Python program a Serial Monitor.

Autori

Tomáš Dobos a Boris Kalmár

Obaja sú študenti CSOŠE Frasattiho III. GDMPS. Projekt vznikol ako práca s Arduinom: od zapojenia a firmvéru cez spracovanie zvuku až po tento web.

Projekt na GitHube