# HG changeset patch # User Michael Pavone # Date 1585292638 25200 # Node ID 508522f08e4dda6310a4f9f61a428d8953f7ffe7 # Parent c3d49c3382249b7a7e3014545380f83a67fa2fc3 Initial stab at VGM logging support diff -r c3d49c338224 -r 508522f08e4d Makefile --- a/Makefile Thu Mar 26 23:53:35 2020 -0700 +++ b/Makefile Fri Mar 27 00:03:58 2020 -0700 @@ -194,7 +194,7 @@ endif endif endif -AUDIOOBJS=ym2612.o psg.o wave.o +AUDIOOBJS=ym2612.o psg.o wave.o vgm.o CONFIGOBJS=config.o tern.o util.o paths.o NUKLEAROBJS=$(FONT) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o RENDEROBJS=ppm.o controller_info.o render_audio.o diff -r c3d49c338224 -r 508522f08e4d bindings.c --- a/bindings.c Thu Mar 26 23:53:35 2020 -0700 +++ b/bindings.c Fri Mar 27 00:03:58 2020 -0700 @@ -36,6 +36,7 @@ UI_RELOAD, UI_SMS_PAUSE, UI_SCREENSHOT, + UI_VGM_LOG, UI_EXIT, UI_PLANE_DEBUG, UI_VRAM_DEBUG, @@ -258,6 +259,39 @@ #define localtime_r(a,b) localtime(a) #endif +char *get_content_config_path(char *config_path, char *config_template, char *default_name) +{ + char *base = tern_find_path(config, config_path, TVAL_PTR).ptrval; + if (!base) { + base = "$HOME"; + } + const system_media *media = current_media(); + tern_node *vars = tern_insert_ptr(NULL, "HOME", get_home_dir()); + vars = tern_insert_ptr(vars, "EXEDIR", get_exe_dir()); + vars = tern_insert_ptr(vars, "USERDATA", (char *)get_userdata_dir()); + vars = tern_insert_ptr(vars, "ROMNAME", media->name); + vars = tern_insert_ptr(vars, "ROMDIR", media->dir); + base = replace_vars(base, vars, 1); + tern_free(vars); + ensure_dir_exists(base); + time_t now = time(NULL); + struct tm local_store; + char fname_part[256]; + char *template = tern_find_path(config, config_template, TVAL_PTR).ptrval; + if (template) { + vars = tern_insert_ptr(NULL, "ROMNAME", media->name); + template = replace_vars(template, vars, 0); + } else { + template = strdup(default_name); + } + strftime(fname_part, sizeof(fname_part), template, localtime_r(&now, &local_store)); + char const *parts[] = {base, PATH_SEP, fname_part}; + char *path = alloc_concat_m(3, parts); + free(base); + free(template); + return path; +} + void handle_binding_up(keybinding * binding) { uint8_t allow_content_binds = content_binds_enabled && current_system; @@ -352,40 +386,23 @@ current_system->gamepad_down(current_system, GAMEPAD_MAIN_UNIT, MAIN_UNIT_PAUSE); } break; - case UI_SCREENSHOT: { + case UI_SCREENSHOT: if (allow_content_binds) { - char *screenshot_base = tern_find_path(config, "ui\0screenshot_path\0", TVAL_PTR).ptrval; - if (!screenshot_base) { - screenshot_base = "$HOME"; - } - const system_media *media = current_media(); - tern_node *vars = tern_insert_ptr(NULL, "HOME", get_home_dir()); - vars = tern_insert_ptr(vars, "EXEDIR", get_exe_dir()); - vars = tern_insert_ptr(vars, "USERDATA", (char *)get_userdata_dir()); - vars = tern_insert_ptr(vars, "ROMNAME", media->name); - vars = tern_insert_ptr(vars, "ROMDIR", media->dir); - screenshot_base = replace_vars(screenshot_base, vars, 1); - tern_free(vars); - ensure_dir_exists(screenshot_base); - time_t now = time(NULL); - struct tm local_store; - char fname_part[256]; - char *template = tern_find_path(config, "ui\0screenshot_template\0", TVAL_PTR).ptrval; - if (template) { - vars = tern_insert_ptr(NULL, "ROMNAME", media->name); - template = replace_vars(template, vars, 0); - } else { - template = strdup("blastem_%c.ppm"); - } - strftime(fname_part, sizeof(fname_part), template, localtime_r(&now, &local_store)); - char const *parts[] = {screenshot_base, PATH_SEP, fname_part}; - char *path = alloc_concat_m(3, parts); - free(screenshot_base); - free(template); + char *path = get_content_config_path("ui\0screenshot_path\0", "ui\0screenshot_template\0", "blastem_%c.ppm"); render_save_screenshot(path); } break; - } + case UI_VGM_LOG: + if (allow_content_binds && current_system->start_vgm_log) { + if (current_system->vgm_logging) { + current_system->stop_vgm_log(current_system); + } else { + char *path = get_content_config_path("ui\0vgm_path\0", "ui\0vgm_template\0", "blastem_%c.vgm"); + current_system->start_vgm_log(current_system, path); + free(path); + } + } + break; case UI_EXIT: #ifndef DISABLE_NUKLEAR if (is_nuklear_active()) { @@ -634,6 +651,8 @@ *subtype_a = UI_SMS_PAUSE; } else if (!strcmp(target + 3, "screenshot")) { *subtype_a = UI_SCREENSHOT; + } else if (!strcmp(target + 3, "vgm_log")) { + *subtype_a = UI_VGM_LOG; } else if(!strcmp(target + 3, "exit")) { *subtype_a = UI_EXIT; } else if (!strcmp(target + 3, "plane_debug")) { diff -r c3d49c338224 -r 508522f08e4d default.cfg --- a/default.cfg Thu Mar 26 23:53:35 2020 -0700 +++ b/default.cfg Fri Mar 27 00:03:58 2020 -0700 @@ -22,6 +22,7 @@ v ui.vram_debug c ui.cram_debug n ui.compositing_debug + m ui.vgm_log esc ui.exit ` ui.save_state 0 ui.set_speed.0 @@ -364,6 +365,10 @@ screenshot_path $HOME #see strftime for the format specifiers valid in screenshot_template screenshot_template blastem_%Y%m%d_%H%M%S.png + #path for storing VGM recordings, accepts the same variables as initial_path + vgm_path $HOME + #see strftime for the format specifiers valid in vgm_template + vgm_template blastem_%Y%m%d_%H%M%S.vgm #path template for saving SRAM, EEPROM and savestates #accepts special variables $HOME, $EXEDIR, $USERDATA, $ROMNAME save_path $USERDATA/blastem/$ROMNAME diff -r c3d49c338224 -r 508522f08e4d genesis.c --- a/genesis.c Thu Mar 26 23:53:35 2020 -0700 +++ b/genesis.c Fri Mar 27 00:03:58 2020 -0700 @@ -404,6 +404,9 @@ context->current_cycle -= deduction; z80_adjust_cycles(z_context, deduction); ym_adjust_cycles(gen->ym, deduction); + if (gen->ym->vgm) { + vgm_adjust_cycles(gen->ym->vgm, deduction); + } gen->psg->cycles -= deduction; if (gen->reset_cycle != CYCLE_NEVER) { gen->reset_cycle -= deduction; @@ -1452,6 +1455,30 @@ set_audio_config(gen); } +static void start_vgm_log(system_header *system, char *filename) +{ + genesis_context *gen = (genesis_context *)system; + vgm_writer *vgm = vgm_write_open(filename, gen->version_reg & HZ50 ? 50 : 60, gen->master_clock, gen->m68k->current_cycle); + if (vgm) { + printf("Started logging VGM to %s\n", filename); + sync_sound(gen, vgm->last_cycle); + ym_vgm_log(gen->ym, gen->master_clock, vgm); + psg_vgm_log(gen->psg, gen->master_clock, vgm); + gen->header.vgm_logging = 1; + } else { + printf("Failed to start logging to %s\n", filename); + } +} + +static void stop_vgm_log(system_header *system) +{ + puts("Stopped VGM log"); + genesis_context *gen = (genesis_context *)system; + vgm_close(gen->ym->vgm); + gen->ym->vgm = gen->psg->vgm = NULL; + gen->header.vgm_logging = 0; +} + genesis_context *alloc_init_genesis(rom_info *rom, void *main_rom, void *lock_on, uint32_t system_opts, uint8_t force_region) { static memmap_chunk z80_map[] = { @@ -1484,6 +1511,8 @@ gen->header.config_updated = config_updated; gen->header.serialize = serialize; gen->header.deserialize = deserialize; + gen->header.start_vgm_log = start_vgm_log; + gen->header.stop_vgm_log = stop_vgm_log; gen->header.type = SYSTEM_GENESIS; gen->header.info = *rom; set_region(gen, rom, force_region); diff -r c3d49c338224 -r 508522f08e4d psg.c --- a/psg.c Thu Mar 26 23:53:35 2020 -0700 +++ b/psg.c Fri Mar 27 00:03:58 2020 -0700 @@ -32,6 +32,9 @@ void psg_write(psg_context * context, uint8_t value) { + if (context->vgm) { + vgm_sn76489_write(context->vgm, context->cycles, value); + } if (value & 0x80) { context->latch = value & 0x70; uint8_t channel = value >> 5 & 0x3; @@ -122,6 +125,30 @@ } } +void psg_vgm_log(psg_context *context, uint32_t master_clock, vgm_writer *vgm) +{ + vgm_sn76489_init(vgm, master_clock / context->clock_inc, 9, 16, 0); + context->vgm = vgm; + for (int chan = 0; chan < 4; chan++) + { + uint8_t base = chan << 5 | 0x80; + vgm_sn76489_write(context->vgm, context->cycles, context->volume[chan] | base | 0x10); + if (chan == 3) { + if (context->noise_use_tone) { + vgm_sn76489_write(context->vgm, context->cycles, 3 | base); + } else { + //0x10 = 0 + //0x20 = 1 + //0x40 = 2 + vgm_sn76489_write(context->vgm, context->cycles, context->counter_load[chan] >> 5 | base); + } + } else { + vgm_sn76489_write(context->vgm, context->cycles, (context->counter_load[chan] & 0xF) | base); + vgm_sn76489_write(context->vgm, context->cycles, context->counter_load[chan] >> 4 & 0x3F); + } + } +} + void psg_serialize(psg_context *context, serialize_buffer *buf) { save_int16(buf, context->lsfr); diff -r c3d49c338224 -r 508522f08e4d psg.h --- a/psg.h Thu Mar 26 23:53:35 2020 -0700 +++ b/psg.h Fri Mar 27 00:03:58 2020 -0700 @@ -9,9 +9,11 @@ #include #include "serialize.h" #include "render_audio.h" +#include "vgm.h" typedef struct { audio_source *audio; + vgm_writer *vgm; uint32_t clock_inc; uint32_t cycles; uint16_t lsfr; @@ -31,6 +33,7 @@ void psg_adjust_master_clock(psg_context * context, uint32_t master_clock); void psg_write(psg_context * context, uint8_t value); void psg_run(psg_context * context, uint32_t cycles); +void psg_vgm_log(psg_context *context, uint32_t master_clock, vgm_writer *vgm); void psg_serialize(psg_context *context, serialize_buffer *buf); void psg_deserialize(deserialize_buffer *buf, void *vcontext); diff -r c3d49c338224 -r 508522f08e4d system.h --- a/system.h Thu Mar 26 23:53:35 2020 -0700 +++ b/system.h Fri Mar 27 00:03:58 2020 -0700 @@ -58,6 +58,8 @@ system_fun config_updated; system_ptrszt_fun_rptr8 serialize; system_ptr8_sizet_fun deserialize; + system_str_fun start_vgm_log; + system_fun stop_vgm_log; rom_info info; arena *arena; char *next_rom; @@ -67,6 +69,7 @@ uint8_t save_state; uint8_t delayed_load_slot; uint8_t has_keyboard; + uint8_t vgm_logging; debugger_type debugger_type; system_type type; }; diff -r c3d49c338224 -r 508522f08e4d vgm.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vgm.c Fri Mar 27 00:03:58 2020 -0700 @@ -0,0 +1,117 @@ +#include +#include +#include +#include "vgm.h" + +vgm_writer *vgm_write_open(char *filename, uint32_t rate, uint32_t clock, uint32_t cycle) +{ + FILE *f = fopen(filename, "wb"); + if (!f) { + return NULL; + } + vgm_writer *writer = calloc(sizeof(vgm_writer), 1); + memcpy(writer->header.ident, "Vgm ", 4); + writer->header.version = 0x150; + writer->header.data_offset = sizeof(writer->header) - offsetof(vgm_header, data_offset); + writer->header.rate = rate; + writer->f = f; + if (1 != fwrite(&writer->header, sizeof(writer->header), 1, f)) { + free(writer); + fclose(f); + return NULL; + } + writer->master_clock = clock; + writer->last_cycle = cycle; + + return writer; +} + +void vgm_sn76489_init(vgm_writer *writer, uint32_t clock, uint16_t feedback, uint8_t shift_reg_size, uint8_t flags) +{ + if (flags && writer->header.version < 0x151) { + writer->header.version = 0x151; + } + writer->header.sn76489_clk = clock, + writer->header.sn76489_fb = feedback; + writer->header.sn76489_shift = shift_reg_size; + writer->header.sn76489_flags = flags; +} + +static void wait_commands(vgm_writer *writer, uint32_t delta) +{ + if (!delta) { + return; + } + if (delta <= 0x10) { + fputc(CMD_WAIT_SHORT + (delta - 1), writer->f); + } else if (delta >= 735 && delta <= (735 + 0x10)) { + fputc(CMD_WAIT_60, writer->f); + wait_commands(writer, delta - 735); + } else if (delta >= 882 && delta <= (882 + 0x10)) { + fputc(CMD_WAIT_50, writer->f); + wait_commands(writer, delta - 882); + } else if (delta > 0xFFFF) { + uint8_t cmd[3] = {CMD_WAIT, 0xFF, 0xFF}; + fwrite(cmd, 1, sizeof(cmd), writer->f); + wait_commands(writer, delta - 0xFFFF); + } else { + uint8_t cmd[3] = {CMD_WAIT, delta, delta >> 8}; + fwrite(cmd, 1, sizeof(cmd), writer->f); + } +} + +static void add_wait(vgm_writer *writer, uint32_t cycle) +{ + uint64_t delta = cycle - writer->last_cycle; + delta *= (uint64_t)44100; + delta /= (uint64_t)writer->master_clock; + + uint32_t mclks_per_sample = writer->master_clock / 44100; + writer->last_cycle += delta * mclks_per_sample; + writer->header.num_samples += delta; + wait_commands(writer, delta); +} + +void vgm_sn76489_write(vgm_writer *writer, uint32_t cycle, uint8_t value) +{ + add_wait(writer, cycle); + uint8_t cmd[2] = {CMD_PSG, value}; + fwrite(cmd, 1, sizeof(cmd), writer->f); +} + +void vgm_ym2612_init(vgm_writer *writer, uint32_t clock) +{ + writer->header.ym2612_clk = clock; +} + +void vgm_ym2612_part1_write(vgm_writer *writer, uint32_t cycle, uint8_t reg, uint8_t value) +{ + add_wait(writer, cycle); + uint8_t cmd[3] = {CMD_YM2612_0, reg, value}; + fwrite(cmd, 1, sizeof(cmd), writer->f); +} + +void vgm_ym2612_part2_write(vgm_writer *writer, uint32_t cycle, uint8_t reg, uint8_t value) +{ + add_wait(writer, cycle); + uint8_t cmd[3] = {CMD_YM2612_1, reg, value}; + fwrite(cmd, 1, sizeof(cmd), writer->f); +} + +void vgm_adjust_cycles(vgm_writer *writer, uint32_t deduction) +{ + if (deduction > writer->last_cycle) { + writer->last_cycle = 0; + } else { + writer->last_cycle -= deduction; + } +} + +void vgm_close(vgm_writer *writer) +{ + writer->header.eof_offset = ftell(writer->f) - offsetof(vgm_header, eof_offset); + fseek(writer->f, SEEK_SET, 0); + fwrite(&writer->header, sizeof(writer->header), 1, writer->f); + fclose(writer->f); + free(writer); +} \ No newline at end of file diff -r c3d49c338224 -r 508522f08e4d vgm.h --- a/vgm.h Thu Mar 26 23:53:35 2020 -0700 +++ b/vgm.h Fri Mar 27 00:03:58 2020 -0700 @@ -1,6 +1,9 @@ #ifndef VGM_H_ #define VGM_H_ +#include +#include + #pragma pack(push, 1) typedef struct { char ident[4]; @@ -71,4 +74,20 @@ uint8_t type; } data_block; +typedef struct { + vgm_header header; + FILE *f; + uint32_t master_clock; + uint32_t last_cycle; +} vgm_writer; + +vgm_writer *vgm_write_open(char *filename, uint32_t rate, uint32_t clock, uint32_t cycle); +void vgm_sn76489_init(vgm_writer *writer, uint32_t clock, uint16_t feedback, uint8_t shift_reg_size, uint8_t flags); +void vgm_sn76489_write(vgm_writer *writer, uint32_t cycle, uint8_t value); +void vgm_ym2612_init(vgm_writer *writer, uint32_t clock); +void vgm_ym2612_part1_write(vgm_writer *writer, uint32_t cycle, uint8_t reg, uint8_t value); +void vgm_ym2612_part2_write(vgm_writer *writer, uint32_t cycle, uint8_t reg, uint8_t value); +void vgm_adjust_cycles(vgm_writer *writer, uint32_t deduction); +void vgm_close(vgm_writer *writer); + #endif //VGM_H_ diff -r c3d49c338224 -r 508522f08e4d ym2612.c --- a/ym2612.c Thu Mar 26 23:53:35 2020 -0700 +++ b/ym2612.c Fri Mar 27 00:03:58 2020 -0700 @@ -779,6 +779,19 @@ return inc; } +void ym_vgm_log(ym2612_context *context, uint32_t master_clock, vgm_writer *vgm) +{ + vgm_ym2612_init(vgm, 6 * master_clock / context->clock_inc); + context->vgm = vgm; + for (uint8_t reg = YM_PART1_START; reg < YM_REG_END; reg++) { + vgm_ym2612_part1_write(context->vgm, context->current_cycle, reg, context->part1_regs[reg - YM_PART1_START]); + } + + for (uint8_t reg = YM_PART2_START; reg < YM_REG_END; reg++) { + vgm_ym2612_part2_write(context->vgm, context->current_cycle, reg, context->part2_regs[reg - YM_PART2_START]); + } +} + void ym_data_write(ym2612_context * context, uint8_t value) { context->write_cycle = context->current_cycle; @@ -791,11 +804,17 @@ if (context->selected_reg < YM_PART2_START) { return; } + if (context->vgm) { + vgm_ym2612_part2_write(context->vgm, context->current_cycle, context->selected_reg, value); + } context->part2_regs[context->selected_reg - YM_PART2_START] = value; } else { if (context->selected_reg < YM_PART1_START) { return; } + if (context->vgm) { + vgm_ym2612_part1_write(context->vgm, context->current_cycle, context->selected_reg, value); + } context->part1_regs[context->selected_reg - YM_PART1_START] = value; } dfprintf(debug_file, "write of %X to reg %X in part %d\n", value, context->selected_reg, context->selected_part+1); diff -r c3d49c338224 -r 508522f08e4d ym2612.h --- a/ym2612.h Thu Mar 26 23:53:35 2020 -0700 +++ b/ym2612.h Fri Mar 27 00:03:58 2020 -0700 @@ -10,6 +10,7 @@ #include #include "serialize.h" #include "render_audio.h" +#include "vgm.h" #define NUM_PART_REGS (0xB7-0x30) #define NUM_CHANNELS 6 @@ -68,6 +69,7 @@ typedef struct { audio_source *audio; + vgm_writer *vgm; uint32_t clock_inc; uint32_t current_cycle; uint32_t write_cycle; @@ -144,6 +146,7 @@ void ym_address_write_part1(ym2612_context * context, uint8_t address); void ym_address_write_part2(ym2612_context * context, uint8_t address); void ym_data_write(ym2612_context * context, uint8_t value); +void ym_vgm_log(ym2612_context *context, uint32_t master_clock, vgm_writer *vgm); uint8_t ym_read_status(ym2612_context * context, uint32_t cycle, uint32_t port); uint8_t ym_load_gst(ym2612_context * context, FILE * gstfile); uint8_t ym_save_gst(ym2612_context * context, FILE * gstfile);