diff options
author | Michael Pavone <pavone@retrodev.com> | 2020-03-27 00:03:58 -0700 |
---|---|---|
committer | Michael Pavone <pavone@retrodev.com> | 2020-03-27 00:03:58 -0700 |
commit | 66975faa75958a1a509a04801331a85291827580 (patch) | |
tree | ef9891c95f5d100b464b2497b64e08db6ad8ab31 | |
parent | ce15f59d46ea29afc64a8018e7da089c81217dea (diff) |
Initial stab at VGM logging support
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | bindings.c | 77 | ||||
-rw-r--r-- | default.cfg | 5 | ||||
-rw-r--r-- | genesis.c | 29 | ||||
-rw-r--r-- | psg.c | 27 | ||||
-rw-r--r-- | psg.h | 3 | ||||
-rw-r--r-- | system.h | 3 | ||||
-rw-r--r-- | vgm.c | 117 | ||||
-rw-r--r-- | vgm.h | 19 | ||||
-rw-r--r-- | ym2612.c | 19 | ||||
-rw-r--r-- | ym2612.h | 3 |
11 files changed, 274 insertions, 30 deletions
@@ -194,7 +194,7 @@ TRANSOBJS+= gen_x86.o backend_x86.o 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 @@ -36,6 +36,7 @@ typedef enum { UI_RELOAD, UI_SMS_PAUSE, UI_SCREENSHOT, + UI_VGM_LOG, UI_EXIT, UI_PLANE_DEBUG, UI_VRAM_DEBUG, @@ -258,6 +259,39 @@ static uint8_t mouse_captured; #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 @@ void handle_binding_up(keybinding * binding) 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); + 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 { - template = strdup("blastem_%c.ppm"); + 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); } - 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); - render_save_screenshot(path); } break; - } case UI_EXIT: #ifndef DISABLE_NUKLEAR if (is_nuklear_active()) { @@ -634,6 +651,8 @@ int parse_binding_target(int device_num, char * target, tern_node * padbuttons, *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 --git a/default.cfg b/default.cfg index 47a9abc..8d76e6d 100644 --- a/default.cfg +++ b/default.cfg @@ -22,6 +22,7 @@ bindings { 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 @@ ui { 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 @@ -404,6 +404,9 @@ m68k_context * sync_components(m68k_context * context, uint32_t address) 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 @@ static void config_updated(system_header *system) 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 @@ genesis_context *alloc_init_genesis(rom_info *rom, void *main_rom, void *lock_on 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); @@ -32,6 +32,9 @@ void psg_adjust_master_clock(psg_context * context, uint32_t master_clock) 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_run(psg_context * context, uint32_t cycles) } } +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); @@ -9,9 +9,11 @@ #include <stdint.h> #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_free(psg_context *context); 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); @@ -58,6 +58,8 @@ struct system_header { 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 @@ struct system_header { uint8_t save_state; uint8_t delayed_load_slot; uint8_t has_keyboard; + uint8_t vgm_logging; debugger_type debugger_type; system_type type; }; @@ -0,0 +1,117 @@ +#include <stdlib.h> +#include <string.h> +#include <stddef.h> +#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 @@ -1,6 +1,9 @@ #ifndef VGM_H_ #define VGM_H_ +#include <stdint.h> +#include <stdio.h> + #pragma pack(push, 1) typedef struct { char ident[4]; @@ -71,4 +74,20 @@ typedef struct { 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_ @@ -779,6 +779,19 @@ static uint32_t ym_calc_phase_inc(ym2612_context * context, ym_operator * operat 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 @@ void ym_data_write(ym2612_context * context, uint8_t value) 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); @@ -10,6 +10,7 @@ #include <stdio.h> #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 { 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_run(ym2612_context * context, uint32_t to_cycle); 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); |