diff sms.c @ 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 8cf7cadc17ee
children a6687a6fb69d
line wrap: on
line diff
--- 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;