1236 lines
44 KiB
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;
|
|
}
|