changeset 2528:90a40be940f7

Implement read-only SC-3000 cassette support
author Michael Pavone <pavone@retrodev.com>
date Mon, 25 Nov 2024 22:26:45 -0800
parents 25e40370e0e4
children a6687a6fb69d
files bindings.c config.c default.cfg nuklear_ui/blastem_nuklear.c sms.c sms.h system.h
diffstat 7 files changed, 204 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/bindings.c	Sat Oct 26 14:31:21 2024 -0700
+++ b/bindings.c	Mon Nov 25 22:26:45 2024 -0800
@@ -19,7 +19,8 @@
 	BIND_NONE,
 	BIND_UI,
 	BIND_GAMEPAD,
-	BIND_MOUSE
+	BIND_MOUSE,
+	BIND_CASSETTE
 };
 
 typedef enum {
@@ -314,6 +315,11 @@
 			current_system->mouse_up(current_system, binding->subtype_a, binding->subtype_b);
 		}
 		break;
+	case BIND_CASSETTE:
+		if (allow_content_binds && current_system->cassette_action) {
+			current_system->cassette_action(current_system, binding->subtype_a);
+		}
+		break;
 	case BIND_UI:
 		switch (binding->subtype_a)
 		{
@@ -649,6 +655,20 @@
 		} else {
 			warning("Gamepad mapping string '%s' refers to an invalid mouse number %c\n", target, target[mouselen]);
 		}
+	} else if (startswith(target, "cassette.")) {
+		if (!strcmp(target + 9, "play")) {
+			*subtype_a = CASSETTE_PLAY;
+		} else if (!strcmp(target + 9, "record")) {
+			*subtype_a = CASSETTE_RECORD;
+		} else if (!strcmp(target + 9, "stop")) {
+			*subtype_a = CASSETTE_STOP;
+		} else if (!strcmp(target + 9, "rewind")) {
+			*subtype_a = CASSETTE_REWIND;
+		} else {
+			warning("Cassette mapping string '%s' refers to an invalid action\n", target);
+			return BIND_NONE;
+		}
+		return BIND_CASSETTE;
 	} else if(startswith(target, "ui.")) {
 		if (!strcmp(target + 3, "vdp_debug_mode")) {
 			*subtype_a = UI_DEBUG_MODE_INC;
--- a/config.c	Sat Oct 26 14:31:21 2024 -0700
+++ b/config.c	Mon Nov 25 22:26:45 2024 -0800
@@ -316,7 +316,7 @@
 	*pads = tern_insert_node(*pads, key, val.ptrval);
 }
 
-#define CONFIG_VERSION 10
+#define CONFIG_VERSION 11
 static tern_node *migrate_config(tern_node *config, int from_version)
 {
 	tern_node *def_config = parse_bundled_config("default.cfg");
@@ -517,6 +517,22 @@
 		config = tern_insert_path(config, "ui\0extensions\0", (tern_val){.ptrval = combined}, TVAL_PTR);
 		break;
 	}
+	case 10: {
+		//Add default bindings for cassette actions
+		char *bind = tern_find_path(config, "bindings\0keys\0f2\0", TVAL_PTR).ptrval;
+		if (!bind) {
+			config = tern_insert_path(config, "bindings\0keys\0f2\0", (tern_val){.ptrval = strdup("cassette.play")}, TVAL_PTR);
+		}
+		bind = tern_find_path(config, "bindings\0keys\0f3\0", TVAL_PTR).ptrval;
+		if (!bind) {
+			config = tern_insert_path(config, "bindings\0keys\0f3\0", (tern_val){.ptrval = strdup("cassette.stop")}, TVAL_PTR);
+		}
+		bind = tern_find_path(config, "bindings\0keys\0f4\0", TVAL_PTR).ptrval;
+		if (!bind) {
+			config = tern_insert_path(config, "bindings\0keys\0f4\0", (tern_val){.ptrval = strdup("cassette.rewind")}, TVAL_PTR);
+		}
+		break;
+	}
 	}
 	char buffer[16];
 	sprintf(buffer, "%d", CONFIG_VERSION);
--- a/default.cfg	Sat Oct 26 14:31:21 2024 -0700
+++ b/default.cfg	Mon Nov 25 22:26:45 2024 -0800
@@ -42,6 +42,9 @@
 		f5 ui.reload
 		z ui.sms_pause
 		rctrl ui.toggle_keyboard_captured
