Chiptune recognition web service https://cat.sync.wtf
  • Python 67.9%
  • JavaScript 11.7%
  • Shell 9.7%
  • CSS 5.2%
  • HTML 4.6%
  • Other 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-27 13:52:14 +02:00
backend feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
bin Prepare for public release 2026-07-06 14:08:32 +02:00
external Prepare for public release 2026-07-06 14:08:32 +02:00
fingerprinter feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
gpaufp@15210ecf51 Update gpaufp submodule to fix GPU hash table store() performance 2026-07-27 13:50:21 +02:00
scripts Prepare for public release 2026-07-06 14:08:32 +02:00
utils feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
.dockerignore feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
.env.example feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
.gitignore feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
.gitmodules fix: use git.sync.wtf URL for gpaufp submodule 2026-07-27 13:52:14 +02:00
.pre-commit-config.yaml Prepare for public release 2026-07-06 14:08:32 +02:00
banlist.txt Prepare for public release 2026-07-06 14:08:32 +02:00
docker-compose.prod.yaml Add DB env vars to register service in docker-compose.prod.yaml 2026-07-27 13:50:21 +02:00
docker-compose.yaml feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
Dockerfile.backend fix: public release fixes - codeberg link, audfprint in image, icon mount 2026-07-06 14:58:48 +02:00
Dockerfile.register feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00
excluded.txt Prepare for public release 2026-07-06 14:08:32 +02:00
README.md feat: SNDH direct linking, AudFPrint fixes, and production improvements 2026-07-27 10:16:32 +02:00

Chip'n Tell

Match user-uploaded audio samples from real Atari ST recordings back to the original .sndh files.

Usage tip: Use a program to monitor the sound output as an input and run live matching in the background while watching demo shows. On Linux you can do this in pavucontrol.

This is the code that runs https://cat.sync.wtf

The rest of this README concerns those who want to self-host or develop on top of this codebase.

Quick Start

# Start all services
podman-compose -f docker-compose.yaml up -d

# Initial setup
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py init

Running Things

# Start all services
podman-compose -f docker-compose.yaml up -d

# Start individual services
podman-compose -f docker-compose.yaml up -d db backend register

# Run fingerprinter inside container
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py <command>

# AudFPrint commands
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py init
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py fingerprint /wav/path/to/file.wav
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py match /wav/path/to/excerpt.wav
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py list

# Register all .sndh files (run inside register container)
podman exec sndh_register_1 /sndh/register.sh /sndh_lf           # Sequential
podman exec sndh_register_1 /sndh/register.sh -j 8 /sndh_lf      # 8 parallel jobs
podman exec sndh_register_1 /sndh/register.sh -j $(nproc) /sndh_lf  # Use all cores

# After registration completes, build the hash table for fast matching
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py build-ht -o /wav/hash_table.pklz

Scraping New Songs from sndh.atari.org

New individual songs are published on sndh.atari.org between full archive releases. These are automatically scraped and registered separately from the main sndh_lf/ archive.

Automated Scraping (Cron)

The scraper runs automatically every day at 2 AM via cron in the register container. It:

  1. Fetches the updates table from sndh.atari.org
  2. Downloads new .sndh files to sndh_new/
  3. Tracks which song IDs have already been processed

Manual Scraping

# Scrape and download new songs from sndh.atari.org
podman exec sndh_register_1 python3 /sndh/scrape_new_songs.py

# Register the scraped songs (runs psgplay, fingerprinting, and builds hash table)
podman exec sndh_register_1 /sndh/register.sh -j 8 /sndh_new

# Verify the songs are in the database
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py list

Cleanup Before Full Archive Updates

When a new full archive is published, clean out the individually scraped songs before re-registering:

# Remove all files from sndh_new/
podman exec sndh_register_1 /sndh/cleanup_sndh_new.sh

# Then re-register the full archive
podman exec sndh_register_1 /sndh/register.sh -j 12 /sndh_lf

Local Development Environment

Prerequisites

  • Podman with rootless mode (tested on openSUSE Tumbleweed)
  • The podman API socket must be running:
    systemctl --user enable --now podman.socket
    

First-Time Setup

1. Ensure the podman socket is accessible:

# Verify the socket file exists
ls -la /run/user/$(id -u)/podman/podman.sock

# If missing, restart the socket
systemctl --user restart podman.socket

# Make the local connection the default (avoids SSH-to-VM fallback)
podman system connection add local unix:///run/user/$(id -u)/podman/podman.sock --default

2. Create .env from the template:

cp .env.example .env
# Edit .env to set a real password

3. Build and start the stack:

DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock \
  podman compose -f docker-compose.yaml up -d db backend

Note: DOCKER_HOST must be set because the docker-compose CLI plugin communicates via the Docker API socket, which podman emulates.

4. Initialize the database schema:

podman exec sndh-backend-1 python3 -c "
import os, psycopg2
host = os.getenv('DB_HOST', 'db')
user = os.getenv('DB_USER', 'postgres')
password = os.getenv('DB_PASSWORD', '')
dbname = os.getenv('DB_NAME', 'sndh_fingerprinter')
conn = psycopg2.connect(host=host, user=user, password=password, dbname='postgres')
conn.autocommit = True
cur = conn.cursor()
cur.execute('SELECT 1 FROM pg_database WHERE datname = %s', (dbname,))
if not cur.fetchone():
    cur.execute(f'CREATE DATABASE {dbname}')
    print(f'Created database: {dbname}')
cur.close()
conn.close()
conn = psycopg2.connect(host=host, user=user, password=password, dbname=dbname)
cur = conn.cursor()
cur.execute('''
    CREATE TABLE IF NOT EXISTS songs (
        id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, sndh_hash TEXT,
        title TEXT, composer TEXT, source_path TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
''')
cur.execute('''
    CREATE TABLE IF NOT EXISTS fingerprints (
        song_id INTEGER NOT NULL, hash INTEGER NOT NULL,
        time_position INTEGER NOT NULL,
        FOREIGN KEY (song_id) REFERENCES songs(id)
    )
''')
cur.execute('CREATE INDEX IF NOT EXISTS idx_fingerprints_song_id ON fingerprints(song_id)')
cur.execute('CREATE INDEX IF NOT EXISTS idx_fingerprints_hash ON fingerprints(hash)')
conn.commit()
cur.close()
conn.close()
print('DB initialized')
"
# Or use the built-in init_db if using the backend's database module:
# podman exec sndh-backend-1 python3 -c "from services.database import get_db_connection; get_db_connection()"

5. Verify everything is running:

curl http://localhost:8000/health
# Expected: {"status":"ok","songs_count":0}

curl http://localhost:8000/api/site-id-cache/stats
# Expected: {"entries":5937,"last_fetch":true}

Running Tests

cd backend && ../.venv/bin/python -m pytest tests/ -v

The services/ and static/ directories are mounted as volumes into the backend container, so code changes take effect immediately on container restart. Restart the backend after making changes:

podman restart sndh-backend-1

Known Issues & Workarounds

Issue Cause Fix
DOCKER_HOST required docker-compose plugin needs Docker API socket Set DOCKER_HOST=unix:///run/user/1000/podman/podman.sock before every compose command
user: root needed for db Rootless podman can't chown to postgres user Set user: root on the db service in docker-compose.yaml
No persistent DB volume Volume permissions fail in rootless podman Volume mount is commented out; data is ephemeral
init_db fails with ${DB_HOST:-db} config.json uses shell syntax not expanded by compose Fixed in fingerprinter/__init__.py to use env vars
gpus: all may fail without GPU GPU passthrough requires NVIDIA Container Toolkit Comment out gpus: all if no GPU available

How It Works

  • New songs are downloaded with their original filenames (e.g., YQN_-_GemTOS_2026.sndh)
  • Processed files go into sndh_new/ directory, separate from sndh_lf/
  • The register.sh script automatically processes sndh_new/ alongside sndh_lf/
  • Song IDs are tracked in sndh_new/.processed_ids.txt to avoid re-downloads
  • Files are removed during full archive re-registration via cleanup_sndh_new.sh

Hash Table (.pklz)

Matching uses a pre-built AudFPrint hash table for speed. Without it, matching falls back to building the hash table from the database on every query (minutes instead of seconds).

Pipeline: register.sh stores fingerprints in DB → build-ht reads DB → writes /wav/hash_table.pklzmatch loads .pklz

# Build hash table from DB contents
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py build-ht -o /wav/hash_table.pklz

# Match uses the pre-built table automatically; if missing, builds from DB (slow, logged as warning)
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py match /wav/path/to/excerpt.wav

Rebuild: After re-registering songs (e.g., after composer normalization changes), rebuild the hash table:

# 1. Delete sha256 markers to force re-registration
find /wav -name "*.sha256" -type f -delete

# 2. Re-run registration
podman exec sndh_register_1 /sndh/register.sh -j 12 /sndh_lf

