joeylib2/tools/vgm2song/vgm2song.c

1236 lines
44 KiB
C

// 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 <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}