+		f2 cassette.play
+		f3 cassette.stop
+		f4 cassette.rewind
 	}
 	pads {
 		default {
@@ -437,4 +440,4 @@
 }
 
 #Don't manually edit `version`, it's used for automatic config migration
-version 10
+version 11
--- a/nuklear_ui/blastem_nuklear.c	Sat Oct 26 14:31:21 2024 -0700
+++ b/nuklear_ui/blastem_nuklear.c	Mon Nov 25 22:26:45 2024 -0800
@@ -524,11 +524,13 @@
 	};
 	static const char *general_binds[] = {
 		"ui.menu", "ui.save_state", "ui.load_state", "ui.toggle_fullscreen", "ui.soft_reset", "ui.reload",
-		"ui.screenshot", "ui.vgm_log", "ui.sms_pause", "ui.toggle_keyboard_captured", "ui.release_mouse", "ui.exit"
+		"ui.screenshot", "ui.vgm_log", "ui.sms_pause", "ui.toggle_keyboard_captured", "ui.release_mouse", "ui.exit",
+		"cassette.play", "cassette.stop", "cassette.rewind"
 	};
 	static const char *general_names[] = {
 		"Show Menu", "Quick Save", "Quick Load", "Toggle Fullscreen", "Soft Reset", "Reload Media",
-		"Internal Screenshot", "Toggle VGM Log", "SMS Pause", "Capture Keyboard", "Release Mouse", "Exit"
+		"Internal Screenshot", "Toggle VGM Log", "SMS Pause", "Capture Keyboard", "Release Mouse", "Exit",
+		"Cassette Play", "Cassette Stop", "Cassette Rewind"
 	};
 	static const char *speed_binds[] = {
 		"ui.next_speed", "ui.prev_speed",
@@ -697,6 +699,9 @@
 		conf_names = tern_insert_ptr(conf_names, "ui.reload", "Reload ROM");
 		conf_names = tern_insert_ptr(conf_names, "ui.sms_pause", "SMS Pause");
 		conf_names = tern_insert_ptr(conf_names, "ui.toggle_keyboard_captured", "Toggle Keyboard Capture");
+		conf_names = tern_insert_ptr(conf_names, "cassette.play", "Cassette Play");
+		conf_names = tern_insert_ptr(conf_names, "cassette.stop", "Cassette Stop");
+		conf_names = tern_insert_ptr(conf_names, "cassette.rewind", "Cassette Rewind");
 	}
 	return tern_find_ptr_default(conf_names, option, (void *)option);
 }
@@ -749,7 +754,10 @@
 		"ui.screenshot",
 		"ui.exit",
 		"ui.release_mouse",
-		"ui.toggle_keyboard_captured"
+		"ui.toggle_keyboard_captured",
+		"cassette.play",
+		"cassette.stop",
+		"cassette.rewind",
 	};
 	static const char *debugger[] = {
 		"ui.vdp_debug_mode",
--- a/sms.c	Sat Oct 26 14:31:21 2024 -0700
+++ b/sms.c	Mon Nov 25 22:26:45 2024 -0800
@@ -19,6 +19,13 @@
 #define Z80_OPTS options
 #endif
 
+enum {
+	TAPE_NONE,
+	TAPE_STOPPED,
+	TAPE_PLAYING,
+	TAPE_RECORDING
+};
+
 static void *memory_io_write(uint32_t location, void *vcontext, uint8_t value)
 {
 	z80_context *z80 = vcontext;
@@ -134,6 +141,94 @@
 	}
 }
 
