Add VGM logging support, closes #564

This commit is contained in:
Leopoldo Pla
2026-06-05 21:47:51 +02:00
parent 208ba4afab
commit 0cc19ba024
10 changed files with 146 additions and 9 deletions
+1
View File
@@ -36,6 +36,7 @@
<items>
<menuItem title="Apple AIFF" state="on" tag="1" id="M3Z-UN-VKZ"/>
<menuItem title="RIFF WAVE" tag="2" id="zA0-Np-4XD"/>
<menuItem title="VGM log" tag="3" id="Yme-gB-L9v"/>
<menuItem title="Raw PCM (Stereo 96KHz, 16-bit LE)" id="r9J-4k-XH5"/>
</items>
</menu>
+3
View File
@@ -2888,6 +2888,9 @@ enum GBWindowResizeAction
case GB_AUDIO_FORMAT_WAV:
_audioSavePanel.allowedFileTypes = @[@"wav"];
break;
case GB_AUDIO_FORMAT_VGM:
_audioSavePanel.allowedFileTypes = @[@"vgm"];
break;
}
}
+114 -4
View File
@@ -411,7 +411,7 @@ static void render(GB_gameboy_t *gb)
}
assert(gb->apu_output.sample_callback);
gb->apu_output.sample_callback(gb, &filtered_output);
if (unlikely(gb->apu_output.output_file)) {
if (unlikely(gb->apu_output.output_file && gb->apu_output.output_format != GB_AUDIO_FORMAT_VGM)) {
#ifdef GB_BIG_ENDIAN
if (gb->apu_output.output_format == GB_AUDIO_FORMAT_WAV) {
filtered_output.left = LE16(filtered_output.left);
@@ -2261,10 +2261,86 @@ typedef struct __attribute__((packed)) {
uint32_t data_size; // = LE32(length of samples)
} wav_header_t;
#define VGM_HEADER_SIZE 0x100
#define VGM_DATA_OFFSET (VGM_HEADER_SIZE - 0x34)
#define VGM_SAMPLE_RATE 44100
static void vgm_set_le32(uint8_t *header, size_t offset, uint32_t value)
{
value = LE32(value);
memcpy(header + offset, &value, sizeof(value));
}
static void vgm_get_header(GB_gameboy_t *gb, uint8_t header[VGM_HEADER_SIZE], uint32_t eof_offset)
{
memset(header, 0, VGM_HEADER_SIZE);
header[0] = 'V';
header[1] = 'g';
header[2] = 'm';
header[3] = ' ';
vgm_set_le32(header, 0x04, eof_offset);
vgm_set_le32(header, 0x08, 0x00000161);
vgm_set_le32(header, 0x18, gb->apu_output.vgm_total_samples);
vgm_set_le32(header, 0x34, VGM_DATA_OFFSET);
vgm_set_le32(header, 0x80, gb->apu_output.vgm_clock_rate);
}
static bool vgm_write_data(GB_gameboy_t *gb, const void *data, size_t size)
{
if (!gb->apu_output.output_file) return false;
if (fwrite(data, size, 1, gb->apu_output.output_file) != 1) {
gb->apu_output.output_error = errno ?: EIO;
fclose(gb->apu_output.output_file);
gb->apu_output.output_file = NULL;
return false;
}
return true;
}
static bool vgm_flush_wait(GB_gameboy_t *gb)
{
while (gb->apu_output.vgm_pending_samples) {
if (gb->apu_output.vgm_pending_samples <= 16) {
uint8_t command = 0x70 + gb->apu_output.vgm_pending_samples - 1;
gb->apu_output.vgm_pending_samples = 0;
if (!vgm_write_data(gb, &command, sizeof(command))) return false;
}
else {
uint16_t samples = MIN(gb->apu_output.vgm_pending_samples, 0xFFFF);
uint8_t command[] = {0x61, samples & 0xFF, samples >> 8};
gb->apu_output.vgm_pending_samples -= samples;
if (!vgm_write_data(gb, command, sizeof(command))) return false;
}
}
return true;
}
void GB_apu_vgm_advance(GB_gameboy_t *gb, uint8_t cycles)
{
if (unlikely(gb->apu_output.output_file && gb->apu_output.output_format == GB_AUDIO_FORMAT_VGM)) {
uint64_t clock_rate = (uint64_t)gb->apu_output.vgm_clock_rate * 2;
if (!clock_rate) return;
uint64_t sample_fraction = gb->apu_output.vgm_sample_fraction + (uint64_t)cycles * VGM_SAMPLE_RATE;
uint32_t samples = sample_fraction / clock_rate;
gb->apu_output.vgm_sample_fraction = sample_fraction % clock_rate;
gb->apu_output.vgm_pending_samples += samples;
gb->apu_output.vgm_total_samples += samples;
}
}
void GB_apu_vgm_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value)
{
if (unlikely(gb->apu_output.output_file && gb->apu_output.output_format == GB_AUDIO_FORMAT_VGM)) {
if (!vgm_flush_wait(gb)) return;
uint8_t command[] = {0xB3, reg - GB_IO_NR10, value};
vgm_write_data(gb, command, sizeof(command));
}
}
int GB_start_audio_recording(GB_gameboy_t *gb, const char *path, GB_audio_format_t format)
{
if (gb->apu_output.sample_rate == 0) {
if (format != GB_AUDIO_FORMAT_VGM && gb->apu_output.sample_rate == 0) {
return EINVAL;
}
@@ -2278,6 +2354,21 @@ int GB_start_audio_recording(GB_gameboy_t *gb, const char *path, GB_audio_format
switch (format) {
case GB_AUDIO_FORMAT_RAW:
return 0;
case GB_AUDIO_FORMAT_VGM: {
gb->apu_output.vgm_pending_samples = 0;
gb->apu_output.vgm_total_samples = 0;
gb->apu_output.vgm_sample_fraction = 0;
gb->apu_output.vgm_clock_rate = GB_get_clock_rate(gb) ?: CPU_FREQUENCY;
uint8_t header[VGM_HEADER_SIZE];
vgm_get_header(gb, header, 0);
if (fwrite(header, sizeof(header), 1, gb->apu_output.output_file) != 1) {
int ret = errno ?: EIO;
fclose(gb->apu_output.output_file);
gb->apu_output.output_file = NULL;
return ret;
}
return 0;
}
case GB_AUDIO_FORMAT_AIFF: {
aiff_header_t header = {0,};
if (fwrite(&header, sizeof(header), 1, gb->apu_output.output_file) != 1) {
@@ -2315,6 +2406,23 @@ int GB_stop_audio_recording(GB_gameboy_t *gb)
switch (gb->apu_output.output_format) {
case GB_AUDIO_FORMAT_RAW:
break;
case GB_AUDIO_FORMAT_VGM: {
if (!vgm_flush_wait(gb)) break;
uint8_t end_command = 0x66;
if (!vgm_write_data(gb, &end_command, sizeof(end_command))) break;
long file_size = ftell(gb->apu_output.output_file);
if (file_size < 0) {
gb->apu_output.output_error = errno ?: EIO;
break;
}
uint8_t header[VGM_HEADER_SIZE];
vgm_get_header(gb, header, file_size - 4);
if (fseek(gb->apu_output.output_file, 0, SEEK_SET) != 0 ||
fwrite(header, sizeof(header), 1, gb->apu_output.output_file) != 1) {
gb->apu_output.output_error = errno ?: EIO;
}
break;
}
case GB_AUDIO_FORMAT_AIFF: {
size_t file_size = ftell(gb->apu_output.output_file);
size_t frames = (file_size - sizeof(aiff_header_t)) / sizeof(GB_sample_t);
@@ -2387,8 +2495,10 @@ int GB_stop_audio_recording(GB_gameboy_t *gb)
break;
}
}
fclose(gb->apu_output.output_file);
gb->apu_output.output_file = NULL;
if (gb->apu_output.output_file) {
fclose(gb->apu_output.output_file);
gb->apu_output.output_file = NULL;
}
int ret = gb->apu_output.output_error;
gb->apu_output.output_error = 0;
+7
View File
@@ -173,6 +173,7 @@ typedef enum {
GB_AUDIO_FORMAT_RAW, // Native endian
GB_AUDIO_FORMAT_AIFF, // Native endian
GB_AUDIO_FORMAT_WAV,
GB_AUDIO_FORMAT_VGM,
} GB_audio_format_t;
typedef struct {
@@ -212,6 +213,10 @@ typedef struct {
FILE *output_file;
GB_audio_format_t output_format;
int output_error;
uint32_t vgm_pending_samples;
uint32_t vgm_total_samples;
uint64_t vgm_sample_fraction;
uint32_t vgm_clock_rate;
/* Not output related, but it's temp state so I'll put it here */
bool square_sweep_disable_stepping;
@@ -241,4 +246,6 @@ internal void GB_apu_div_secondary_event(GB_gameboy_t *gb);
internal void GB_apu_delayed_envelope_tick(GB_gameboy_t *gb);
internal void GB_apu_init(GB_gameboy_t *gb);
internal void GB_apu_run(GB_gameboy_t *gb, bool force);
internal void GB_apu_vgm_advance(GB_gameboy_t *gb, uint8_t cycles);
internal void GB_apu_vgm_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value);
#endif
+1
View File
@@ -1767,6 +1767,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
default:
if ((addr & 0xFF) >= GB_IO_NR10 && (addr & 0xFF) <= GB_IO_WAV_END) {
GB_apu_vgm_write(gb, addr & 0xFF, value);
GB_apu_write(gb, addr & 0xFF, value);
return;
}
+1
View File
@@ -494,6 +494,7 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
}
gb->cycles_since_last_sync += cycles;
gb->cycles_since_run += cycles;
GB_apu_vgm_advance(gb, cycles);
gb->rumble_on_cycles += gb->rumble_strength & 3;
gb->rumble_off_cycles += (gb->rumble_strength & 3) ^ 3;
+4 -1
View File
@@ -2310,6 +2310,9 @@ static void toggle_audio_recording(unsigned index)
else if (strcasecmp(".wav", filename + length - 4) == 0) {
format = GB_AUDIO_FORMAT_WAV;
}
else if (strcasecmp(".vgm", filename + length - 4) == 0) {
format = GB_AUDIO_FORMAT_VGM;
}
}
}
@@ -2317,7 +2320,7 @@ static void toggle_audio_recording(unsigned index)
free(filename);
if (error) {
char *message = NULL;
asprintf(&message, "Could not finalize recording: %s", strerror(error));
asprintf(&message, "Could not start recording: %s", strerror(error));
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window);
free(message);
return;
+1 -1
View File
@@ -52,7 +52,7 @@ char *do_save_recording_dialog(unsigned frequency)
NSWindow *key = [NSApp keyWindow];
NSSavePanel *dialog = [NSSavePanel savePanel];
dialog.title = @"Audio recording save location";
dialog.allowedFileTypes = @[@"aiff", @"aif", @"aifc", @"wav", @"raw", @"pcm"];
dialog.allowedFileTypes = @[@"aiff", @"aif", @"aifc", @"wav", @"vgm", @"raw", @"pcm"];
if ([dialog runModal] != NSModalResponseOK) return nil;
[key makeKeyAndOrderFront:nil];
NSString *ret = [[dialog URL] path];
+9 -1
View File
@@ -255,6 +255,9 @@ static void filter_changed(void *dialog,
case 'I':
asprintf(&new_filename, "%s.wav", basename);
break;
case 'G':
asprintf(&new_filename, "%s.vgm", basename);
break;
case 'a':
asprintf(&new_filename, "%s.raw", basename);
break;
@@ -322,12 +325,17 @@ char *do_save_recording_dialog(unsigned frequency)
gtk_file_filter_add_pattern(filter, "*.aifc");
gtk_file_filter_set_name(filter, "Apple AIFF");
gtk_file_chooser_add_filter(dialog, filter);
filter = gtk_file_filter_new();
gtk_file_filter_add_pattern(filter, "*.wav");
gtk_file_filter_set_name(filter, "RIFF WAVE");
gtk_file_chooser_add_filter(dialog, filter);
filter = gtk_file_filter_new();
gtk_file_filter_add_pattern(filter, "*.vgm");
gtk_file_filter_set_name(filter, "VGM log");
gtk_file_chooser_add_filter(dialog, filter);
filter = gtk_file_filter_new();
gtk_file_filter_add_pattern(filter, "*.raw");
gtk_file_filter_add_pattern(filter, "*.pcm");
+5 -2
View File
@@ -83,7 +83,7 @@ char *do_save_recording_dialog(unsigned frequency)
{
OPENFILENAMEW dialog;
wchar_t filename[MAX_PATH + 5] = L"recording.wav";
static wchar_t filter[] = L"RIFF WAVE\0*.wav\0Apple AIFF\0*.aiff;*.aif;*.aifc\0Raw PCM (Stereo \u200b\u200b\u200b\u200b\u200b\u200b\u200bHz, 16-bit LE)\0*.raw;*.pcm;\0All files\0*.*\0\0";
static wchar_t filter[] = L"RIFF WAVE\0*.wav\0Apple AIFF\0*.aiff;*.aif;*.aifc\0VGM log\0*.vgm\0Raw PCM (Stereo \u200b\u200b\u200b\u200b\u200b\u200b\u200bHz, 16-bit LE)\0*.raw;*.pcm;\0All files\0*.*\0\0";
memset(&dialog, 0, sizeof(dialog));
dialog.lStructSize = sizeof(dialog);
@@ -93,7 +93,7 @@ char *do_save_recording_dialog(unsigned frequency)
wchar_t temp[11];
_itow(frequency, temp, 10);
memcpy(filter + sizeof("RIFF WAVE\0*.wav\0Apple AIFF\0*.aiff;*.aif;*.aifc\0Raw PCM (Stereo ") - 1, temp, min(wcslen(temp), 7) * sizeof(wchar_t));
memcpy(filter + sizeof("RIFF WAVE\0*.wav\0Apple AIFF\0*.aiff;*.aif;*.aifc\0VGM log\0*.vgm\0Raw PCM (Stereo ") - 1, temp, min(wcslen(temp), 7) * sizeof(wchar_t));
dialog.nFilterIndex = 1;
dialog.lpstrInitialDir = NULL;
@@ -109,6 +109,9 @@ char *do_save_recording_dialog(unsigned frequency)
wcscat(filename, L".aiff");
break;
case 3:
wcscat(filename, L".vgm");
break;
case 4:
wcscat(filename, L".raw");
break;
}