Mercurial > repos > blastem
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;