+static void cassette_run(sms_context *sms, uint32_t cycle)
+{
+	if (!sms->cassette) {
+		return;
+	}
+	if (cycle > sms->cassette_cycle) {
+		uint64_t diff = cycle - sms->cassette_cycle;
+		diff *= sms->cassette_wave.sample_rate;
+		diff /= sms->normal_clock;
+		if (sms->cassette_state == TAPE_PLAYING) {
+			uint64_t bytes_per_sample = sms->cassette_wave.num_channels * sms->cassette_wave.bits_per_sample / 8;
+			uint64_t offset = diff * bytes_per_sample + sms->cassette_offset;
+			if (offset > UINT32_MAX || offset > sms->cassette->size - bytes_per_sample) {
+				sms->cassette_offset = sms->cassette->size - bytes_per_sample;
+			} else {
+				sms->cassette_offset = offset;
+			}
+			static uint32_t last_displayed_seconds;
+			uint32_t seconds = (sms->cassette_offset - (sms->cassette_wave.format_header.size + offsetof(wave_header, audio_format))) / (bytes_per_sample * sms->cassette_wave.sample_rate);
+			if (seconds != last_displayed_seconds) {
+				last_displayed_seconds = seconds;
+				printf("Cassette: %02d:%02d\n", seconds / 60, seconds % 60);
+			}
+		}
+		diff *= sms->normal_clock;
+		diff /= sms->cassette_wave.sample_rate;
+		sms->cassette_cycle += diff;
+	}
+}
+
+static uint8_t cassette_read(sms_context *sms, uint32_t cycle)
+{
+	cassette_run(sms, cycle);
+	if (sms->cassette_state != TAPE_PLAYING) {
+		return 0;
+	}
+	int64_t sample = 0;
+	for (uint16_t i = 0; i < sms->cassette_wave.num_channels; i++)
+	{
+		if (sms->cassette_wave.audio_format == 3) {
+			if (sms->cassette_wave.bits_per_sample == 64) {
+				sample += 32767.0 * ((double *)(((char *)sms->cassette->buffer) + sms->cassette_offset))[i];
+			} else if (sms->cassette_wave.bits_per_sample == 32) {
+				sample += 32767.0f * ((float *)(((char *)sms->cassette->buffer) + sms->cassette_offset))[i];
+			}
+		} else if (sms->cassette_wave.audio_format == 1) {
+			if (sms->cassette_wave.bits_per_sample == 32) {
+				sample += ((int32_t *)(((char *)sms->cassette->buffer) + sms->cassette_offset))[i];
+			} else if (sms->cassette_wave.bits_per_sample == 16) {
+				sample += ((int16_t *)(((char *)sms->cassette->buffer) + sms->cassette_offset))[i];
+			} else if (sms->cassette_wave.bits_per_sample == 8) {
+				sample += ((uint8_t *)sms->cassette->buffer)[sms->cassette_offset + i] - 0x80;
+			}
+		}
+	}
+	uint32_t bytes_per_sample = sms->cassette_wave.num_channels * sms->cassette_wave.bits_per_sample / 8;
+	if (sms->cassette_offset == sms->cassette->size - bytes_per_sample) {
+		sms->cassette_state = TAPE_STOPPED;
+		puts("Cassette reached end of file, playback stoped");
+	}
+	return sample > 0 ? 0x80 : 0;
+}
+
+static void cassette_action(system_header *header, uint8_t action)
+{
+	sms_context *sms = (sms_context*)header;
+	if (!sms->cassette) {
+		return;
+	}
+	cassette_run(sms, sms->z80->Z80_CYCLE);
+	switch(action)
+	{
+	case CASSETTE_PLAY:
+		sms->cassette_state = TAPE_PLAYING;
+		puts("Cassette playback started");
+		break;
+	case CASSETTE_RECORD:
+		break;
+	case CASSETTE_STOP:
+		sms->cassette_state = TAPE_STOPPED;
+		puts("Cassette playback stoped");
+		break;
+	case CASSETTE_REWIND:
+		sms->cassette_offset = sms->cassette_wave.format_header.size + offsetof(wave_header, audio_format);
+		break;
+	}
+}
+
 static uint8_t i8255_input_poll(i8255 *ppi, uint32_t cycle, uint32_t port)
 {
 	if (port > 1) {
@@ -142,10 +237,9 @@
 	sms_context *sms = ppi->system;
 	if (sms->kb_mux == 7) {
 		if (port) {
-			//TODO: cassette-in
 			//TODO: printer port BUSY/FAULT
 			uint8_t port_b = io_data_read(sms->io.ports+1, cycle);
-			return (port_b >> 2 & 0xF) | 0x10;
+			return (port_b >> 2 & 0xF) | 0x10 | cassette_read(sms, cycle);
 		} else {
 			uint8_t port_a = io_data_read(sms->io.ports, cycle);
 			uint8_t port_b = io_data_read(sms->io.ports+1, cycle);
@@ -154,9 +248,8 @@
 	}
 	//TODO: keyboard matrix ghosting
 	if (port) {
-		//TODO: cassette-in
 		//TODO: printer port BUSY/FAULT
-		return (sms->keystate[sms->kb_mux] >> 8) | 0x10;
+		return (sms->keystate[sms->kb_mux] >> 8) | 0x10 | cassette_read(sms, cycle);
 	}
 	return sms->keystate[sms->kb_mux];
 }
@@ -604,6 +697,7 @@
 		target_cycle = sms->z80->Z80_CYCLE;
 		vdp_run_context(sms->vdp, target_cycle);
 		psg_run(sms->psg, target_cycle);
+		cassette_run(sms, target_cycle);
 
 		if (system->save_state) {
 			while (!sms->z80->pc) {
@@ -622,6 +716,7 @@
 			z80_adjust_cycles(sms->z80, adjust);
 			vdp_adjust_cycles(sms->vdp, adjust);
 			sms->psg->cycles -= adjust;
+			sms->cassette_cycle -= adjust;
 			target_cycle -= adjust;
 		}
 	}
@@ -886,6 +981,36 @@
 #endif
 }
 
+void load_cassette(sms_context *sms, system_media *media)
+{
+	sms->cassette = NULL;
+	sms->cassette_state = TAPE_NONE;
+	memcpy(&sms->cassette_wave, media->buffer, offsetof(wave_header, data_header));
+	if (memcmp(sms->cassette_wave.chunk.format, "WAVE", 4)) {
+		return;
+	}
+	if (sms->cassette_wave.chunk.size < offsetof(wave_header, data_header)) {
+		return;
+	}
+	if (memcmp(sms->cassette_wave.format_header.id, "fmt ", 4)) {
+		return;
+	}
+	if (sms->cassette_wave.format_header.size < offsetof(wave_header, data_header) - offsetof(wave_header, audio_format)) {
+		return;
+	}
+	if (sms->cassette_wave.bits_per_sample != 8 && sms->cassette_wave.bits_per_sample != 16) {
+		return;
+	}
+	uint32_t data_sub_chunk = sms->cassette_wave.format_header.size + offsetof(wave_header, audio_format);
+	if (data_sub_chunk > media->size || media->size - data_sub_chunk < sizeof(riff_sub_chunk)) {
+		return;
+	}
+	memcpy(&sms->cassette_wave.data_header, ((uint8_t *)media->buffer) + data_sub_chunk, sizeof(riff_sub_chunk));
+	sms->cassette_state = TAPE_STOPPED;
+	sms->cassette_offset = data_sub_chunk;
+	sms->cassette = media;
+}
+
 sms_context *alloc_configure_sms(system_media *media, uint32_t opts, uint8_t force_region)
 {
 	sms_context *sms = calloc(1, sizeof(sms_context));
@@ -947,7 +1072,10 @@
 		sms->start_button_region = 0xC0;
 	} else if (is_sc3000) {
 		sms->keystate = calloc(sizeof(uint16_t), 7);
-		memset(sms->keystate, 0xFF, sizeof(uint16_t) * 7);
+		for (int i = 0; i < 7; i++)
+		{
+			sms->keystate[i] = 0xFFF;
+		}
 		sms->i8255 = calloc(1, sizeof(i8255));
 		i8255_init(sms->i8255, i8255_output_updated, i8255_input_poll);
 		sms->i8255->system = sms;
@@ -956,6 +1084,9 @@
 	} else {
 		init_z80_opts(zopts, sms->header.info.map, sms->header.info.map_chunks, io_map, 4, 15, 0xFF);
 	}
+	if (is_sc3000 && media->chain) {
+		load_cassette(sms, media->chain);
+	}
 	sms->z80 = init_z80_context(zopts);
 	sms->z80->system = sms;
 	sms->z80->Z80_OPTS->gen.debug_cmd_handler = debug_commands;
@@ -1019,6 +1150,7 @@
 	sms->header.serialize = serialize;
 	sms->header.deserialize = deserialize;
 	sms->header.toggle_debug_view = toggle_debug_view;
+	sms->header.cassette_action = cassette_action;
 	sms->header.type = SYSTEM_SMS;
 
 	return sms;
--- a/sms.h	Sat Oct 26 14:31:21 2024 -0700
+++ b/sms.h	Mon Nov 25 22:26:45 2024 -0800
@@ -11,6 +11,7 @@
 #endif
 #include "io.h"
 #include "i8255.h"
+#include "wave.h"
 
 #define SMS_RAM_SIZE (8*1024)
 #define SMS_CART_RAM_SIZE (32*1024)
@@ -24,6 +25,7 @@
 	i8255         *i8255;
 	uint16_t      *keystate;
 	uint8_t       *rom;
+	system_media  *cassette;
 	uint32_t      rom_size;
 	uint32_t      master_clock;
 	uint32_t      normal_clock;
@@ -34,6 +36,10 @@
 	uint8_t       bank_regs[4];
 	uint8_t       cart_ram[SMS_CART_RAM_SIZE];
 	uint8_t       kb_mux;
+	uint8_t       cassette_state;
+	uint32_t      cassette_offset;
+	uint32_t      cassette_cycle;
+	wave_header   cassette_wave;
 } sms_context;
 
 sms_context *alloc_configure_sms(system_media *media, uint32_t opts, uint8_t force_region);
--- a/system.h	Sat Oct 26 14:31:21 2024 -0700
+++ b/system.h	Mon Nov 25 22:26:45 2024 -0800
@@ -38,6 +38,13 @@
 	NUM_DEBUG_TYPES
 };
 
+enum {
+	CASSETTE_PLAY,
+	CASSETTE_RECORD,
+	CASSETTE_STOP,
+	CASSETTE_REWIND
+};
+
 typedef void (*system_fun)(system_header *);
 typedef uint16_t (*system_fun_r16)(system_header *);
 typedef void (*system_str_fun)(system_header *, char *);
@@ -82,6 +89,7 @@
 	system_str_fun          start_vgm_log;
 	system_fun              stop_vgm_log;
 	system_u8_fun           toggle_debug_view;
+	system_u8_fun           cassette_action;
 	rom_info          info;
 	arena             *arena;
 	char              *next_rom;