// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // vgm2song.c - a VGM chip log as a JoeyLib .song, for tools/songbake. // // JoeyLib's chip tracker (jlMusicPlay) plays three square tone voices, each // a frequency and a 4-bit attenuation (2 dB steps), and a noise voice, a // pitch 0..31 (the YM2149's noise period on the Atari ST's 2 MHz chip) and // an attenuation. A VGM file is a chip's register writes and the waits // between them, which a tracker exports as its song plays out: Furnace (the // recommended tracker, docs/Assets.adoc) does, as do most others. Write the // song for one of these and export VGM: // // TI SN76489 (Sega Master System, BBC Micro, TI-99/4A): three tones and a // noise channel, as JoeyLib has; the attenuation carries over as it is. // Its noise runs at one of three rates or tone 2's, taken to the noise // pitch nearest that rate. Its "periodic" noise is a pitched pulse (the // noise rate over the shift register's width: the Master System's bass // trick), so it plays on a tone voice that is free, at that pitch; with // all three busy it plays as noise, with a warning. A chip without the // /8 prescaler (the VGM's flag says so, or its clock is under // SN_DIRECT_CLOCK_MAX: the TI-99/4A's 447 kHz SN94624, whose files // Furnace leaves unflagged) divides its clock by 4 where the SN76489 // divides by 32. // AY-3-8910 / YM2149 (the Atari ST's chip): three tones; its noise mixes // into the tone channels, so the noise voice sounds while any channel // that has it enabled sounds, at the loudest of them. Volume v is // attenuation 15 - v (exact on the ST). The volume envelope is followed // tick by tick (its period and shape, restarted by each write to its // shape); one that repeats faster than two rows is a timbre (the // "buzzer"), not a volume: it plays at full volume, and a channel whose // tone is off sounds at the envelope's own frequency, as the buzzer // does, with a warning. // // The song is the chip's state at the end of each tick: one .song row a // tick (its tickms), sampled from the VGM's own timing, so the length and // the loop point are kept to within a tick and nothing drifts. The default // tick is the VGM's update interval rounded down to whole milliseconds (16 // for 60 Hz, 20 for 50 Hz), short enough that every update keeps a row of // its own: its header's rate, or the shortest gap between writes that comes // more than once when that is shorter (a song whose engine outruns the // system's rate). A row holds what changed since the one before it; the // loop row restates every voice, so a loop starts from the right state. Runs // of empty rows become repeat blocks. A tone too high to hear (above // MAX_AUDIBLE_HZ: the chips' PCM trick) is silence. // // vgm2song [--chip sn|ay] [--tick MS] IN.vgm OUT.song // // IN may be gzip-compressed (.vgz). --chip picks the chip when the VGM // drives both; only the first of a pair of chips is converted, and any other // chip (the Master System's FM unit, say) is left out, with a warning. The // JYM1 stream songbake makes must stay under 64 KB: without --tick, a song // that would not fit gets the shortest tick, a whole multiple of the // default, that does (merging changes faster than it: vibrato, arpeggios), // with a warning; --tick is taken as it is. // // Times are in VGM samples (44100 a second) and computed in doubles in the // order the Python original computed them, so the rows come out the same. #include #include #include #include #include #include "toolBuf.h" #include "toolFile.h" #include "toolZlib.h" #define TOOL_NAME "vgm2song" #define USAGE_EXIT 2 #define VGM_IDENT "Vgm " #define VGM_IDENT_BYTES 4u #define VGM_RATE 44100 #define VGM_HEADER_MIN 0x40u #define VGM_VERSION_FIELD 0x08u #define VGM_SN_CLOCK_FIELD 0x0Cu #define VGM_GD3_FIELD 0x14u #define VGM_LOOP_FIELD 0x1Cu #define VGM_RATE_FIELD 0x24u #define VGM_DATA_OFFSET_FIELD 0x34u #define VGM_DATA_OFFSET_V150 0x150u #define VGM_AY_CLOCK_FIELD 0x74u #define VGM_AY_CLOCK_V151 0x151u #define VGM_CLOCK_MASK 0x3FFFFFFFu #define GZIP_MAGIC0 0x1Fu #define GZIP_MAGIC1 0x8Bu #define GD3_IDENT "Gd3 " #define GD3_SIZE_AT 8u #define GD3_TEXT_AT 12u #define GD3_TITLE 0u #define GD3_AUTHOR 6u #define GD3_STRINGS (GD3_AUTHOR + 1u) #define GD3_TEXT_MAX 256u #define UTF16_SURROGATE_HI 0xD800u #define UTF16_SURROGATE_LO 0xDC00u #define UTF16_SURROGATE_END 0xE000u #define PRINTABLE_FIRST 32u #define PRINTABLE_END 127u #define CMD_SN 0x50u #define CMD_SN2 0x30u #define CMD_AY 0xA0u #define CMD_WAIT 0x61u #define CMD_WAIT_NTSC 0x62u #define CMD_WAIT_PAL 0x63u #define CMD_END 0x66u #define CMD_DATA_BLOCK 0x67u #define DATA_BLOCK_SIZE_AT 3u #define DATA_BLOCK_HEADER 7u #define WAIT_NTSC 735 #define WAIT_PAL 882 #define AY_SECOND_CHIP 0x80u #define SHORT_WAIT_FIRST 0x70u #define SHORT_WAIT_LAST 0x7Fu #define DAC_WAIT_FIRST 0x80u #define DAC_WAIT_LAST 0x8Fu // JoeyLib's voices (tools/songbake, src/core/music.c). #define TONE_VOICES 3u #define VOICES 4u #define NOISE 3u #define ATTEN_OFF 15u #define ATTEN_MAX 14u #define NOISE_PITCH_MAX 31.0 #define FREQ_MAX 65535.0 #define MAX_AUDIBLE_HZ 20000.0 // The noise pitch is the YM2149's noise period on the ST's 2 MHz chip, // whose noise generator runs at 2 MHz / 16 / period. #define YM_NOISE_HZ (2000000.0 / 16.0) #define JYM_LIMIT 0xFFFFu #define TICK_MS_MAX 1000 #define MS_PER_SECOND 1000 #define JYM_NOTE_BYTES 4u #define JYM_HEADER_BYTES 8u #define JYM_WAIT_MAX 127u #define REPEAT_MIN 4u #define REPEAT_MAX 256u // The .song's columns: the widest cell is "@65535:14". #define CELL_WIDTH 10 #define CELL_TEXT_MAX 16u // The SN76489: tone = clock / (32 * period); noise rates clock / 512, 1024, // 2048, or tone 2's. Without the /8 prescaler, an eighth of those dividers. #define SN_TONE_DIVIDER 32.0 #define SN_NOISE_RATES 3u #define SN_PRESCALER 8.0 #define SN_DIRECT_CLOCK_MAX 1000000u #define SN_NOISE_TONE2 3u #define SN_NOISE_RATE_MASK 3u #define SN_NOISE_WHITE 0x04u #define SN_NOISE_MASK 7u #define SN_PERIOD_ZERO 1024u #define SN_SHIFT_WIDTH_DEFAULT 16u #define VGM_SN_SHIFT_WIDTH 0x2Au #define VGM_SN_FLAGS 0x2Bu #define SN_FLAG_ZERO_IS_400 0x01u #define SN_FLAG_NO_PRESCALER 0x08u #define SN_LATCH 0x80u #define SN_CHANNEL_SHIFT 5u #define SN_CHANNEL_MASK 3u #define SN_VOLUME 0x10u #define SN_LOW_MASK 0x0Fu #define SN_HIGH_MASK 0x3Fu #define SN_PERIOD_HIGH 0x3F0u #define SN_HIGH_SHIFT 4u // The AY: tone = clock / (16 * period), noise clock / (16 * period); the // envelope steps 16 levels a cycle of 256 * period clocks. A YM2149 with // pin 26 low halves its clock (the VGM's AY flags say so). #define AY_DIVIDER 16u #define AY_REGISTERS 16u #define AY_REG_MASK 0x0Fu #define AY_REG_NOISE 6u #define AY_REG_MIXER 7u #define AY_REG_VOLUME 8u #define AY_REG_ENVELOPE_LO 11u #define AY_REG_ENVELOPE_HI 12u #define AY_REG_SHAPE 13u #define AY_MIXER_ALL_OFF 0xFFu #define AY_TONE_HIGH_MASK 0x0Fu #define AY_NOISE_MASK 0x1Fu #define AY_ENVELOPE 0x10u #define AY_VOLUME_MASK 0x0Fu #define AY_LEVEL_MAX 15u #define AY_ENVELOPE_STEPS 16 #define AY_ENVELOPE_CYCLE 256u #define AY_SHAPE_MASK 0x0Fu #define AY_SHAPE_CONTINUE 0x08u #define AY_SHAPE_ATTACK 0x04u #define AY_SHAPE_ALTERNATE 0x02u #define AY_SHAPE_HOLD 0x01u #define VGM_AY_FLAGS 0x79u #define AY_FLAG_HALF_CLOCK 0x10u // An envelope that repeats faster than this many rows is a timbre; its // frequency is a cycle's, or half that when it alternates (a triangle). #define AY_ENVELOPE_ROWS_MIN 2.0 #define AY_TRIANGLE_CYCLES 2u #define BYTE_BITS 8u typedef enum ChipE { CHIP_NONE = 0, CHIP_SN, CHIP_AY } ChipE; // One register write: when (VGM samples) and what (the SN's byte, or the // AY's register and value). typedef struct WriteT { uint64_t time; uint8_t reg; uint8_t value; } WriteT; typedef struct WriteListT { WriteT *writes; size_t count; size_t cap; } WriteListT; // A voice at the end of a row: silent, or a value (Hz, or the noise pitch) // at an attenuation. typedef struct VoiceT { bool on; uint32_t value; uint32_t atten; } VoiceT; typedef struct StateT { VoiceT voice[VOICES]; } StateT; // The chip being converted: its clock, and the SN76489's or the AY's // registers and what they have counted. typedef struct ModelT { ChipE chip; uint32_t clock; // SN76489 uint32_t shiftWidth; double toneDivider; double noiseDividers[SN_NOISE_RATES]; uint32_t periodZero; uint32_t period[TONE_VOICES]; uint32_t snAtten[VOICES]; uint32_t noise; uint32_t latchChannel; bool latchVolume; uint32_t periodicAsNoise; // AY-3-8910 uint8_t regs[AY_REGISTERS]; uint64_t envelopeStart; uint32_t buzzRows; } ModelT; // What the VGM holds for the chip. typedef struct VgmT { ModelT model; char name[64]; WriteListT writes; bool hasLoop; uint64_t loopTime; uint64_t endTime; uint32_t rate; uint32_t titleCount; char title[GD3_STRINGS][GD3_TEXT_MAX]; ToolBufT warnings; } VgmT; static void ayEnvelopeHz(const ModelT *m, double *hz); static bool ayEnvelopeLevel(const ModelT *m, double time, double rowSamples, uint32_t *level); static uint32_t ayLevel(const ModelT *m, uint32_t channel, double time, double rowSamples); static void ayVoices(ModelT *m, double time, double rowSamples, StateT *out); static void cellText(uint32_t voice, const VoiceT *v, char out[CELL_TEXT_MAX]); static int commandLength(uint32_t op); static void commandNeeds(size_t len, size_t at, size_t operands, const char *path); static uint32_t defaultTick(const WriteListT *writes, uint32_t rate); static int gapCompare(const void *a, const void *b); static void gd3Strings(const uint8_t *data, size_t len, size_t at, VgmT *vgm); static void modelReset(ModelT *m); static uint32_t noisePitch(double rate); static double pyFloorDiv(double a, double b); static void readVgm(const char *path, ChipE chip, VgmT *vgm); static void rowText(ToolBufT *out, char cells[VOICES][CELL_TEXT_MAX]); static size_t sampleRows(ModelT *m, const WriteListT *writes, uint32_t tickMs, uint64_t endTime, StateT **states); static double snToneHz(const ModelT *m, uint32_t channel); static void snVoices(ModelT *m, StateT *out); static void snWrite(ModelT *m, uint32_t value); static size_t songLines(const StateT *states, size_t count, bool hasLoop, size_t loopRow, ToolBufT *lines); static void textLine(ToolBufT *text, const char *fmt, ...) __attribute__((format(printf, 2, 3))); static void toneVoice(double hz, uint32_t atten, VoiceT *v); static uint32_t u32(const uint8_t *data, size_t len, size_t at); static void usage(const char *message); static bool voiceSame(const VoiceT *a, const VoiceT *b); static void writeAdd(WriteListT *list, uint64_t time, uint8_t reg, uint8_t value); // Operand bytes of every command but the waits, end and data blocks, by // range (VGM 1.71): first, last, length. static const uint8_t kCommandLengths[][3] = { { 0x30, 0x3F, 1 }, { 0x40, 0x4E, 2 }, { 0x4F, 0x50, 1 }, { 0x51, 0x5F, 2 }, { 0x64, 0x64, 3 }, { 0x68, 0x68, 11 }, { 0x70, 0x8F, 0 }, { 0x90, 0x91, 4 }, { 0x92, 0x92, 5 }, { 0x93, 0x93, 10 }, { 0x94, 0x94, 1 }, { 0x95, 0x95, 4 }, { 0xA0, 0xBF, 2 }, { 0xC0, 0xDF, 3 }, { 0xE0, 0xFF, 4 } }; // Writes to other chips: two-byte register writes (YM2413 through YM2612, // YMF262...) and the others' ranges. static const uint8_t kOtherChipRanges[][2] = { { 0x51, 0x5F }, { 0xA1, 0xBF }, { 0xC0, 0xDF }, { 0xE1, 0xFF } }; static const uint32_t kSnNoiseDividers[SN_NOISE_RATES] = { 512u, 1024u, 2048u }; #define COUNT_OF(a) (sizeof(a) / sizeof((a)[0])) // The frequency of a repeating envelope: a cycle a period, two when it // alternates. static void ayEnvelopeHz(const ModelT *m, double *hz) { uint32_t period; uint32_t cycles; period = (uint32_t)m->regs[AY_REG_ENVELOPE_LO] | ((uint32_t)m->regs[AY_REG_ENVELOPE_HI] << BYTE_BITS); if (period < 1u) { period = 1u; } cycles = (m->regs[AY_REG_SHAPE] & AY_SHAPE_ALTERNATE) != 0u ? AY_TRIANGLE_CYCLES : 1u; *hz = (double)m->clock / (double)((uint64_t)AY_ENVELOPE_CYCLE * period * cycles); } // The envelope's level (0..15) at time; false when it repeats too fast for // rows to follow. static bool ayEnvelopeLevel(const ModelT *m, double time, double rowSamples, uint32_t *level) { uint32_t period; uint32_t shape; double cycleSamples; int64_t step; int64_t cycle; uint32_t position; uint32_t end; bool attack; period = (uint32_t)m->regs[AY_REG_ENVELOPE_LO] | ((uint32_t)m->regs[AY_REG_ENVELOPE_HI] << BYTE_BITS); if (period < 1u) { period = 1u; } shape = m->regs[AY_REG_SHAPE] & AY_SHAPE_MASK; cycleSamples = (double)((uint64_t)AY_ENVELOPE_CYCLE * period * VGM_RATE) / (double)m->clock; if ((shape & AY_SHAPE_CONTINUE) != 0u && (shape & AY_SHAPE_HOLD) == 0u && cycleSamples < AY_ENVELOPE_ROWS_MIN * rowSamples) { return false; } step = (int64_t)((time - (double)m->envelopeStart) * AY_ENVELOPE_STEPS / cycleSamples); cycle = step / AY_ENVELOPE_STEPS; position = (uint32_t)(step % AY_ENVELOPE_STEPS); attack = (shape & AY_SHAPE_ATTACK) != 0u; if (cycle > 0) { if ((shape & AY_SHAPE_CONTINUE) == 0u) { *level = 0u; return true; } if ((shape & AY_SHAPE_HOLD) != 0u) { end = attack ? AY_LEVEL_MAX : 0u; *level = (shape & AY_SHAPE_ALTERNATE) != 0u ? AY_LEVEL_MAX - end : end; return true; } if ((shape & AY_SHAPE_ALTERNATE) != 0u && cycle % 2 != 0) { attack = !attack; } } *level = attack ? position : AY_LEVEL_MAX - position; return true; } // A channel's level: its volume, or the envelope's (full while the // envelope is too fast to follow). static uint32_t ayLevel(const ModelT *m, uint32_t channel, double time, double rowSamples) { uint32_t volume; uint32_t level; volume = m->regs[AY_REG_VOLUME + channel]; if ((volume & AY_ENVELOPE) == 0u) { return volume & AY_VOLUME_MASK; } return ayEnvelopeLevel(m, time, rowSamples, &level) ? level : AY_LEVEL_MAX; } // The AY's voices at time: each channel's tone (or the buzzer's), and the // noise at the loudest of the channels it is mixed into. static void ayVoices(ModelT *m, double time, double rowSamples, StateT *out) { uint32_t channel; uint32_t level; uint32_t period; uint32_t noiseLevel; uint32_t envelope; uint32_t mixer; bool anyEnvelope; bool buzzing; double hz; memset(out, 0, sizeof(*out)); mixer = m->regs[AY_REG_MIXER]; noiseLevel = 0u; anyEnvelope = false; for (channel = 0; channel < TONE_VOICES; channel++) { anyEnvelope = anyEnvelope || (m->regs[AY_REG_VOLUME + channel] & AY_ENVELOPE) != 0u; } if (anyEnvelope && !ayEnvelopeLevel(m, time, rowSamples, &envelope)) { m->buzzRows++; } for (channel = 0; channel < TONE_VOICES; channel++) { level = ayLevel(m, channel, time, rowSamples); period = (uint32_t)m->regs[channel * 2u] | (((uint32_t)m->regs[channel * 2u + 1u] & AY_TONE_HIGH_MASK) << BYTE_BITS); buzzing = (m->regs[AY_REG_VOLUME + channel] & AY_ENVELOPE) != 0u && !ayEnvelopeLevel(m, time, rowSamples, &envelope); if (level == 0u) { out->voice[channel].on = false; } else if (((mixer >> channel) & 1u) == 0u) { toneVoice((double)m->clock / (double)(AY_DIVIDER * (period > 1u ? period : 1u)), AY_LEVEL_MAX - level, &out->voice[channel]); } else if (buzzing) { ayEnvelopeHz(m, &hz); toneVoice(hz, 0u, &out->voice[channel]); } else { out->voice[channel].on = false; } if (((mixer >> (TONE_VOICES + channel)) & 1u) == 0u && level > noiseLevel) { noiseLevel = level; } } if (noiseLevel != 0u) { period = m->regs[AY_REG_NOISE] & AY_NOISE_MASK; out->voice[NOISE].on = true; out->voice[NOISE].value = noisePitch((double)m->clock / (double)(AY_DIVIDER * (period > 1u ? period : 1u))); out->voice[NOISE].atten = AY_LEVEL_MAX - noiseLevel; } } // A voice's .song cell: "off", "@Hz:atten", or "Npitch:atten". static void cellText(uint32_t voice, const VoiceT *v, char out[CELL_TEXT_MAX]) { if (!v->on) { snprintf(out, CELL_TEXT_MAX, "off"); } else if (voice < TONE_VOICES) { snprintf(out, CELL_TEXT_MAX, "@%u:%u", v->value, v->atten); } else { snprintf(out, CELL_TEXT_MAX, "N%u:%u", v->value, v->atten); } } // A command's operand bytes, or -1 when VGM 1.71 has no such command. static int commandLength(uint32_t op) { size_t i; for (i = 0; i < COUNT_OF(kCommandLengths); i++) { if (op >= kCommandLengths[i][0] && op <= kCommandLengths[i][1]) { return kCommandLengths[i][2]; } } return -1; } // A fatal error unless the command at at has operands operand bytes in. static void commandNeeds(size_t len, size_t at, size_t operands, const char *path) { if (at + operands >= len) { toolFail("%s ends inside a command at 0x%zX", path, at); } } // The update interval, rounded down to whole milliseconds: the header's // rate, or the shortest gap between writes that comes more than once when // that is shorter. static uint32_t defaultTick(const WriteListT *writes, uint32_t rate) { uint64_t *gaps; size_t gapCount; size_t i; double frame; double ms; gaps = (uint64_t *)malloc((writes->count + 1u) * sizeof(uint64_t)); gapCount = 0u; if (gaps == NULL) { toolFail("out of memory"); } for (i = 1; i < writes->count; i++) { if (writes->writes[i].time != writes->writes[i - 1u].time) { gaps[gapCount++] = writes->writes[i].time - writes->writes[i - 1u].time; } } // Write times only grow, so the gaps between the distinct ones are // these; sorted, the first that comes twice is the shortest repeated. frame = rate != 0u ? (double)VGM_RATE / (double)rate : (double)WAIT_NTSC; qsort(gaps, gapCount, sizeof(uint64_t), gapCompare); for (i = 1; i < gapCount; i++) { if (gaps[i] == gaps[i - 1u]) { if ((double)gaps[i] < frame) { frame = (double)gaps[i]; } break; } } free(gaps); ms = floor(frame * MS_PER_SECOND / VGM_RATE); return ms < 1.0 ? 1u : (uint32_t)ms; } static int gapCompare(const void *a, const void *b) { uint64_t x; uint64_t y; x = *(const uint64_t *)a; y = *(const uint64_t *)b; return (x > y) - (x < y); } // The GD3 tag's strings at at (UTF-16LE, each ended by a 0), as ASCII: a // character outside the printable ASCII range (a surrogate pair is one, a // broken one or a trailing odd byte one too) becomes '?'. Only the first // GD3_STRINGS are kept. static void gd3Strings(const uint8_t *data, size_t len, size_t at, VgmT *vgm) { size_t end; size_t i; size_t n; uint32_t unit; uint32_t next; char c; vgm->titleCount = 0u; if (at + VGM_IDENT_BYTES > len || memcmp(data + at, GD3_IDENT, VGM_IDENT_BYTES) != 0) { return; } end = at + GD3_TEXT_AT + u32(data, len, at + GD3_SIZE_AT); if (end > len) { end = len; } n = 0u; vgm->titleCount = 1u; vgm->title[0][0] = '\0'; for (i = at + GD3_TEXT_AT; i < end;) { if (i + 1u >= end) { c = '?'; i++; } else { unit = (uint32_t)data[i] | ((uint32_t)data[i + 1u] << BYTE_BITS); i += 2u; if (unit == 0u) { if (vgm->titleCount < GD3_STRINGS) { vgm->title[vgm->titleCount][0] = '\0'; } vgm->titleCount++; n = 0u; continue; } if (unit >= UTF16_SURROGATE_HI && unit < UTF16_SURROGATE_LO && i + 1u < end) { next = (uint32_t)data[i] | ((uint32_t)data[i + 1u] << BYTE_BITS); if (next >= UTF16_SURROGATE_LO && next < UTF16_SURROGATE_END) { i += 2u; } } c = (unit >= PRINTABLE_FIRST && unit < PRINTABLE_END) ? (char)unit : '?'; } if (vgm->titleCount <= GD3_STRINGS && n + 1u < GD3_TEXT_MAX) { vgm->title[vgm->titleCount - 1u][n++] = c; vgm->title[vgm->titleCount - 1u][n] = '\0'; } } } // The chip at power-on: every SN76489 channel silent, every AY tone and // noise off; nothing counted. static void modelReset(ModelT *m) { memset(m->period, 0, sizeof(m->period)); m->snAtten[0] = ATTEN_OFF; m->snAtten[1] = ATTEN_OFF; m->snAtten[2] = ATTEN_OFF; m->snAtten[NOISE] = ATTEN_OFF; m->noise = 0u; m->latchChannel = 0u; m->latchVolume = true; m->periodicAsNoise = 0u; memset(m->regs, 0, sizeof(m->regs)); m->regs[AY_REG_MIXER] = AY_MIXER_ALL_OFF; m->envelopeStart = 0u; m->buzzRows = 0u; } // The noise pitch (0..31) whose rate on the ST is nearest rate. static uint32_t noisePitch(double rate) { double pitch; pitch = nearbyint(YM_NOISE_HZ / rate); if (pitch > NOISE_PITCH_MAX) { pitch = NOISE_PITCH_MAX; } return pitch < 0.0 ? 0u : (uint32_t)pitch; } // a // b for doubles as Python computes it (floor of the exact quotient, // from the remainder), not floor(a / b), which can round up across an // integer. static double pyFloorDiv(double a, double b) { double mod; double div; double floorDiv; mod = fmod(a, b); div = (a - mod) / b; if (mod != 0.0 && ((b < 0.0) != (mod < 0.0))) { div -= 1.0; } if (div == 0.0) { return copysign(0.0, a / b); } floorDiv = floor(div); if (div - floorDiv > 0.5) { floorDiv += 1.0; } return floorDiv; } // The VGM at path: the chip's model and writes, the loop and end times, the // header's rate, the title strings, and warnings. A fatal error on anything // it cannot read. static void readVgm(const char *path, ChipE chip, VgmT *vgm) { uint8_t *raw; uint8_t *data; size_t rawLen; size_t len; size_t start; size_t at; size_t loopAt; uint32_t version; uint32_t snClock; uint32_t ayClock; uint32_t flags; uint32_t op; uint32_t secondChip; uint32_t otherChip; uint64_t time; int length; size_t i; bool hasLoopAt; WriteListT sn; WriteListT ay; if (!toolReadFile(path, &raw, &rawLen)) { toolFail("cannot read %s", path); } data = raw; len = rawLen; if (rawLen >= 2u && raw[0] == GZIP_MAGIC0 && raw[1] == GZIP_MAGIC1) { if (!toolInflate(raw, rawLen, TOOL_ZLIB_GZIP, &data, &len)) { toolFail("%s is not a valid gzip file", path); } free(raw); } if (len < VGM_HEADER_MIN || memcmp(data, VGM_IDENT, VGM_IDENT_BYTES) != 0) { toolFail("%s is not a VGM file", path); } version = u32(data, len, VGM_VERSION_FIELD); start = VGM_HEADER_MIN; if (version >= VGM_DATA_OFFSET_V150 && u32(data, len, VGM_DATA_OFFSET_FIELD) != 0u) { start = VGM_DATA_OFFSET_FIELD + u32(data, len, VGM_DATA_OFFSET_FIELD); } snClock = u32(data, len, VGM_SN_CLOCK_FIELD); ayClock = (version >= VGM_AY_CLOCK_V151 && start > VGM_AY_CLOCK_FIELD) ? u32(data, len, VGM_AY_CLOCK_FIELD) : 0u; hasLoopAt = u32(data, len, VGM_LOOP_FIELD) != 0u; loopAt = VGM_LOOP_FIELD + u32(data, len, VGM_LOOP_FIELD); vgm->rate = u32(data, len, VGM_RATE_FIELD); vgm->titleCount = 0u; if (u32(data, len, VGM_GD3_FIELD) != 0u) { gd3Strings(data, len, VGM_GD3_FIELD + u32(data, len, VGM_GD3_FIELD), vgm); } memset(&sn, 0, sizeof(sn)); memset(&ay, 0, sizeof(ay)); secondChip = 0u; otherChip = 0u; time = 0u; vgm->hasLoop = false; for (at = start; at < len;) { if (hasLoopAt && at == loopAt) { vgm->hasLoop = true; vgm->loopTime = time; } op = data[at]; if (op == CMD_END) { break; } if (op == CMD_WAIT) { commandNeeds(len, at, 2u, path); time += (uint32_t)data[at + 1u] | ((uint32_t)data[at + 2u] << BYTE_BITS); at += 3u; } else if (op == CMD_WAIT_NTSC) { time += WAIT_NTSC; at += 1u; } else if (op == CMD_WAIT_PAL) { time += WAIT_PAL; at += 1u; } else if (op >= SHORT_WAIT_FIRST && op <= SHORT_WAIT_LAST) { time += op - SHORT_WAIT_FIRST + 1u; at += 1u; } else if (op >= DAC_WAIT_FIRST && op <= DAC_WAIT_LAST) { time += op - DAC_WAIT_FIRST; at += 1u; } else if (op == CMD_DATA_BLOCK) { at += DATA_BLOCK_HEADER + u32(data, len, at + DATA_BLOCK_SIZE_AT); } else if (op == CMD_SN) { commandNeeds(len, at, 1u, path); writeAdd(&sn, time, 0u, data[at + 1u]); at += 2u; } else if (op == CMD_SN2) { secondChip++; at += 2u; } else if (op == CMD_AY) { commandNeeds(len, at, 1u, path); if ((data[at + 1u] & AY_SECOND_CHIP) != 0u) { secondChip++; } else { commandNeeds(len, at, 2u, path); writeAdd(&ay, time, data[at + 1u], data[at + 2u]); } at += 3u; } else { length = commandLength(op); if (length < 0) { toolFail("unknown VGM command 0x%02X at 0x%zX", op, at); } for (i = 0; i < COUNT_OF(kOtherChipRanges); i++) { if (op >= kOtherChipRanges[i][0] && op <= kOtherChipRanges[i][1]) { otherChip++; break; } } at += 1u + (size_t)length; } } vgm->endTime = time; if (secondChip != 0u) { textLine(&vgm->warnings, "%u writes to a second chip left out", secondChip); } if (otherChip != 0u) { textLine(&vgm->warnings, "%u writes to other chips (FM, PCM) left out: their parts do not play", otherChip); } if (chip == CHIP_NONE) { if (sn.count > 0u && ay.count > 0u) { toolFail("the VGM drives sn and ay; pick one with --chip"); } if (sn.count == 0u && ay.count == 0u) { toolFail("the VGM drives neither the SN76489 nor the AY-3-8910; pick one with --chip"); } chip = sn.count > 0u ? CHIP_SN : CHIP_AY; } if ((chip == CHIP_SN ? sn.count : ay.count) == 0u) { toolFail("the VGM has no %s writes", chip == CHIP_SN ? "SN76489" : "AY-3-8910"); } memset(&vgm->model, 0, sizeof(vgm->model)); vgm->model.chip = chip; if (chip == CHIP_SN) { vgm->writes = sn; free(ay.writes); flags = data[VGM_SN_FLAGS]; vgm->model.clock = snClock & VGM_CLOCK_MASK; vgm->model.shiftWidth = data[VGM_SN_SHIFT_WIDTH] != 0u ? data[VGM_SN_SHIFT_WIDTH] : SN_SHIFT_WIDTH_DEFAULT; vgm->model.toneDivider = SN_TONE_DIVIDER; vgm->model.periodZero = (flags & SN_FLAG_ZERO_IS_400) != 0u ? SN_PERIOD_ZERO : 1u; for (i = 0; i < SN_NOISE_RATES; i++) { vgm->model.noiseDividers[i] = (double)kSnNoiseDividers[i]; } if ((flags & SN_FLAG_NO_PRESCALER) != 0u || vgm->model.clock < SN_DIRECT_CLOCK_MAX) { vgm->model.toneDivider = SN_TONE_DIVIDER / SN_PRESCALER; for (i = 0; i < SN_NOISE_RATES; i++) { vgm->model.noiseDividers[i] = (double)kSnNoiseDividers[i] / SN_PRESCALER; } } snprintf(vgm->name, sizeof(vgm->name), "TI SN76489 at %u Hz", vgm->model.clock); } else { vgm->writes = ay; free(sn.writes); vgm->model.clock = ayClock & VGM_CLOCK_MASK; if (len > VGM_AY_FLAGS && (data[VGM_AY_FLAGS] & AY_FLAG_HALF_CLOCK) != 0u) { vgm->model.clock /= 2u; } snprintf(vgm->name, sizeof(vgm->name), "AY-3-8910/YM2149 at %u Hz", vgm->model.clock); } if (vgm->model.clock == 0u) { toolFail("the VGM gives the chip no clock"); } free(data); } // A .song row: the cells padded to CELL_WIDTH and two spaces apart, with // no space at the end. static void rowText(ToolBufT *out, char cells[VOICES][CELL_TEXT_MAX]) { char line[VOICES * (CELL_WIDTH + 2) + 1]; int n; uint32_t v; n = 0; for (v = 0; v < VOICES; v++) { n += snprintf(line + n, sizeof(line) - (size_t)n, "%s%-*s", v == 0u ? "" : " ", CELL_WIDTH, cells[v]); } while (n > 0 && line[n - 1] == ' ') { n--; } toolBufAdd(out, line, (size_t)n); toolBufByte(out, '\n'); } // The chip's voices at the end of every row, the first to the last the // song lasts; how many rows. static size_t sampleRows(ModelT *m, const WriteListT *writes, uint32_t tickMs, uint64_t endTime, StateT **states) { double rowSamples; double rowEnd; double rows; size_t count; size_t row; size_t at; StateT *out; rowSamples = (double)(tickMs * VGM_RATE) / MS_PER_SECOND; rows = ceil((double)endTime / rowSamples); count = rows < 1.0 ? 1u : (size_t)rows; out = (StateT *)malloc(count * sizeof(StateT)); if (out == NULL) { toolFail("out of memory"); } at = 0u; for (row = 0; row < count; row++) { rowEnd = (double)(row + 1u) * rowSamples; while (at < writes->count && (double)writes->writes[at].time < rowEnd) { if (m->chip == CHIP_SN) { snWrite(m, writes->writes[at].value); } else { m->regs[writes->writes[at].reg & AY_REG_MASK] = writes->writes[at].value; if ((writes->writes[at].reg & AY_REG_MASK) == AY_REG_SHAPE) { m->envelopeStart = writes->writes[at].time; } } at++; } if (m->chip == CHIP_SN) { snVoices(m, &out[row]); } else { ayVoices(m, rowEnd, rowSamples, &out[row]); } } *states = out; return count; } // A tone channel's frequency: a period of 0 is periodZero. static double snToneHz(const ModelT *m, uint32_t channel) { uint32_t period; period = m->period[channel] != 0u ? m->period[channel] : m->periodZero; return (double)m->clock / (m->toneDivider * (double)period); } // The SN76489's voices: its three tones, and its noise, or its periodic // noise as a pulse on a free tone voice (tone 2's first when tone 2 is its // clock). static void snVoices(ModelT *m, StateT *out) { static const uint32_t tone2First[TONE_VOICES] = { 2u, 0u, 1u }; static const uint32_t inOrder[TONE_VOICES] = { 0u, 1u, 2u }; const uint32_t *order; uint32_t channel; uint32_t rateSel; double rate; memset(out, 0, sizeof(*out)); for (channel = 0; channel < TONE_VOICES; channel++) { toneVoice(snToneHz(m, channel), m->snAtten[channel], &out->voice[channel]); } if (m->snAtten[NOISE] >= ATTEN_OFF) { return; } rateSel = m->noise & SN_NOISE_RATE_MASK; rate = rateSel == SN_NOISE_TONE2 ? snToneHz(m, 2u) : (double)m->clock / m->noiseDividers[rateSel]; if ((m->noise & SN_NOISE_WHITE) == 0u) { order = rateSel == SN_NOISE_TONE2 ? tone2First : inOrder; for (channel = 0; channel < TONE_VOICES; channel++) { if (!out->voice[order[channel]].on) { toneVoice(rate / (double)m->shiftWidth, m->snAtten[NOISE], &out->voice[order[channel]]); return; } } m->periodicAsNoise++; } out->voice[NOISE].on = true; out->voice[NOISE].value = noisePitch(rate); out->voice[NOISE].atten = m->snAtten[NOISE]; } // One byte to the SN76489: a latch (channel, volume or tone, the low bits) // or data for the latched register. static void snWrite(ModelT *m, uint32_t value) { uint32_t channel; uint32_t low; if ((value & SN_LATCH) != 0u) { channel = (value >> SN_CHANNEL_SHIFT) & SN_CHANNEL_MASK; m->latchChannel = channel; m->latchVolume = (value & SN_VOLUME) != 0u; low = value & SN_LOW_MASK; if (m->latchVolume) { m->snAtten[channel] = low; } else if (channel < TONE_VOICES) { m->period[channel] = (m->period[channel] & SN_PERIOD_HIGH) | low; } else { m->noise = low & SN_NOISE_MASK; } return; } channel = m->latchChannel; if (m->latchVolume) { m->snAtten[channel] = value & SN_LOW_MASK; } else if (channel < TONE_VOICES) { m->period[channel] = (m->period[channel] & SN_LOW_MASK) | ((value & SN_HIGH_MASK) << SN_HIGH_SHIFT); } else { m->noise = value & SN_NOISE_MASK; } } // The .song rows: each voice that changed, every voice on the loop row; // runs of empty rows as repeat blocks. Returns the JYM1 bytes they bake to // (about: a wait byte at most every row that has events, plus the long // runs). static size_t songLines(const StateT *states, size_t count, bool hasLoop, size_t loopRow, ToolBufT *lines) { static const VoiceT none = { false, 0u, 0u }; char cells[VOICES][CELL_TEXT_MAX]; char empty[VOICES][CELL_TEXT_MAX]; const VoiceT *last[VOICES]; size_t events; size_t emptyRows; size_t waitRows; size_t run; size_t row; size_t e; uint32_t v; bool changed; bool isLoop; for (v = 0; v < VOICES; v++) { last[v] = &none; snprintf(empty[v], CELL_TEXT_MAX, "---"); } lines->len = 0u; events = 0u; emptyRows = 0u; waitRows = 0u; for (row = 0; row <= count; row++) { isLoop = hasLoop && row == loopRow; changed = false; if (row < count) { for (v = 0; v < VOICES; v++) { if (isLoop || !voiceSame(&states[row].voice[v], last[v])) { cellText(v, &states[row].voice[v], cells[v]); events += states[row].voice[v].on ? JYM_NOTE_BYTES : 1u; changed = true; } else { snprintf(cells[v], CELL_TEXT_MAX, "---"); } last[v] = &states[row].voice[v]; } } // The empty rows owed, before a loop, a changed row, or the end. if (isLoop || changed || row == count) { while (emptyRows > 0u) { run = emptyRows < REPEAT_MAX ? emptyRows : REPEAT_MAX; if (run >= REPEAT_MIN) { textLine(lines, "repeat %zu", run); rowText(lines, empty); toolBufAdd(lines, "endrepeat\n", strlen("endrepeat\n")); } else { for (e = 0; e < run; e++) { rowText(lines, empty); } } emptyRows -= run; } } if (row == count) { break; } if (isLoop) { toolBufAdd(lines, "loop\n", strlen("loop\n")); } if (changed) { rowText(lines, cells); waitRows++; } else { emptyRows++; } } return JYM_HEADER_BYTES + events + waitRows + count / JYM_WAIT_MAX + 1u; } // A line of formatted text added to text (the warnings, the song), a // newline after it. static void textLine(ToolBufT *text, const char *fmt, ...) { char line[256]; va_list args; int n; va_start(args, fmt); n = vsnprintf(line, sizeof(line), fmt, args); va_end(args); if (n < 0) { n = 0; } else if ((size_t)n >= sizeof(line)) { n = (int)sizeof(line) - 1; } toolBufAdd(text, line, (size_t)n); toolBufByte(text, '\n'); } // A tone voice at hz and atten, or silent when it is off or too high to // hear. static void toneVoice(double hz, uint32_t atten, VoiceT *v) { double rounded; if (atten >= ATTEN_OFF || hz > MAX_AUDIBLE_HZ) { v->on = false; return; } rounded = nearbyint(hz); if (rounded > FREQ_MAX) { rounded = FREQ_MAX; } v->on = true; v->value = rounded < 1.0 ? 1u : (uint32_t)rounded; v->atten = atten < ATTEN_MAX ? atten : ATTEN_MAX; } // A little-endian 32-bit field, 0 past the end. static uint32_t u32(const uint8_t *data, size_t len, size_t at) { if (at + 4u > len) { return 0u; } return (uint32_t)data[at] | ((uint32_t)data[at + 1u] << 8) | ((uint32_t)data[at + 2u] << 16) | ((uint32_t)data[at + 3u] << 24); } static void usage(const char *message) { fprintf(stderr, "usage: %s [--chip {sn,ay}] [--tick MS] IN.vgm OUT.song\n", TOOL_NAME); fprintf(stderr, "%s: error: %s\n", TOOL_NAME, message); exit(USAGE_EXIT); } static bool voiceSame(const VoiceT *a, const VoiceT *b) { if (a->on != b->on) { return false; } return !a->on || (a->value == b->value && a->atten == b->atten); } static void writeAdd(WriteListT *list, uint64_t time, uint8_t reg, uint8_t value) { WriteT *writes; size_t cap; if (list->count == list->cap) { cap = list->cap == 0u ? 256u : list->cap * 2u; writes = (WriteT *)realloc(list->writes, cap * sizeof(WriteT)); if (writes == NULL) { toolFail("out of memory"); } list->writes = writes; list->cap = cap; } list->writes[list->count].time = time; list->writes[list->count].reg = reg; list->writes[list->count].value = value; list->count++; } int main(int argc, char **argv) { const char *paths[2]; const char *base; const char *value; ChipE chip; VgmT vgm; StateT *states; ToolBufT lines; ToolBufT out; size_t count; size_t size; size_t loopRow; size_t pathCount; size_t at; long tick; uint32_t baseTick; uint32_t tickMs; double rowSamples; bool hasLoopRow; int a; char line[GD3_TEXT_MAX + 32]; toolFileName(TOOL_NAME); chip = CHIP_NONE; tick = 0; pathCount = 0u; for (a = 1; a < argc; a++) { value = NULL; if (strcmp(argv[a], "--chip") == 0 || strcmp(argv[a], "--tick") == 0) { if (a + 1 >= argc) { usage(strcmp(argv[a], "--chip") == 0 ? "argument --chip: expected one argument" : "argument --tick: expected one argument"); } value = argv[a + 1]; } else if (strncmp(argv[a], "--chip=", strlen("--chip=")) == 0 || strncmp(argv[a], "--tick=", strlen("--tick=")) == 0) { value = strchr(argv[a], '=') + 1; } if (value != NULL) { if (strncmp(argv[a], "--chip", strlen("--chip")) == 0) { if (strcmp(value, "sn") == 0) { chip = CHIP_SN; } else if (strcmp(value, "ay") == 0) { chip = CHIP_AY; } else { usage("argument --chip: invalid choice"); } } else if (!toolParseInt(value, strlen(value), &tick)) { usage("argument --tick: invalid int value"); } if (strchr(argv[a], '=') == NULL) { a++; } continue; } if (argv[a][0] == '-' && argv[a][1] != '\0') { usage("unrecognized arguments"); } if (pathCount == 2u) { usage("unrecognized arguments"); } paths[pathCount++] = argv[a]; } if (pathCount < 2u) { usage("the following arguments are required: vgm, song"); } memset(&vgm, 0, sizeof(vgm)); readVgm(paths[0], chip, &vgm); if (tick == 0) { baseTick = defaultTick(&vgm.writes, vgm.rate); } else if (tick < 1 || tick > TICK_MS_MAX) { toolFail("--tick is 1..%d ms", TICK_MS_MAX); } else { baseTick = (uint32_t)tick; } if (baseTick > TICK_MS_MAX) { toolFail("--tick is 1..%d ms", TICK_MS_MAX); } memset(&lines, 0, sizeof(lines)); tickMs = baseTick; for (;;) { modelReset(&vgm.model); count = sampleRows(&vgm.model, &vgm.writes, tickMs, vgm.endTime, &states); rowSamples = (double)(tickMs * VGM_RATE) / MS_PER_SECOND; hasLoopRow = vgm.hasLoop && vgm.loopTime < vgm.endTime; loopRow = hasLoopRow ? (size_t)pyFloorDiv((double)vgm.loopTime, rowSamples) : 0u; size = songLines(states, count, hasLoopRow, loopRow, &lines); if (tick != 0 || size < JYM_LIMIT || tickMs + baseTick > TICK_MS_MAX) { break; } free(states); tickMs += baseTick; } if (tickMs != baseTick) { textLine(&vgm.warnings, "%u ms rows would bake past JYM1's 64 KB; using %u ms rows", baseTick, tickMs); } if (vgm.model.chip == CHIP_SN && vgm.model.periodicAsNoise != 0u) { textLine(&vgm.warnings, "%u rows have periodic noise with all three tone voices busy, played as noise", vgm.model.periodicAsNoise); } if (vgm.model.chip == CHIP_AY && vgm.model.buzzRows != 0u) { textLine(&vgm.warnings, "%u rows have an envelope repeating faster than two rows (a timbre), played at full volume", vgm.model.buzzRows); } base = strrchr(paths[0], '/'); base = base != NULL ? base + 1 : paths[0]; memset(&out, 0, sizeof(out)); textLine(&out, "; Converted by tools/vgm2song from %s", base); if (hasLoopRow) { textLine(&out, "; %s, %zu rows of %u ms, looping at row %zu", vgm.name, count, tickMs, loopRow); } else { textLine(&out, "; %s, %zu rows of %u ms", vgm.name, count, tickMs); } if (vgm.titleCount > GD3_AUTHOR && vgm.title[GD3_TITLE][0] != '\0') { snprintf(line, sizeof(line), "; title: %s\n", vgm.title[GD3_TITLE]); toolBufAdd(&out, line, strlen(line)); } if (vgm.titleCount > GD3_AUTHOR && vgm.title[GD3_AUTHOR][0] != '\0') { snprintf(line, sizeof(line), "; author: %s\n", vgm.title[GD3_AUTHOR]); toolBufAdd(&out, line, strlen(line)); } textLine(&out, "\ntickms %u", tickMs); toolBufAdd(&out, lines.data, lines.len); if (!toolWriteFile(paths[1], out.data, out.len)) { toolFail("cannot write %s", paths[1]); } for (at = 0; at < vgm.warnings.len;) { fprintf(stderr, "%s: warning: ", TOOL_NAME); while (at < vgm.warnings.len && vgm.warnings.data[at] != '\n') { fputc(vgm.warnings.data[at++], stderr); } fputc('\n', stderr); at++; } if (size >= JYM_LIMIT) { fprintf(stderr, "%s: warning: this bakes to about %zu bytes, over JYM1's 64 KB; try a longer --tick\n", TOOL_NAME, size); } free(states); toolBufFree(&out); toolBufFree(&lines); toolBufFree(&vgm.warnings); free(vgm.writes.writes); return 0; }