mirror of
https://github.com/LIJI32/SameBoy.git
synced 2026-10-11 22:19:50 +00:00
Add VGM logging support, closes #564
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user