# 3. Truncate DB (songs have UNIQUE constraint on name)
podman exec sndh_register_1 python3 -c "
import psycopg2, json
c = json.load(open('/sndh/fingerprinter/config.json'))
conn = psycopg2.connect(host=c['host'], user=c['user'], password=c['password'], dbname=c['database'])
conn.cursor().execute('TRUNCATE songs CASCADE'); conn.commit(); conn.close()
"

# 4. Re-register
podman exec sndh_register_1 /sndh/register.sh -j 12 /sndh_lf

# 5. Rebuild hash table
podman exec sndh_register_1 python3 /sndh/fingerprinter/__init__.py build-ht -o /wav/hash_table.pklz

Skip mechanism: register.sh uses .sha256 marker files in /wav/ to skip already-processed songs. If a marker file exists and contains the matching SHA256 hash of the .sndh file, the song is skipped. To force re-processing, delete the marker files.

JavaScript Linting

Linting runs in a container using oxlint (no Node.js installation required).

# Run linting
./lint-js.sh

# Via pre-commit (runs on staged files)
.venv/bin/pre-commit run js-lint

# Install pre-commit hooks
.venv/bin/pre-commit install

Pre-commit hooks are configured in .pre-commit-config.yaml. JavaScript files in backend/static/ are linted automatically on commit.

REST API

Health Check

GET /health

Response:

{
  "status": "ok",
  "songs_count": 9931
}

List/Search Songs

GET /api/songs?q=Nibbel&composer=Techno&limit=50&offset=0

Parameters:

  • q - Search song name (case-insensitive)
  • composer - Filter by composer (case-insensitive)
  • limit - Max results (1-200, default 50)
  • offset - Pagination offset (default 0)

Response:

{
  "songs": [
    {
      "id": 1,
      "name": "Techno/Dentro.sndh.1",
      "sndh_hash": "abc123...",
      "title": "Dentro",
      "composer": "Techno"
    }
  ],
  "total": 9931,
  "limit": 50,
  "offset": 0
}

Get Song Details

GET /api/songs/{id}

Response:

{
  "id": 1,
  "name": "Techno/Dentro.sndh.1",
  "sndh_hash": "abc123...",
  "title": "Dentro",
  "composer": "Techno",
  "source_path": "Techno/Dentro.sndh.1.in.wav",
  "created_at": "2025-05-22T10:00:00"
}

Upload Audio for Matching

POST /api/upload
Content-Type: multipart/form-data

file: <audio file>
min_matches: 10 (optional, default 10)

Accepts any format ffmpeg can decode (MP3, WAV, OGG, FLAC, etc.) Max file size: 50MB Rate limit: 5 uploads/minute per IP

Response:

{
  "matches": [
    {
      "song_name": "Techno/Dentro.sndh.1",
      "song_id": 1,
      "title": "Dentro",
      "composer": "Techno",
      "matches": 4080,
      "match_rate": 99.0,
      "coverage": 85.3,
      "confidence": 93.5,
      "time_offset": 0
    }
  ],
  "file_name": "recording.mp3",
  "file_size": 2189725
}

Matching quality indicators:

  • Strong match: 20+ aligned hashes, 0.5%+ rate, 25%+ confidence
  • Weak match: <10 aligned hashes, <0.1% rate (likely false positive)
  • Clean WAV self-match: 100% rate, thousands of aligned hashes

Clicking the song title or path in the UI opens the song's search results on sndh.atari.org.

Database Schema

songs table

Column Type Description
id SERIAL Primary key
name TEXT Song identifier (e.g., "Techno/Dentro.sndh.1")
sndh_hash TEXT SHA256 hash of source .sndh file
title TEXT Title from TITL tag
composer TEXT Composer from COMM tag
source_path TEXT Relative path to WAV file (e.g., "Techno/Dentro.sndh.1.in.wav")
created_at TIMESTAMP Registration timestamp

fingerprints table

Column Type Description
song_id INTEGER Foreign key to songs.id
hash INTEGER AudFPrint hash value
time_position INTEGER Frame position in audio

AudFPrint Configuration

Key parameters and their recommended values:

--density 20.0       # Target hashes per second
--hashbits 20        # Bits per hash (1M distinct fingerprints)
--bucketsize 100     # Entries per bucket
--maxtimebits 14     # Time range up to ~6 minutes
--min-count 10       # Minimum matching hashes for a match (MIN_MATCHES)
--match-win 2        # Maximum frame skew for matches (matcher.window)

Application-level thresholds:

MIN_MATCHES = 10     # Minimum aligned hashes to consider a match
MIN_MATCH_RATE = 0.001  # Minimum match rate (0.1%) to consider a match

Matching quality indicators:

  • Clean WAV self-match: 100% rate, thousands of aligned hashes
  • Real recording match: 20+ aligned hashes, 0.5%+ rate
  • False positive threshold: <10 aligned hashes, <0.1% rate

Protection Limits

  • File size: 50MB max
  • Rate limit: 5 uploads/minute per IP
  • Disk space: 100MB free required
  • Database capacity: 10,000 songs max (configurable)

GPU Acceleration

The system automatically uses GPU-accelerated AudFPrint modules when available, providing 2-15x speedup for fingerprinting and matching operations.

Requirements

Host System (for local development)

  • NVIDIA GPU with CUDA support
  • NVIDIA drivers installed
  • NVIDIA Container Toolkit installed

Install NVIDIA Container Toolkit (Ubuntu/openSUSE):

# Add NVIDIA package repository
distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \
   && curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - \
   && curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

# Install NVIDIA Container Toolkit
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

For openSUSE:

sudo zypper install nvidia-container-toolkit
sudo systemctl restart docker

Verify installation:

nvidia-smi  # Should show GPU information
docker run --rm --gpus all nvidia/cuda:13.2.2-base nvidia-smi  # Should show GPU info in container

Container Images

The backend and register images now use nvidia/cuda:13.2.2-base-ubuntu22.04 as base and include:

  • CUDA 13.2.2 runtime (compatible with CUDA 13.0-13.3)
  • CuPy (cupy-cuda13x) for GPU-accelerated NumPy
  • cuSignal for GPU-accelerated signal processing

Running with GPU Support

Local Development (Podman)

# Enable GPU in podman (may require root or user configuration)
podman system connection default --set-default

# Start services with GPU access
podman-compose -f docker-compose.yaml up -d

# Verify GPU is accessible in container
podman exec sndh_backend_1 nvidia-smi
podman exec sndh_register_1 nvidia-smi

Note: Podman may require additional configuration for GPU access. See Podman GPU documentation.

Production (rootless Docker)

The production host uses rootless Docker with GPU support. The compose file is docker-compose.prod.yaml.

  1. Install NVIDIA Container Toolkit on the host
  2. Create a .env file with POSTGRES_PASSWORD and DB_PASSWORD
  3. Provision a data directory at the path set in DATA_DIR containing sndh_lf/, sndh_new/, wav/, banlist.txt, gpaufp/, and icon.png
  4. Start services:
docker compose -f docker-compose.prod.yaml up -d

Required .env variables:

  • POSTGRES_PASSWORD — database password
  • DB_PASSWORD — backend database password (same)
  • REGISTRY — container registry URL (e.g. registry.example.com/project)
  • DATA_DIR — path to data directory on host (e.g. /home/user/sndh)

Optional .env variables:

  • DOCKER_SOCK — path to Docker socket (default: /run/user/988/docker.sock)

GPU Configuration Options

The GPU acceleration can be controlled via environment variables:

Variable Values Default Description
NVIDIA_VISIBLE_DEVICES all, 0, 1, 0,1 all Which GPUs to expose
NVIDIA_DRIVER_CAPABILITIES compute, utility, all compute,utility GPU capabilities

Example - Limit to GPU 0 only:

# In docker-compose.yaml
environment:
  - NVIDIA_VISIBLE_DEVICES=0

Monitoring GPU Usage

Check GPU utilization:

# On host
nvidia-smi

# In container
podman exec sndh_backend_1 nvidia-smi
docker exec sndh-backend-1 nvidia-smi

Check if GPU acceleration is active:

# Check fingerprinter logs
podman logs sndh_backend_1 | grep -i gpu

# Or check directly
podman exec sndh_backend_1 python3 -c "import sys; sys.path.insert(0, '/app/fingerprinter'); from fingerprinter import GPU_AVAILABLE; print('GPU:', GPU_AVAILABLE)"

VRAM Requirements

  • Minimum: 500MB free VRAM (configurable in fingerprinter/__init__.py)
  • Recommended: 1GB+ for production dataset (9,951 songs, 46M fingerprints)
  • Hash table size: ~350-400MB in GPU memory

The system automatically falls back to CPU if insufficient VRAM is available.

Fallback Behavior

If GPU is not available (no NVIDIA drivers, no CuPy, or insufficient VRAM):

  • System automatically uses CPU-only AudFPrint modules
  • All functionality works normally (just slower)
  • Logs a warning message: Using CPU-only AudFPrint modules

No manual configuration is required - the fallback is completely transparent.