Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mp3_to_vgm

Convert an MP3 (or WAV/FLAC/...) recording into an AY-3-8910 VGM chiptune that plays in the MSX AY-3-8910/YM2149 VGM Analyzer.

A .vgm file is not audio — it is a timed log of register writes to the AY-3-8910 PSG (3 square-wave tone channels + 1 noise generator, 16 volume levels). This tool performs automatic music transcription: it analyses the recording frame-by-frame and re-synthesises it as a 3-voice chiptune. The AY-3-8910/YM2149 is the sound chip in the MSX, ZX Spectrum, Atari ST and many arcade boards.

Two quality levels

  • Fast (default) — analyses the raw mix and assigns the loudest distinct pitches each frame to the three voices. Quick, depends only on numpy/librosa/soundfile. Best for sparse, melodic material.
  • Best (--separate) — first splits the song into vocals / bass / other / drums stems with Demucs, then gives each stem its own voice: vocals→lead, other→harmony, bass→bass voice, drums→noise. Much cleaner on full mixes. Adds a one-time Demucs/PyTorch install and ~10–60 s of separation per track (cached afterwards).

Either way the result is lossy — three squares plus one noise channel can't hold a full arrangement — but melodic lines and basslines come through well.

Install

pip install -r requirements.txt
# ffmpeg must be on PATH for MP3 decoding:
#   macOS: brew install ffmpeg

demucs (and the PyTorch it pulls in) is only needed for --separate; the fast path runs without it. The first --separate run downloads the htdemucs model weights (~80 MB).

Usage

# fast path
python -m mp3_to_vgm.cli song.mp3                     # -> song.vgm and song.vgz
python -m mp3_to_vgm.cli song.mp3 out.vgm --self-check

# best quality: stem separation
python -m mp3_to_vgm.cli song.mp3 out.vgm --separate --self-check

Load the resulting .vgm or .vgz at https://hrubix.github.io/VGM_Analyzer/.

Options:

Option Effect
--separate split into stems and map one source per voice (best quality)
--demucs-model NAME Demucs model for --separate (default htdemucs)
--demucs-device D torch device mps/cpu/cuda (auto-detected)
--max-voices {1..3} cap simultaneous tone channels (drop inner harmony)
--no-noise disable the noise/percussion channel
--gain X volume scaling before quantisation (e.g. 2.0 if too quiet)
--fps {50|60} frame/timing rate: 50 = PAL, 60 = NTSC
--clock HZ AY input clock (default 1789772, MSX = 3.579545 MHz / 2)
--no-vgz don't also write the gzipped .vgz
--self-check validate the written .vgm header and stream

How it works

The pipeline is a chain of small stages, each owning one concern:

  1. separate.py (only with --separate) — split the mix into four stem WAVs with Demucs (get_model/apply_model), written via soundfile and cached under .stems_cache/ next to the output so re-runs are instant.
  2. audio.py — decode to mono PCM and run an STFT with one column per VGM frame (50 fps → 882-sample hop, matching the VGM 0x63 wait), plus per-frame onset / flatness / centroid features.
  3. transcribe.py — two paths producing the same Frame list:
    • transcribe (mix): peak-pick the strongest distinct pitches, assign to channels A/B/C with continuity, route percussive frames to noise.
    • transcribe_stems (--separate): one source per voice (bass low-clamped, vocals as lead, other polyphonic for harmony) with adaptive channel allocation and gated drum→noise.
  4. ay.py — map frequency → 12-bit tone period and amplitude → 4-bit log volume; the RegisterFile tracks the 14 registers and emits only changed values (delta encoding).
  5. vgm.py — write the VGM v1.51 header (AY8910 clock at 0x74) plus the A0 aa dd / 63 / 66 command stream; also gzip to .vgz.

Why drums sometimes don't play (--separate)

Demucs always emits a drums stem; on a kit-less acoustic recording it contains only breath/bow/bleed. Routing that to noise unconditionally produces a constant hiss, so the drum→noise mapping is gated: noise is disabled entirely unless drums are a meaningful share of the whole-mix energy (≥8%), and per frame it only fires on energy-dominant onset transients. The converter prints drums share of mix = X% — check that first if percussion is missing or over-present.

Limitations

Reducing full polyphonic audio to 3 squares + 1 noise is inherently lossy. Sparse, melodic source material transcribes best; dense mixes and vocals come out approximate even with --separate. Tune results with --separate, --gain, --max-voices, and --no-noise.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages