1183 lines
42 KiB
C
1183 lines
42 KiB
C
// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
// See LICENSE.md for license information.
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// vgmCheck.c - checks of tools/vgm2song, for scripts/check-vgm.sh.
|
|
//
|
|
// vgmCheck synthetic REPO WORK
|
|
// VGM files written to WORK, with register writes whose results are
|
|
// worked out by hand, go through vgm2song and songbake (JOEY_BUILD's
|
|
// tools/, else REPO's build/tools/); the baked JYM1 is decoded and
|
|
// every row's voices must be the ones expected. Covers the SN76489
|
|
// (tones, white noise, periodic noise on a free tone voice, the
|
|
// 447 kHz no-prescaler clock), the AY-3-8910 (tones, mixed noise, an
|
|
// envelope decay followed row by row, a buzz sounding at the
|
|
// envelope's frequency), every wait command, a loop point, skipped
|
|
// commands, .vgz, --tick, the fit to 64 KB, and the warnings and
|
|
// refusals. Prints each failure, then "SYNTHETIC passed/checked";
|
|
// exit 1 on a failure.
|
|
//
|
|
// vgmCheck pitch WAV JYM
|
|
// Furnace's own audio of a song (WAV) against the JYM1 baked from
|
|
// its VGM: the share of WINDOW-sample windows, loud enough and not
|
|
// dominated by noise, whose strongest pitch is a tone the JYM1 has
|
|
// sounding at the window's middle (within PITCH_CENTS, or an octave
|
|
// or the third harmonic of it). Prints "PITCH matched/tested".
|
|
//
|
|
// This replaces tests/host/vgmCheck.py and measures the pitch windows as it
|
|
// did, in numpy's order (gateFft), so the counts are the ones it gave.
|
|
|
|
#define _DEFAULT_SOURCE
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <math.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <fcntl.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "gateArg.h"
|
|
#include "gateFft.h"
|
|
#include "gateText.h"
|
|
#include "gateWav.h"
|
|
#include "toolBuf.h"
|
|
#include "toolFile.h"
|
|
#include "toolZlib.h"
|
|
|
|
#define TOOL_NAME "vgmCheck"
|
|
#define FAIL_EXIT 1
|
|
#define MODE_ARGS 4
|
|
#define MODE_ARG 1
|
|
#define FIRST_ARG 2
|
|
#define WHAT_MAX 64u
|
|
#define TOOL_ARGS_MAX 8u
|
|
#define CHILD_EXEC_FAILED 127
|
|
#define READ_CHUNK 4096u
|
|
#define ROWS_FIRST_CAP 64u
|
|
|
|
#define VGM_RATE 44100
|
|
#define FRAME 735
|
|
#define WINDOW 4096u
|
|
#define HOP 2048u
|
|
#define PITCH_CENTS 50.0
|
|
#define PITCH_BAND_LOW 50.0
|
|
#define PITCH_BAND_HIGH 5000.0
|
|
#define PITCH_LOUD_SHARE 0.1
|
|
#define CENTS_PER_OCTAVE 1200.0
|
|
#define MS_PER_SECOND 1000.0
|
|
|
|
// A JYM1 stream: "JYM1", the tick and the loop's event offset (16-bit
|
|
// little-endian), then the events.
|
|
#define JYM_MAGIC "JYM1"
|
|
#define JYM_MAGIC_BYTES 4u
|
|
#define JYM_TICK_AT 4u
|
|
#define JYM_LOOP_AT 6u
|
|
#define JYM_HEADER 8u
|
|
#define OP_END 0x00u
|
|
#define OP_NOTE 0x10u
|
|
#define OP_OFF 0x20u
|
|
#define OP_TEMPO 0x40u
|
|
#define OP_WAIT 0x80u
|
|
#define OP_WAIT_MASK 0x7Fu
|
|
#define NOTE_BYTES 4u
|
|
#define NOTE_VALUE_LO 1u
|
|
#define NOTE_VALUE_HI 2u
|
|
#define NOTE_ATTEN 3u
|
|
#define TEMPO_BYTES 3u
|
|
#define VOICES 4u
|
|
#define TONE_VOICES 3u
|
|
#define NOISE_VOICE 3u
|
|
#define BYTE_BITS 8u
|
|
#define BYTE_MASK 0xFFu
|
|
|
|
// A VGM 1.71 file as Furnace writes one: a 0x100-byte header, then the
|
|
// commands and the end command.
|
|
#define VGM_IDENT "Vgm "
|
|
#define VGM_IDENT_BYTES 4u
|
|
#define VGM_HEADER 0x100u
|
|
#define VGM_EOF_AT 0x04u
|
|
#define VGM_VERSION_AT 0x08u
|
|
#define VGM_VERSION 0x171u
|
|
#define VGM_SN_CLOCK_AT 0x0Cu
|
|
#define VGM_LOOP_AT 0x1Cu
|
|
#define VGM_RATE_AT 0x24u
|
|
#define VGM_SN_FEEDBACK_AT 0x28u
|
|
#define VGM_SN_FEEDBACK 0x0009u
|
|
#define VGM_SN_WIDTH_AT 0x2Au
|
|
#define VGM_DATA_AT 0x34u
|
|
#define VGM_AY_CLOCK_AT 0x74u
|
|
#define VGM_FIELD_BYTES 4u
|
|
#define VGM_SN_WIDTH 16u
|
|
#define VGM_NO_LOOP (-1L)
|
|
#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 CMD_SHORT_WAIT 0x70u
|
|
#define CMD_GG_STEREO 0x4Fu
|
|
#define CMD_YM2413 0x51u
|
|
#define CMD_UNKNOWN 0x01u
|
|
#define SN_LATCH 0x80u
|
|
#define SN_VOLUME 0x90u
|
|
#define SN_CHANNEL_SHIFT 5u
|
|
#define SN_LOW_MASK 0x0Fu
|
|
#define SN_HIGH_MASK 0x3Fu
|
|
#define SN_HIGH_SHIFT 4u
|
|
|
|
// The chips' clocks and update rates the cases use.
|
|
#define SN_CLOCK 3579545u
|
|
#define TI_CLOCK 447443u
|
|
#define AY_CLOCK 2000000u
|
|
#define NTSC_UPDATE_HZ 60u
|
|
#define PAL_UPDATE_HZ 50u
|
|
|
|
// snCase: tone 0 period 254 (3579545 / (32 * 254) = 440.4 Hz) at
|
|
// attenuation 2 and white noise at clock / 1024 (3495.6 Hz: pitch 35.8,
|
|
// held at 31) at 5; frame 3 tone 1 period 127 (880.8 Hz) at 0, tone 0 off;
|
|
// frame 6 (the loop) periodic noise clocked by tone 2 (period 100:
|
|
// 1118.6 Hz, attenuation 15, so tone voice 2 is free): a 1118.6 / 16 =
|
|
// 69.9 Hz pulse on tone voice 2 at the noise's 4; frame 9 a Game Gear
|
|
// stereo write and a data block (skipped); the song ends at frame 12.
|
|
#define SN_TONE0_PERIOD 254u
|
|
#define SN_TONE0_ATTEN 2u
|
|
#define SN_NOISE_WHITE_1024 0xE5u
|
|
#define SN_NOISE_ATTEN_5 0xF5u
|
|
#define SN_TONE1_PERIOD 127u
|
|
#define SN_TONE1_ATTEN 0u
|
|
#define SN_TONE0_SILENT 0x9Fu
|
|
#define SN_TONE2_PERIOD 100u
|
|
#define SN_TONE2_ATTEN 15u
|
|
#define SN_NOISE_PERIODIC_T2 0xE3u
|
|
#define SN_NOISE_ATTEN_4 0xF4u
|
|
#define SN_SHORT_WAIT_15 14u
|
|
#define SN_DAC_WAIT 16
|
|
#define SN_SHORT_WAIT_SAMPLES 15
|
|
#define SN_DAC_WAIT_16 0x7Fu
|
|
#define GG_STEREO_ALL 0xFFu
|
|
// A data block: 0x67, its 0x66 mark, its type (YM2612 PCM), its size, and
|
|
// its bytes.
|
|
#define DATA_BLOCK_MARK 0x66u
|
|
#define DATA_BLOCK_TYPE 0x00u
|
|
#define DATA_BLOCK_BYTES 4u
|
|
#define SN_LOOP_FRAME 6
|
|
#define SN_FRAMES 12
|
|
#define SN_OPENING_FRAMES 3u
|
|
#define SN_CLOSING_FRAMES 3u
|
|
#define SN_TICK_MS 16u
|
|
#define SN_SLOW_TICK "33"
|
|
#define SN_SLOW_TICK_MS 33u
|
|
#define SN_BEFORE_FRAME 3
|
|
#define TI_TONE_ATTEN 0u
|
|
|
|
// ayCase: frame 0 tone A period 284 (2000000 / (16 * 284) = 440.1 Hz) at
|
|
// volume 13 (attenuation 2); channel B noise only (mixer 0x2A, then 0x2E),
|
|
// noise period 10 (pitch 10) at volume 15 (attenuation 0); channel C on
|
|
// the envelope, shape 0 (one decay, then silence), period 2000 (a 256 ms
|
|
// cycle: one level every 16 ms), tone C period 1600 / 16 = 100 (1250 Hz).
|
|
// Frame 10: tone C off, the envelope repeating (shape 8) at period 10
|
|
// (2000000 / 2560 = 781.25 Hz): a buzz, on tone voice 2 at its frequency.
|
|
#define AY_TONE_A_LO 0u
|
|
#define AY_TONE_A_HI 1u
|
|
#define AY_TONE_C_LO 4u
|
|
#define AY_TONE_C_HI 5u
|
|
#define AY_NOISE 6u
|
|
#define AY_MIXER 7u
|
|
#define AY_VOLUME_A 8u
|
|
#define AY_VOLUME_B 9u
|
|
#define AY_VOLUME_C 10u
|
|
#define AY_ENVELOPE_LO 11u
|
|
#define AY_ENVELOPE_HI 12u
|
|
#define AY_SHAPE 13u
|
|
#define AY_TONE_A_PERIOD 284u
|
|
#define AY_TONE_C_PERIOD 100u
|
|
#define AY_NOISE_PERIOD 10u
|
|
#define AY_MIXER_START 0x2Au
|
|
#define AY_MIXER_BUZZ 0x2Eu
|
|
#define AY_VOLUME_A_13 13u
|
|
#define AY_VOLUME_B_15 15u
|
|
#define AY_VOLUME_C_ENVELOPE 0x10u
|
|
#define AY_DECAY_PERIOD 2000u
|
|
#define AY_SHAPE_DECAY 0x00u
|
|
#define AY_BUZZ_PERIOD 10u
|
|
#define AY_SHAPE_REPEAT 0x08u
|
|
#define AY_DECAY_FRAMES 10u
|
|
#define AY_BUZZ_FRAMES 4u
|
|
#define AY_TICK_MS 20u
|
|
#define AY_DECAY_ROWS 10
|
|
#define AY_BUZZ_ROW 11
|
|
#define AY_LEVEL_MAX 15L
|
|
#define AY_STEP_MS 16.0
|
|
#define AY_TONE_A_HZ 440u
|
|
#define AY_TONE_A_ATTEN 2u
|
|
#define AY_TONE_C_HZ 1250u
|
|
#define AY_BUZZ_HZ 781u
|
|
#define AY_NOISE_PITCH 10u
|
|
|
|
// fitCase: a note every 60 Hz frame for FIT_FRAMES frames, about 100 KB at
|
|
// 16 ms rows, so the converter goes to 32 ms rows, which fit.
|
|
#define FIT_FRAMES 25000u
|
|
#define FIT_PERIOD_BASE 200u
|
|
#define FIT_PERIODS 50u
|
|
#define FIT_TICK_MS 32
|
|
|
|
// refusalCases: an AY volume write in an SN file, a YM2413 write and a
|
|
// second SN chip's.
|
|
#define REFUSE_AY_VOLUME 15u
|
|
#define YM2413_REG 0x10u
|
|
#define YM2413_VALUE 0x20u
|
|
#define SN2_VALUE 0x40u
|
|
|
|
// gzip (RFC 1952) around the deflate stream toolDeflateZlib makes, its
|
|
// zlib header and Adler-32 dropped.
|
|
#define ZLIB_HEADER 2u
|
|
#define ZLIB_TRAILER 4u
|
|
#define GZIP_ID1 0x1Fu
|
|
#define GZIP_ID2 0x8Bu
|
|
#define GZIP_DEFLATE 8u
|
|
#define GZIP_OS_UNKNOWN 0xFFu
|
|
#define GZIP_PLAIN_BYTES 6u
|
|
|
|
// A voice after a row: silent (None), 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 RowT {
|
|
VoiceT voice[VOICES];
|
|
} RowT;
|
|
|
|
// A JYM1 decoded: its tick, its loop row (or none), and the voices after
|
|
// each row; ok is false for a bake that failed.
|
|
typedef struct SongT {
|
|
bool ok;
|
|
uint32_t tick;
|
|
bool hasLoop;
|
|
size_t loopRow;
|
|
RowT *rows;
|
|
size_t rowCount;
|
|
size_t rowCap;
|
|
} SongT;
|
|
|
|
// The checks made: how many, and each failure's line (each ending in a
|
|
// zero byte).
|
|
typedef struct ChecksT {
|
|
ToolBufT failures;
|
|
size_t failureCount;
|
|
size_t count;
|
|
} ChecksT;
|
|
|
|
// A frame's expected voices.
|
|
typedef struct FrameWantT {
|
|
long frame;
|
|
RowT row;
|
|
} FrameWantT;
|
|
|
|
static void ay(ToolBufT *commands, uint32_t reg, uint32_t value);
|
|
static void ayCase(const char *repo, const char *work, ChecksT *checks);
|
|
static void bake(const char *repo, const char *work, const char *name, const ToolBufT *data, const char *extra, SongT *song, ToolBufT *err);
|
|
static void check(ChecksT *checks, bool ok, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
|
|
static void expectFlag(ChecksT *checks, const char *what, bool got);
|
|
static void expectNumber(ChecksT *checks, const char *what, bool have, long got, long want);
|
|
static void expectRow(ChecksT *checks, const char *what, const SongT *song, long row, const RowT *want);
|
|
static void expectVoice(ChecksT *checks, const char *what, const SongT *song, long row, uint32_t voice, VoiceT want);
|
|
static void fitCase(const char *repo, const char *work, ChecksT *checks);
|
|
static void gzipOf(const ToolBufT *data, ToolBufT *out);
|
|
static void jymRows(const char *path, SongT *song);
|
|
static char *pathOf(const char *dir, const char *name, const char *suffix);
|
|
static int pitch(const char *wavPath, const char *jymPath);
|
|
static void putLe32(uint8_t *at, uint32_t value);
|
|
static void refusalCases(const char *repo, const char *work, ChecksT *checks);
|
|
static long rowOf(long sample, uint32_t tickMs);
|
|
static bool rowSame(const RowT *a, const RowT *b);
|
|
static void rowText(ToolBufT *out, const RowT *row);
|
|
static long rowTotal(long samples, uint32_t tickMs);
|
|
static bool runTool(const char *path, char *const *args, ToolBufT *err);
|
|
static void sn(ToolBufT *commands, uint32_t value);
|
|
static void snCase(const char *repo, const char *work, ChecksT *checks);
|
|
static void snTone(ToolBufT *commands, uint32_t channel, uint32_t period, uint32_t atten);
|
|
static void songFree(SongT *song);
|
|
static bool songSame(const SongT *a, const SongT *b);
|
|
static int synthetic(const char *repo, const char *work);
|
|
static const char *trimmed(ToolBufT *text);
|
|
static void vgm(const ToolBufT *commands, uint32_t snClock, uint32_t ayClock, long loopAt, uint32_t rate, ToolBufT *out);
|
|
static void vgmWait(ToolBufT *commands, uint32_t samples);
|
|
static void vgmWaits(ToolBufT *commands, uint32_t command, uint32_t count);
|
|
static bool voiceSame(VoiceT a, VoiceT b);
|
|
static void voiceText(ToolBufT *out, VoiceT voice);
|
|
|
|
// The partials a heard pitch may be of a tone the song plays.
|
|
static const double kHarmonics[] = { 1.0, 2.0, 3.0, 0.5 };
|
|
|
|
#define VOICE_ON(value, atten) { true, (value), (atten) }
|
|
#define VOICE_OFF { false, 0u, 0u }
|
|
|
|
// snCase's rows: frame 0 tone 0 and white noise; frame 3 tone 1, tone 0
|
|
// off; frame 6 (the loop) periodic noise on tone voice 2; frame 11 the
|
|
// same.
|
|
static const FrameWantT kSnFrames[] = {
|
|
{ 0, { { VOICE_ON(440u, 2u), VOICE_OFF, VOICE_OFF, VOICE_ON(31u, 5u) } } },
|
|
{ 3, { { VOICE_OFF, VOICE_ON(881u, 0u), VOICE_OFF, VOICE_ON(31u, 5u) } } },
|
|
{ 6, { { VOICE_OFF, VOICE_ON(881u, 0u), VOICE_ON(70u, 4u), VOICE_OFF } } },
|
|
{ 11, { { VOICE_OFF, VOICE_ON(881u, 0u), VOICE_ON(70u, 4u), VOICE_OFF } } }
|
|
};
|
|
|
|
static const char kUsage[] = "usage: vgmCheck synthetic REPO WORK\n vgmCheck pitch WAV JYM\n";
|
|
|
|
|
|
|
|
// An AY-3-8910 register write.
|
|
static void ay(ToolBufT *commands, uint32_t reg, uint32_t value) {
|
|
toolBufByte(commands, CMD_AY);
|
|
toolBufByte(commands, reg);
|
|
toolBufByte(commands, value);
|
|
}
|
|
|
|
|
|
// The AY-3-8910: tones, mixed noise, an envelope's decay followed row by
|
|
// row, and a buzz (the defines above say what each write is).
|
|
static void ayCase(const char *repo, const char *work, ChecksT *checks) {
|
|
ToolBufT commands;
|
|
ToolBufT data;
|
|
ToolBufT err;
|
|
SongT song;
|
|
RowT want;
|
|
char what[WHAT_MAX];
|
|
long level;
|
|
int row;
|
|
|
|
memset(&commands, 0, sizeof(commands));
|
|
memset(&data, 0, sizeof(data));
|
|
memset(&err, 0, sizeof(err));
|
|
ay(&commands, AY_TONE_A_LO, AY_TONE_A_PERIOD & BYTE_MASK);
|
|
ay(&commands, AY_TONE_A_HI, AY_TONE_A_PERIOD >> BYTE_BITS);
|
|
ay(&commands, AY_TONE_C_LO, AY_TONE_C_PERIOD);
|
|
ay(&commands, AY_TONE_C_HI, 0u);
|
|
ay(&commands, AY_NOISE, AY_NOISE_PERIOD);
|
|
ay(&commands, AY_MIXER, AY_MIXER_START);
|
|
ay(&commands, AY_VOLUME_A, AY_VOLUME_A_13);
|
|
ay(&commands, AY_VOLUME_B, AY_VOLUME_B_15);
|
|
ay(&commands, AY_VOLUME_C, AY_VOLUME_C_ENVELOPE);
|
|
ay(&commands, AY_ENVELOPE_LO, AY_DECAY_PERIOD & BYTE_MASK);
|
|
ay(&commands, AY_ENVELOPE_HI, AY_DECAY_PERIOD >> BYTE_BITS);
|
|
ay(&commands, AY_SHAPE, AY_SHAPE_DECAY);
|
|
vgmWaits(&commands, CMD_WAIT_PAL, AY_DECAY_FRAMES);
|
|
ay(&commands, AY_MIXER, AY_MIXER_BUZZ);
|
|
ay(&commands, AY_ENVELOPE_LO, AY_BUZZ_PERIOD);
|
|
ay(&commands, AY_ENVELOPE_HI, 0u);
|
|
ay(&commands, AY_SHAPE, AY_SHAPE_REPEAT);
|
|
vgmWaits(&commands, CMD_WAIT_PAL, AY_BUZZ_FRAMES);
|
|
vgm(&commands, 0u, AY_CLOCK, VGM_NO_LOOP, PAL_UPDATE_HZ, &data);
|
|
bake(repo, work, "ay", &data, NULL, &song, &err);
|
|
if (!song.ok) {
|
|
check(checks, false, "ay: did not convert: %s", trimmed(&err));
|
|
return;
|
|
}
|
|
expectNumber(checks, "ay: tick (50 Hz updates)", true, (long)song.tick, AY_TICK_MS);
|
|
check(checks, !song.hasLoop, "ay: no loop: got row %zu, want none", song.loopRow);
|
|
expectFlag(checks, "ay: buzz warning", strstr(gateTextString(&err), "envelope repeating faster") != NULL);
|
|
// Tone C follows the decay: at the end of row r (20 * (r + 1) ms), the
|
|
// level is 15 - floor(time / 16 ms), silent once the decay is over.
|
|
for (row = 0; row < AY_DECAY_ROWS; row++) {
|
|
level = AY_LEVEL_MAX - (long)((double)(AY_TICK_MS * (uint32_t)(row + 1)) / AY_STEP_MS);
|
|
memset(&want, 0, sizeof(want));
|
|
want.voice[0] = (VoiceT)VOICE_ON(AY_TONE_A_HZ, AY_TONE_A_ATTEN);
|
|
want.voice[NOISE_VOICE] = (VoiceT)VOICE_ON(AY_NOISE_PITCH, 0u);
|
|
if (level > 0) {
|
|
want.voice[TONE_VOICES - 1u] = (VoiceT)VOICE_ON(AY_TONE_C_HZ, (uint32_t)(AY_LEVEL_MAX - level));
|
|
}
|
|
snprintf(what, sizeof(what), "ay: row %d", row);
|
|
expectRow(checks, what, &song, row, &want);
|
|
}
|
|
want.voice[TONE_VOICES - 1u] = (VoiceT)VOICE_ON(AY_BUZZ_HZ, 0u);
|
|
expectRow(checks, "ay: buzz row", &song, AY_BUZZ_ROW, &want);
|
|
songFree(&song);
|
|
toolBufFree(&commands);
|
|
toolBufFree(&data);
|
|
toolBufFree(&err);
|
|
}
|
|
|
|
|
|
// Data written as WORK/name.vgm, converted (given "--tick extra"
|
|
// when extra is not NULL) and baked; the song decoded (song->ok false when
|
|
// a tool failed), and what vgm2song said on stderr (and songbake, when it
|
|
// failed).
|
|
static void bake(const char *repo, const char *work, const char *name, const ToolBufT *data, const char *extra, SongT *song, ToolBufT *err) {
|
|
const char *build;
|
|
char *tools;
|
|
char *converter;
|
|
char *baker;
|
|
char *vgmPath;
|
|
char *songPath;
|
|
char *jymPath;
|
|
char *args[TOOL_ARGS_MAX];
|
|
size_t count;
|
|
size_t converted;
|
|
|
|
memset(song, 0, sizeof(*song));
|
|
err->len = 0u;
|
|
build = getenv("JOEY_BUILD");
|
|
tools = build != NULL ? pathOf(build, "tools", "") : pathOf(repo, "build/tools", "");
|
|
converter = pathOf(tools, "vgm2song", "");
|
|
baker = pathOf(tools, "songbake", "");
|
|
vgmPath = pathOf(work, name, ".vgm");
|
|
songPath = pathOf(work, name, ".song");
|
|
jymPath = pathOf(work, name, ".jym");
|
|
if (!toolWriteFile(vgmPath, data->data, data->len)) {
|
|
toolFail("cannot write %s", vgmPath);
|
|
}
|
|
count = 0u;
|
|
args[count++] = converter;
|
|
if (extra != NULL) {
|
|
args[count++] = (char *)"--tick";
|
|
args[count++] = (char *)extra;
|
|
}
|
|
args[count++] = vgmPath;
|
|
args[count++] = songPath;
|
|
args[count] = NULL;
|
|
if (runTool(converter, args, err)) {
|
|
converted = err->len;
|
|
count = 0u;
|
|
args[count++] = baker;
|
|
args[count++] = songPath;
|
|
args[count++] = jymPath;
|
|
args[count] = NULL;
|
|
if (runTool(baker, args, err)) {
|
|
err->len = converted;
|
|
jymRows(jymPath, song);
|
|
}
|
|
}
|
|
free(tools);
|
|
free(converter);
|
|
free(baker);
|
|
free(vgmPath);
|
|
free(songPath);
|
|
free(jymPath);
|
|
}
|
|
|
|
|
|
// A check: counted, and its line (printf-style) kept when it fails.
|
|
static void check(ChecksT *checks, bool ok, const char *fmt, ...) {
|
|
va_list args;
|
|
|
|
checks->count++;
|
|
if (ok) {
|
|
return;
|
|
}
|
|
va_start(args, fmt);
|
|
gateTextAddV(&checks->failures, fmt, args);
|
|
va_end(args);
|
|
toolBufByte(&checks->failures, '\0');
|
|
checks->failureCount++;
|
|
}
|
|
|
|
|
|
// Something that must be so.
|
|
static void expectFlag(ChecksT *checks, const char *what, bool got) {
|
|
check(checks, got, "%s: got false, want true", what);
|
|
}
|
|
|
|
|
|
// A number a song gives (none when have is false), against the one wanted.
|
|
static void expectNumber(ChecksT *checks, const char *what, bool have, long got, long want) {
|
|
if (have) {
|
|
check(checks, got == want, "%s: got %ld, want %ld", what, got, want);
|
|
} else {
|
|
check(checks, false, "%s: got none, want %ld", what, want);
|
|
}
|
|
}
|
|
|
|
|
|
// A row's voices against the ones wanted.
|
|
static void expectRow(ChecksT *checks, const char *what, const SongT *song, long row, const RowT *want) {
|
|
ToolBufT text;
|
|
|
|
if (row < 0 || (size_t)row >= song->rowCount) {
|
|
check(checks, false, "%s: no row %ld in a song of %zu", what, row, song->rowCount);
|
|
return;
|
|
}
|
|
memset(&text, 0, sizeof(text));
|
|
gateTextAdd(&text, "%s: got ", what);
|
|
rowText(&text, &song->rows[row]);
|
|
gateTextAdd(&text, ", want ");
|
|
rowText(&text, want);
|
|
check(checks, rowSame(&song->rows[row], want), "%s", gateTextString(&text));
|
|
toolBufFree(&text);
|
|
}
|
|
|
|
|
|
// One voice of a row against the one wanted.
|
|
static void expectVoice(ChecksT *checks, const char *what, const SongT *song, long row, uint32_t voice, VoiceT want) {
|
|
RowT wanted;
|
|
|
|
if (row < 0 || (size_t)row >= song->rowCount) {
|
|
check(checks, false, "%s: no row %ld in a song of %zu", what, row, song->rowCount);
|
|
return;
|
|
}
|
|
wanted = song->rows[row];
|
|
wanted.voice[voice] = want;
|
|
expectRow(checks, what, song, row, &wanted);
|
|
}
|
|
|
|
|
|
// The fit to 64 KB: a song too long for 16 ms rows goes to 32 ms rows.
|
|
static void fitCase(const char *repo, const char *work, ChecksT *checks) {
|
|
ToolBufT commands;
|
|
ToolBufT data;
|
|
ToolBufT err;
|
|
SongT song;
|
|
uint32_t frame;
|
|
|
|
memset(&commands, 0, sizeof(commands));
|
|
memset(&data, 0, sizeof(data));
|
|
memset(&err, 0, sizeof(err));
|
|
for (frame = 0u; frame < FIT_FRAMES; frame++) {
|
|
snTone(&commands, 0u, FIT_PERIOD_BASE + frame % FIT_PERIODS, 0u);
|
|
toolBufByte(&commands, CMD_WAIT_NTSC);
|
|
}
|
|
vgm(&commands, SN_CLOCK, 0u, VGM_NO_LOOP, NTSC_UPDATE_HZ, &data);
|
|
bake(repo, work, "fit", &data, NULL, &song, &err);
|
|
if (song.ok) {
|
|
expectNumber(checks, "fit: tick", true, (long)song.tick, FIT_TICK_MS);
|
|
} else {
|
|
check(checks, false, "fit: did not convert: %s", trimmed(&err));
|
|
}
|
|
expectFlag(checks, "fit: warning", strstr(gateTextString(&err), "using 32 ms rows") != NULL);
|
|
songFree(&song);
|
|
toolBufFree(&commands);
|
|
toolBufFree(&data);
|
|
toolBufFree(&err);
|
|
}
|
|
|
|
|
|
// data gzip-compressed (a .vgz): one gzip member around a deflate stream.
|
|
static void gzipOf(const ToolBufT *data, ToolBufT *out) {
|
|
uint8_t *zlib;
|
|
size_t zlibLen;
|
|
|
|
if (!toolDeflateZlib(data->data, data->len, &zlib, &zlibLen)) {
|
|
toolFail("cannot compress");
|
|
}
|
|
toolBufByte(out, GZIP_ID1);
|
|
toolBufByte(out, GZIP_ID2);
|
|
toolBufByte(out, GZIP_DEFLATE);
|
|
toolBufZeros(out, GZIP_PLAIN_BYTES);
|
|
toolBufByte(out, GZIP_OS_UNKNOWN);
|
|
toolBufAdd(out, zlib + ZLIB_HEADER, zlibLen - ZLIB_HEADER - ZLIB_TRAILER);
|
|
toolBufLe32(out, toolCrc32(0u, data->data, data->len));
|
|
toolBufLe32(out, (uint32_t)data->len);
|
|
free(zlib);
|
|
}
|
|
|
|
|
|
// A JYM1 file's tick, loop row, and the voices after each row.
|
|
static void jymRows(const char *path, SongT *song) {
|
|
const uint8_t *events;
|
|
uint8_t *data;
|
|
size_t len;
|
|
size_t count;
|
|
size_t at;
|
|
size_t i;
|
|
uint32_t loopOfs;
|
|
uint32_t op;
|
|
RowT state;
|
|
ToolBufT problem;
|
|
|
|
memset(&problem, 0, sizeof(problem));
|
|
if (!gateTextReadFile(path, &data, &len, &problem)) {
|
|
toolFail("%s", gateTextString(&problem));
|
|
}
|
|
if (len < JYM_HEADER || memcmp(data, JYM_MAGIC, JYM_MAGIC_BYTES) != 0) {
|
|
toolFail("%s: not a JYM1 file", path);
|
|
}
|
|
memset(song, 0, sizeof(*song));
|
|
memset(&state, 0, sizeof(state));
|
|
song->ok = true;
|
|
song->tick = (uint32_t)data[JYM_TICK_AT] | ((uint32_t)data[JYM_TICK_AT + 1u] << BYTE_BITS);
|
|
loopOfs = (uint32_t)data[JYM_LOOP_AT] | ((uint32_t)data[JYM_LOOP_AT + 1u] << BYTE_BITS);
|
|
events = data + JYM_HEADER;
|
|
count = len - JYM_HEADER;
|
|
at = 0u;
|
|
while (at < count) {
|
|
if (at == loopOfs) {
|
|
song->hasLoop = true;
|
|
song->loopRow = song->rowCount;
|
|
}
|
|
op = events[at];
|
|
if (op == OP_END) {
|
|
break;
|
|
}
|
|
if (op >= OP_NOTE && op < OP_NOTE + VOICES) {
|
|
if (at + NOTE_BYTES > count) {
|
|
toolFail("%s: cut short", path);
|
|
}
|
|
state.voice[op - OP_NOTE].on = true;
|
|
state.voice[op - OP_NOTE].value = (uint32_t)events[at + NOTE_VALUE_LO] | ((uint32_t)events[at + NOTE_VALUE_HI] << BYTE_BITS);
|
|
state.voice[op - OP_NOTE].atten = events[at + NOTE_ATTEN];
|
|
at += NOTE_BYTES;
|
|
} else if (op >= OP_OFF && op < OP_OFF + VOICES) {
|
|
memset(&state.voice[op - OP_OFF], 0, sizeof(VoiceT));
|
|
at++;
|
|
} else if (op == OP_TEMPO) {
|
|
at += TEMPO_BYTES;
|
|
} else if ((op & OP_WAIT) != 0u) {
|
|
for (i = 0u; i < (op & OP_WAIT_MASK); i++) {
|
|
if (song->rowCount == song->rowCap) {
|
|
song->rowCap = song->rowCap == 0u ? ROWS_FIRST_CAP : song->rowCap * 2u;
|
|
song->rows = (RowT *)realloc(song->rows, song->rowCap * sizeof(RowT));
|
|
if (song->rows == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
}
|
|
song->rows[song->rowCount++] = state;
|
|
}
|
|
at++;
|
|
} else {
|
|
toolFail("%s: bad JYM1 op 0x%02X", path, (unsigned)op);
|
|
}
|
|
}
|
|
free(data);
|
|
}
|
|
|
|
|
|
// dir/name then suffix, malloc'd.
|
|
static char *pathOf(const char *dir, const char *name, const char *suffix) {
|
|
ToolBufT path;
|
|
|
|
memset(&path, 0, sizeof(path));
|
|
gateTextAdd(&path, "%s/%s%s", dir, name, suffix);
|
|
gateTextString(&path);
|
|
return (char *)path.data;
|
|
}
|
|
|
|
|
|
// pitch: the windows of Furnace's audio whose strongest pitch is one the
|
|
// JYM1 sounds there.
|
|
static int pitch(const char *wavPath, const char *jymPath) {
|
|
const RowT *state;
|
|
GateWavT wav;
|
|
GateFftT fft;
|
|
ToolBufT problem;
|
|
SongT song;
|
|
double *audio;
|
|
double *window;
|
|
double *squares;
|
|
double *piece;
|
|
double *spectrum;
|
|
double *freqs;
|
|
double loud;
|
|
double a;
|
|
double b;
|
|
double c;
|
|
double bend;
|
|
double heard;
|
|
double leastAtten;
|
|
size_t frames;
|
|
size_t bins;
|
|
size_t start;
|
|
size_t peak;
|
|
size_t k;
|
|
size_t v;
|
|
size_t h;
|
|
long row;
|
|
long tested;
|
|
long matched;
|
|
bool anyTone;
|
|
bool hit;
|
|
|
|
memset(&problem, 0, sizeof(problem));
|
|
if (!gateWavRead(wavPath, &wav, &problem)) {
|
|
toolFail("%s", gateTextString(&problem));
|
|
}
|
|
audio = gateWavMono(&wav);
|
|
frames = gateWavFrames(&wav);
|
|
jymRows(jymPath, &song);
|
|
bins = WINDOW / 2u + 1u;
|
|
window = (double *)calloc(WINDOW, sizeof(double));
|
|
piece = (double *)calloc(WINDOW, sizeof(double));
|
|
spectrum = (double *)calloc(bins, sizeof(double));
|
|
freqs = (double *)calloc(bins, sizeof(double));
|
|
squares = (double *)calloc(frames > 0u ? frames : 1u, sizeof(double));
|
|
if (window == NULL || piece == NULL || spectrum == NULL || freqs == NULL || squares == NULL) {
|
|
toolFail("out of memory");
|
|
}
|
|
for (k = 0u; k < bins; k++) {
|
|
freqs[k] = gateRfftFreq(k, WINDOW, wav.rate);
|
|
}
|
|
gateHanning(window, WINDOW);
|
|
gateFftInit(&fft, WINDOW);
|
|
for (k = 0u; k < frames; k++) {
|
|
squares[k] = audio[k] * audio[k];
|
|
}
|
|
loud = sqrt(gateMean(squares, frames)) * PITCH_LOUD_SHARE;
|
|
tested = 0;
|
|
matched = 0;
|
|
for (start = 0u; start + WINDOW < frames; start += HOP) {
|
|
if (sqrt(gateMean(squares + start, WINDOW)) < loud) {
|
|
continue;
|
|
}
|
|
row = (long)floor(((double)start + (double)WINDOW / 2.0) / (double)wav.rate * MS_PER_SECOND / (double)song.tick);
|
|
if (row >= (long)song.rowCount) {
|
|
break;
|
|
}
|
|
state = &song.rows[row];
|
|
anyTone = false;
|
|
leastAtten = 0.0;
|
|
for (v = 0u; v < TONE_VOICES; v++) {
|
|
if (state->voice[v].on && (!anyTone || (double)state->voice[v].atten < leastAtten)) {
|
|
leastAtten = (double)state->voice[v].atten;
|
|
}
|
|
anyTone = anyTone || state->voice[v].on;
|
|
}
|
|
// No tone, or the noise as loud as the loudest: nothing pitched to
|
|
// hear.
|
|
if (!anyTone || (state->voice[NOISE_VOICE].on && (double)state->voice[NOISE_VOICE].atten <= leastAtten)) {
|
|
continue;
|
|
}
|
|
for (k = 0u; k < WINDOW; k++) {
|
|
piece[k] = audio[start + k] * window[k];
|
|
}
|
|
gateFftAbs(&fft, piece, WINDOW, spectrum);
|
|
peak = 0u;
|
|
for (k = 0u; k < bins; k++) {
|
|
if (!(freqs[k] >= PITCH_BAND_LOW && freqs[k] <= PITCH_BAND_HIGH)) {
|
|
spectrum[k] = 0.0;
|
|
}
|
|
peak = spectrum[k] > spectrum[peak] ? k : peak;
|
|
}
|
|
if (peak == 0u || peak >= bins - 1u) {
|
|
continue;
|
|
}
|
|
a = spectrum[peak - 1u];
|
|
b = spectrum[peak];
|
|
c = spectrum[peak + 1u];
|
|
bend = a - 2.0 * b + c != 0.0 ? 0.5 * (a - c) / (a - 2.0 * b + c) : 0.0;
|
|
heard = freqs[peak] + bend * (freqs[1] - freqs[0]);
|
|
tested++;
|
|
hit = false;
|
|
for (v = 0u; v < TONE_VOICES; v++) {
|
|
for (h = 0u; h < GATE_COUNT_OF(kHarmonics) && state->voice[v].on; h++) {
|
|
hit = hit || fabs(CENTS_PER_OCTAVE * log2(heard / ((double)state->voice[v].value * kHarmonics[h]))) <= PITCH_CENTS;
|
|
}
|
|
}
|
|
matched += hit ? 1 : 0;
|
|
}
|
|
printf("PITCH %ld/%ld\n", matched, tested);
|
|
gateFftFree(&fft);
|
|
gateWavFree(&wav);
|
|
songFree(&song);
|
|
free(audio);
|
|
free(window);
|
|
free(piece);
|
|
free(spectrum);
|
|
free(freqs);
|
|
free(squares);
|
|
return 0;
|
|
}
|
|
|
|
|
|
// A 32-bit little-endian field.
|
|
static void putLe32(uint8_t *at, uint32_t value) {
|
|
uint32_t i;
|
|
|
|
for (i = 0u; i < VGM_FIELD_BYTES; i++) {
|
|
at[i] = (uint8_t)((value >> (i * BYTE_BITS)) & BYTE_MASK);
|
|
}
|
|
}
|
|
|
|
|
|
// The refusals and warnings: an unknown command, two chips without
|
|
// --chip, another chip's writes, and a second SN chip's.
|
|
static void refusalCases(const char *repo, const char *work, ChecksT *checks) {
|
|
ToolBufT commands;
|
|
ToolBufT data;
|
|
ToolBufT err;
|
|
SongT song;
|
|
|
|
memset(&commands, 0, sizeof(commands));
|
|
memset(&data, 0, sizeof(data));
|
|
memset(&err, 0, sizeof(err));
|
|
toolBufByte(&commands, CMD_UNKNOWN);
|
|
toolBufByte(&commands, CMD_WAIT_NTSC);
|
|
vgm(&commands, SN_CLOCK, 0u, VGM_NO_LOOP, NTSC_UPDATE_HZ, &data);
|
|
bake(repo, work, "unknown", &data, NULL, &song, &err);
|
|
expectFlag(checks, "unknown command refused", strstr(gateTextString(&err), "unknown VGM command") != NULL);
|
|
songFree(&song);
|
|
|
|
commands.len = 0u;
|
|
data.len = 0u;
|
|
snTone(&commands, 0u, SN_TONE0_PERIOD, 0u);
|
|
ay(&commands, AY_VOLUME_A, REFUSE_AY_VOLUME);
|
|
toolBufByte(&commands, CMD_WAIT_NTSC);
|
|
vgm(&commands, SN_CLOCK, AY_CLOCK, VGM_NO_LOOP, NTSC_UPDATE_HZ, &data);
|
|
bake(repo, work, "both", &data, NULL, &song, &err);
|
|
expectFlag(checks, "two chips need --chip", strstr(gateTextString(&err), "--chip") != NULL);
|
|
songFree(&song);
|
|
|
|
commands.len = 0u;
|
|
data.len = 0u;
|
|
snTone(&commands, 0u, SN_TONE0_PERIOD, 0u);
|
|
toolBufByte(&commands, CMD_YM2413);
|
|
toolBufByte(&commands, YM2413_REG);
|
|
toolBufByte(&commands, YM2413_VALUE);
|
|
toolBufByte(&commands, CMD_SN2);
|
|
toolBufByte(&commands, SN2_VALUE);
|
|
toolBufByte(&commands, CMD_WAIT_NTSC);
|
|
vgm(&commands, SN_CLOCK, 0u, VGM_NO_LOOP, NTSC_UPDATE_HZ, &data);
|
|
bake(repo, work, "others", &data, NULL, &song, &err);
|
|
expectFlag(checks, "other chip warned", song.ok && strstr(gateTextString(&err), "other chips") != NULL);
|
|
songFree(&song);
|
|
|
|
commands.len = 0u;
|
|
data.len = 0u;
|
|
snTone(&commands, 0u, SN_TONE0_PERIOD, 0u);
|
|
toolBufByte(&commands, CMD_SN2);
|
|
toolBufByte(&commands, SN_TONE0_SILENT);
|
|
toolBufByte(&commands, CMD_WAIT_NTSC);
|
|
vgm(&commands, SN_CLOCK, 0u, VGM_NO_LOOP, NTSC_UPDATE_HZ, &data);
|
|
bake(repo, work, "second", &data, NULL, &song, &err);
|
|
expectFlag(checks, "second chip warned", song.ok && strstr(gateTextString(&err), "second chip") != NULL);
|
|
songFree(&song);
|
|
toolBufFree(&commands);
|
|
toolBufFree(&data);
|
|
toolBufFree(&err);
|
|
}
|
|
|
|
|
|
// The row a VGM sample falls in at tickMs rows.
|
|
static long rowOf(long sample, uint32_t tickMs) {
|
|
return (long)floor((double)sample / ((double)(tickMs * VGM_RATE) / MS_PER_SECOND));
|
|
}
|
|
|
|
|
|
static bool rowSame(const RowT *a, const RowT *b) {
|
|
uint32_t v;
|
|
|
|
for (v = 0u; v < VOICES; v++) {
|
|
if (!voiceSame(a->voice[v], b->voice[v])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// A row's voices, each "(value, attenuation)" or "off".
|
|
static void rowText(ToolBufT *out, const RowT *row) {
|
|
uint32_t v;
|
|
|
|
for (v = 0u; v < VOICES; v++) {
|
|
gateTextAdd(out, "%s", v > 0u ? " " : "");
|
|
voiceText(out, row->voice[v]);
|
|
}
|
|
}
|
|
|
|
|
|
// math.ceil of samples / a row's samples: how many rows they fill.
|
|
static long rowTotal(long samples, uint32_t tickMs) {
|
|
return (long)ceil((double)samples / ((double)(tickMs * VGM_RATE) / MS_PER_SECOND));
|
|
}
|
|
|
|
|
|
// One of make/tools.mk's tools, run: its stdout dropped, its stderr
|
|
// appended to err; true when it exits 0. A tool that is not there is
|
|
// fatal.
|
|
static bool runTool(const char *path, char *const *args, ToolBufT *err) {
|
|
int pipeEnds[2];
|
|
int status;
|
|
int nullFd;
|
|
pid_t child;
|
|
ssize_t got;
|
|
uint8_t chunk[READ_CHUNK];
|
|
|
|
if (access(path, X_OK) != 0) {
|
|
toolFail("cannot run %s: %s", path, strerror(errno));
|
|
}
|
|
if (pipe(pipeEnds) != 0) {
|
|
toolFail("cannot make a pipe");
|
|
}
|
|
child = fork();
|
|
if (child < 0) {
|
|
toolFail("cannot run %s", path);
|
|
}
|
|
if (child == 0) {
|
|
nullFd = open("/dev/null", O_WRONLY);
|
|
dup2(nullFd, STDOUT_FILENO);
|
|
dup2(pipeEnds[1], STDERR_FILENO);
|
|
close(pipeEnds[0]);
|
|
close(pipeEnds[1]);
|
|
execv(path, args);
|
|
_exit(CHILD_EXEC_FAILED);
|
|
}
|
|
close(pipeEnds[1]);
|
|
while ((got = read(pipeEnds[0], chunk, sizeof(chunk))) > 0) {
|
|
toolBufAdd(err, chunk, (size_t)got);
|
|
}
|
|
close(pipeEnds[0]);
|
|
if (waitpid(child, &status, 0) < 0) {
|
|
toolFail("cannot wait for %s", path);
|
|
}
|
|
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
|
}
|
|
|
|
|
|
// An SN76489 write.
|
|
static void sn(ToolBufT *commands, uint32_t value) {
|
|
toolBufByte(commands, CMD_SN);
|
|
toolBufByte(commands, value);
|
|
}
|
|
|
|
|
|
// The SN76489: tones, white and periodic noise, every wait form, a loop,
|
|
// skipped commands, .vgz, --tick, and the TI-99/4A's clock (the defines
|
|
// above say what each write is).
|
|
static void snCase(const char *repo, const char *work, ChecksT *checks) {
|
|
ToolBufT commands;
|
|
ToolBufT data;
|
|
ToolBufT packed;
|
|
ToolBufT err;
|
|
SongT song;
|
|
SongT other;
|
|
RowT want;
|
|
char what[WHAT_MAX];
|
|
long loopAt;
|
|
size_t i;
|
|
|
|
memset(&commands, 0, sizeof(commands));
|
|
memset(&data, 0, sizeof(data));
|
|
memset(&packed, 0, sizeof(packed));
|
|
memset(&err, 0, sizeof(err));
|
|
snTone(&commands, 0u, SN_TONE0_PERIOD, SN_TONE0_ATTEN);
|
|
sn(&commands, SN_NOISE_WHITE_1024);
|
|
sn(&commands, SN_NOISE_ATTEN_5);
|
|
vgmWaits(&commands, CMD_WAIT_NTSC, SN_OPENING_FRAMES);
|
|
snTone(&commands, 1u, SN_TONE1_PERIOD, SN_TONE1_ATTEN);
|
|
sn(&commands, SN_TONE0_SILENT);
|
|
vgmWait(&commands, FRAME);
|
|
vgmWait(&commands, FRAME - SN_DAC_WAIT);
|
|
toolBufByte(&commands, SN_DAC_WAIT_16);
|
|
vgmWait(&commands, FRAME);
|
|
loopAt = (long)commands.len;
|
|
snTone(&commands, 2u, SN_TONE2_PERIOD, SN_TONE2_ATTEN);
|
|
sn(&commands, SN_NOISE_PERIODIC_T2);
|
|
sn(&commands, SN_NOISE_ATTEN_4);
|
|
toolBufByte(&commands, CMD_SHORT_WAIT + SN_SHORT_WAIT_15);
|
|
vgmWait(&commands, FRAME - SN_SHORT_WAIT_SAMPLES);
|
|
vgmWait(&commands, FRAME);
|
|
vgmWait(&commands, FRAME);
|
|
toolBufByte(&commands, CMD_GG_STEREO);
|
|
toolBufByte(&commands, GG_STEREO_ALL);
|
|
toolBufByte(&commands, CMD_DATA_BLOCK);
|
|
toolBufByte(&commands, DATA_BLOCK_MARK);
|
|
toolBufByte(&commands, DATA_BLOCK_TYPE);
|
|
toolBufLe32(&commands, DATA_BLOCK_BYTES);
|
|
for (i = 1u; i <= DATA_BLOCK_BYTES; i++) {
|
|
toolBufByte(&commands, (uint32_t)i);
|
|
}
|
|
vgmWaits(&commands, CMD_WAIT_NTSC, SN_CLOSING_FRAMES);
|
|
vgm(&commands, SN_CLOCK, 0u, loopAt, NTSC_UPDATE_HZ, &data);
|
|
bake(repo, work, "sn", &data, NULL, &song, &err);
|
|
if (!song.ok) {
|
|
check(checks, false, "sn: did not convert: %s", trimmed(&err));
|
|
return;
|
|
}
|
|
expectNumber(checks, "sn: tick (60 Hz updates)", true, (long)song.tick, SN_TICK_MS);
|
|
expectNumber(checks, "sn: loop row", song.hasLoop, (long)song.loopRow, rowOf(SN_LOOP_FRAME * FRAME, SN_TICK_MS));
|
|
expectNumber(checks, "sn: rows", true, (long)song.rowCount, rowTotal(SN_FRAMES * FRAME, SN_TICK_MS));
|
|
for (i = 0u; i < GATE_COUNT_OF(kSnFrames); i++) {
|
|
snprintf(what, sizeof(what), "sn: frame %ld", kSnFrames[i].frame);
|
|
expectRow(checks, what, &song, rowOf(kSnFrames[i].frame * FRAME, SN_TICK_MS), &kSnFrames[i].row);
|
|
}
|
|
want = kSnFrames[0].row;
|
|
expectVoice(checks, "sn: the frame before 3", &song, rowOf(SN_BEFORE_FRAME * FRAME, SN_TICK_MS) - 1, 0u, want.voice[0]);
|
|
|
|
// The same file compressed (.vgz) bakes the same; at --tick 33 it keeps
|
|
// its tick and its length.
|
|
gzipOf(&data, &packed);
|
|
bake(repo, work, "snGz", &packed, NULL, &other, &err);
|
|
if (other.ok) {
|
|
check(checks, songSame(&other, &song), "sn: .vgz: not the song the .vgm bakes");
|
|
} else {
|
|
check(checks, false, "sn: .vgz: did not convert: %s", trimmed(&err));
|
|
}
|
|
songFree(&other);
|
|
bake(repo, work, "snSlow", &data, SN_SLOW_TICK, &other, &err);
|
|
expectNumber(checks, "sn: --tick 33", other.ok, (long)other.tick, SN_SLOW_TICK_MS);
|
|
expectNumber(checks, "sn: --tick 33 rows", other.ok, (long)other.rowCount, rowTotal(SN_FRAMES * FRAME, SN_SLOW_TICK_MS));
|
|
songFree(&other);
|
|
|
|
// The TI-99/4A's chip: 447443 Hz, no /8 prescaler: period 254 is still
|
|
// 447443 / (4 * 254) = 440.4 Hz.
|
|
commands.len = 0u;
|
|
data.len = 0u;
|
|
snTone(&commands, 0u, SN_TONE0_PERIOD, TI_TONE_ATTEN);
|
|
toolBufByte(&commands, CMD_WAIT_NTSC);
|
|
vgm(&commands, TI_CLOCK, 0u, VGM_NO_LOOP, NTSC_UPDATE_HZ, &data);
|
|
bake(repo, work, "ti", &data, NULL, &other, &err);
|
|
want.voice[0].atten = TI_TONE_ATTEN;
|
|
if (other.ok) {
|
|
expectVoice(checks, "sn: 447 kHz tone", &other, 0, 0u, want.voice[0]);
|
|
} else {
|
|
check(checks, false, "sn: 447 kHz tone: did not convert: %s", trimmed(&err));
|
|
}
|
|
songFree(&other);
|
|
songFree(&song);
|
|
toolBufFree(&commands);
|
|
toolBufFree(&data);
|
|
toolBufFree(&packed);
|
|
toolBufFree(&err);
|
|
}
|
|
|
|
|
|
// An SN76489 tone: the channel's period (latch and data bytes) and its
|
|
// attenuation.
|
|
static void snTone(ToolBufT *commands, uint32_t channel, uint32_t period, uint32_t atten) {
|
|
sn(commands, SN_LATCH | (channel << SN_CHANNEL_SHIFT) | (period & SN_LOW_MASK));
|
|
sn(commands, (period >> SN_HIGH_SHIFT) & SN_HIGH_MASK);
|
|
sn(commands, SN_VOLUME | (channel << SN_CHANNEL_SHIFT) | atten);
|
|
}
|
|
|
|
|
|
static void songFree(SongT *song) {
|
|
free(song->rows);
|
|
memset(song, 0, sizeof(*song));
|
|
}
|
|
|
|
|
|
// Two bakes of a song alike: tick, loop, and every row.
|
|
static bool songSame(const SongT *a, const SongT *b) {
|
|
size_t i;
|
|
|
|
if (a->tick != b->tick || a->hasLoop != b->hasLoop || (a->hasLoop && a->loopRow != b->loopRow) || a->rowCount != b->rowCount) {
|
|
return false;
|
|
}
|
|
for (i = 0u; i < a->rowCount; i++) {
|
|
if (!rowSame(&a->rows[i], &b->rows[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// synthetic: every case, its failures, and the count.
|
|
static int synthetic(const char *repo, const char *work) {
|
|
ChecksT checks;
|
|
const char *at;
|
|
size_t i;
|
|
|
|
memset(&checks, 0, sizeof(checks));
|
|
snCase(repo, work, &checks);
|
|
ayCase(repo, work, &checks);
|
|
fitCase(repo, work, &checks);
|
|
refusalCases(repo, work, &checks);
|
|
at = (const char *)checks.failures.data;
|
|
for (i = 0u; i < checks.failureCount; i++) {
|
|
printf("vgmCheck: FAIL %s\n", at);
|
|
at += strlen(at) + 1u;
|
|
}
|
|
printf("SYNTHETIC %zu/%zu\n", checks.count - checks.failureCount, checks.count);
|
|
toolBufFree(&checks.failures);
|
|
return checks.failureCount > 0u ? FAIL_EXIT : 0;
|
|
}
|
|
|
|
|
|
// text without its leading and trailing white space, as a C string.
|
|
static const char *trimmed(ToolBufT *text) {
|
|
const char *start;
|
|
|
|
while (text->len > 0u && isspace(text->data[text->len - 1u])) {
|
|
text->len--;
|
|
}
|
|
start = gateTextString(text);
|
|
while (isspace((unsigned char)*start)) {
|
|
start++;
|
|
}
|
|
return start;
|
|
}
|
|
|
|
|
|
// A VGM 1.71 file of the commands (then the end command), with the
|
|
// loop at byte loopAt of them (VGM_NO_LOOP for none); rate is its update
|
|
// rate, as Furnace writes it.
|
|
static void vgm(const ToolBufT *commands, uint32_t snClock, uint32_t ayClock, long loopAt, uint32_t rate, ToolBufT *out) {
|
|
uint8_t header[VGM_HEADER];
|
|
|
|
memset(header, 0, sizeof(header));
|
|
memcpy(header, VGM_IDENT, VGM_IDENT_BYTES);
|
|
putLe32(header + VGM_EOF_AT, (uint32_t)(VGM_HEADER + commands->len + 1u - VGM_EOF_AT));
|
|
putLe32(header + VGM_VERSION_AT, VGM_VERSION);
|
|
putLe32(header + VGM_SN_CLOCK_AT, snClock);
|
|
putLe32(header + VGM_RATE_AT, rate);
|
|
header[VGM_SN_FEEDBACK_AT] = VGM_SN_FEEDBACK & BYTE_MASK;
|
|
header[VGM_SN_FEEDBACK_AT + 1u] = VGM_SN_FEEDBACK >> BYTE_BITS;
|
|
header[VGM_SN_WIDTH_AT] = VGM_SN_WIDTH;
|
|
putLe32(header + VGM_DATA_AT, VGM_HEADER - VGM_DATA_AT);
|
|
putLe32(header + VGM_AY_CLOCK_AT, ayClock);
|
|
if (loopAt != VGM_NO_LOOP) {
|
|
putLe32(header + VGM_LOOP_AT, (uint32_t)(VGM_HEADER + (size_t)loopAt - VGM_LOOP_AT));
|
|
}
|
|
out->len = 0u;
|
|
toolBufAdd(out, header, sizeof(header));
|
|
toolBufAdd(out, commands->data, commands->len);
|
|
toolBufByte(out, CMD_END);
|
|
}
|
|
|
|
|
|
// A wait of samples (0x61).
|
|
static void vgmWait(ToolBufT *commands, uint32_t samples) {
|
|
toolBufByte(commands, CMD_WAIT);
|
|
toolBufByte(commands, samples & BYTE_MASK);
|
|
toolBufByte(commands, samples >> BYTE_BITS);
|
|
}
|
|
|
|
|
|
// count one-byte commands in a row.
|
|
static void vgmWaits(ToolBufT *commands, uint32_t command, uint32_t count) {
|
|
uint32_t i;
|
|
|
|
for (i = 0u; i < count; i++) {
|
|
toolBufByte(commands, command);
|
|
}
|
|
}
|
|
|
|
|
|
// Both silent, or the same value at the same attenuation.
|
|
static bool voiceSame(VoiceT a, VoiceT b) {
|
|
if (a.on != b.on) {
|
|
return false;
|
|
}
|
|
return !a.on || (a.value == b.value && a.atten == b.atten);
|
|
}
|
|
|
|
|
|
static void voiceText(ToolBufT *out, VoiceT voice) {
|
|
if (!voice.on) {
|
|
gateTextAdd(out, "off");
|
|
return;
|
|
}
|
|
gateTextAdd(out, "(%u, %u)", (unsigned)voice.value, (unsigned)voice.atten);
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
toolFileName(TOOL_NAME);
|
|
if (argc == MODE_ARGS && strcmp(argv[MODE_ARG], "synthetic") == 0) {
|
|
return synthetic(argv[FIRST_ARG], argv[FIRST_ARG + 1]);
|
|
}
|
|
if (argc == MODE_ARGS && strcmp(argv[MODE_ARG], "pitch") == 0) {
|
|
return pitch(argv[FIRST_ARG], argv[FIRST_ARG + 1]);
|
|
}
|
|
fputs(kUsage, stderr);
|
|
return GATE_USAGE_EXIT;
|
|
}
|