changeset 1909:508522f08e4d

Initial stab at VGM logging support
author Michael Pavone <pavone@retrodev.com>
date Fri, 27 Mar 2020 00:03:58 -0700
parents c3d49c338224
children ee178f08611b
files Makefile bindings.c default.cfg genesis.c psg.c psg.h system.h vgm.c vgm.h ym2612.c ym2612.h
diffstat 11 files changed, 275 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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")) {
--- 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
--- 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);
--- 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);
--- 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 <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_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);
 
--- 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;
 };
--- /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 <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
--- 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 <stdint.h>
+#include <stdio.h>
+
 #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_
--- 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);
--- 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 <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 {
 	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);