changeset 2431:61c0bfe10887

Somewhat busted support for Pico ADPCM
author Michael Pavone <pavone@retrodev.com>
date Tue, 06 Feb 2024 21:47:11 -0800
parents fb8d6ebf9d5f
children 18816c5c11d4
files Makefile genesis.c genesis.h pico_pcm.c pico_pcm.h
diffstat 5 files changed, 498 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Tue Feb 06 06:34:49 2024 -0800
+++ b/Makefile	Tue Feb 06 21:47:11 2024 -0800
@@ -234,12 +234,12 @@
 
 MAINOBJS=blastem.o system.o genesis.o debug.o gdb_remote.o vdp.o $(RENDEROBJS) io.o romdb.o hash.o menu.o xband.o \
 	realtec.o i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) gst.o \
-	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o zip.o bindings.o jcart.o gen_player.o coleco.o \
+	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o zip.o bindings.o jcart.o gen_player.o coleco.o pico_pcm.o \
 	segacd.o lc8951.o cdimage.o cdd_mcu.o cd_graphics.o cdd_fader.o sft_mapper.o mediaplayer.o oscilloscope.o
 
 LIBOBJS=libblastem.o system.o genesis.o vdp.o io.o romdb.o hash.o xband.o realtec.o \
 	i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) gst.o \
-	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o rom.db.o gen_player.o coleco.o $(LIBZOBJS) \
+	$(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o rom.db.o gen_player.o coleco.o pico_pcm.o $(LIBZOBJS) \
 	segacd.o lc8951.o cdimage.o cdd_mcu.o cd_graphics.o cdd_fader.o sft_mapper.o mediaplayer.o
 
 ifdef NONUKLEAR
--- a/genesis.c	Tue Feb 06 06:34:49 2024 -0800
+++ b/genesis.c	Tue Feb 06 21:47:11 2024 -0800
@@ -681,6 +681,102 @@
 	return context;
 }
 
+static void sync_sound_pico(genesis_context * gen, uint32_t target)
+{
+	while (target > gen->psg->cycles && target - gen->psg->cycles > MAX_SOUND_CYCLES)
+	{
+		uint32_t cur_target = gen->psg->cycles + MAX_SOUND_CYCLES;
+		psg_run(gen->psg, cur_target);
+		pico_pcm_run(gen->adpcm, cur_target);
+	}
+	psg_run(gen->psg, target);
+	pico_pcm_run(gen->adpcm, target);
+}
+
+static void adjust_int_cycle_pico(m68k_context *context, vdp_context *v_context)
+{
+	genesis_context *gen = context->system;
+	if (context->sync_cycle - context->current_cycle > gen->max_cycles) {
+		context->sync_cycle = context->current_cycle + gen->max_cycles;
+	}
+	context->int_cycle = CYCLE_NEVER;
+	uint8_t mask = context->status & 0x7;
+	if (mask < 6) {
+		uint32_t next_vint = vdp_next_vint(v_context);
+		if (next_vint != CYCLE_NEVER) {
+			context->int_cycle = next_vint;
+			context->int_num = 6;
+		}
+		if (mask < 5) {
+			uint32_t next_hint = vdp_next_hint(v_context);
+			if (next_hint != CYCLE_NEVER) {
+				next_hint = next_hint < context->current_cycle ? context->current_cycle : next_hint;
+				if (next_hint < context->int_cycle) {
+					context->int_cycle = next_hint;
+					context->int_num = 5;
+
+				}
+			}
+			if (mask < 3) {
+				uint32_t next_pcm_int = pico_pcm_next_int(gen->adpcm);
+				if (next_pcm_int != CYCLE_NEVER && next_pcm_int < context->int_cycle) {
+					context->int_cycle = next_pcm_int;
+					context->int_num = 3;
+				}
+				if (mask < 2 && (v_context->regs[REG_MODE_3] & BIT_EINT_EN) && gen->header.type == SYSTEM_GENESIS) {
+					uint32_t next_eint_port0 = io_next_interrupt(gen->io.ports, context->current_cycle);
+					uint32_t next_eint_port1 = io_next_interrupt(gen->io.ports + 1, context->current_cycle);
+					uint32_t next_eint_port2 = io_next_interrupt(gen->io.ports + 2, context->current_cycle);
+					uint32_t next_eint = next_eint_port0 < next_eint_port1
+						? (next_eint_port0 < next_eint_port2 ? next_eint_port0 : next_eint_port2)
+						: (next_eint_port1 < next_eint_port2 ? next_eint_port1 : next_eint_port2);
+					if (next_eint != CYCLE_NEVER) {
+						next_eint = next_eint < context->current_cycle ? context->current_cycle : next_eint;
+						if (next_eint < context->int_cycle) {
+							context->int_cycle = next_eint;
+							context->int_num = 2;
+						}
+					}
+				}
+			}
+		}
+	}
+	if (context->int_cycle > context->current_cycle && context->int_pending == INT_PENDING_SR_CHANGE) {
+		context->int_pending = INT_PENDING_NONE;
+	}
+	/*if (context->int_cycle != old_int_cycle) {
+		printf("int cycle changed to: %d, level: %d @ %d(%d), frame: %d, vcounter: %d, hslot: %d, mask: %d, hint_counter: %d\n", context->int_cycle, context->int_num, v_context->cycles, context->current_cycle, v_context->frame, v_context->vcounter, v_context->hslot, context->status & 0x7, v_context->hint_counter);
+		old_int_cycle = context->int_cycle;
+	}*/
+
+	if (context->status & M68K_STATUS_TRACE || context->trace_pending) {
+		context->target_cycle = context->current_cycle;
+		return;
+	}
+
+	context->target_cycle = context->int_cycle < context->sync_cycle ? context->int_cycle : context->sync_cycle;
+	if (context->should_return || gen->header.enter_debugger || context->wp_hit) {
+		context->target_cycle = context->current_cycle;
+	} else if (context->target_cycle < context->current_cycle) {
+		//Changes to SR can result in an interrupt cycle that's in the past
+		//This can cause issues with the implementation of STOP though
+		context->target_cycle = context->current_cycle;
+	}
+	if (context->target_cycle == context->int_cycle) {
+		//Currently delays from Z80 access and refresh are applied only when we sync
+		//this can cause extra latency when it comes to interrupts
+		//to prevent this code forces some extra synchronization in the period immediately before an interrupt
+		if ((context->target_cycle - context->current_cycle) > gen->int_latency_prev1) {
+			context->target_cycle = context->sync_cycle = context->int_cycle - gen->int_latency_prev1;
+		} else if ((context->target_cycle - context->current_cycle) > gen->int_latency_prev2) {
+			context->target_cycle = context->sync_cycle = context->int_cycle - gen->int_latency_prev2;
+		} else {
+			context->target_cycle = context->sync_cycle = context->current_cycle;
+		}
+
+	}
+}
+
 static m68k_context* sync_components_pico(m68k_context * context, uint32_t address)
 {
 	genesis_context * gen = context->system;
@@ -692,7 +788,7 @@
 	}
 
 	uint32_t mclks = context->current_cycle;
-	psg_run(gen->psg, mclks);
+	sync_sound_pico(gen, mclks);
 	vdp_run_context(v_context, mclks);
 	if (mclks >= gen->reset_cycle) {
 		gen->reset_requested = 1;
@@ -737,6 +833,7 @@
 				vgm_adjust_cycles(gen->psg->vgm, deduction);
 			}
 			gen->psg->cycles -= deduction;
+			gen->adpcm->cycle -= deduction;
 			if (gen->reset_cycle != CYCLE_NEVER) {
 				gen->reset_cycle -= deduction;
 			}
@@ -756,7 +853,7 @@
 	if (!address && (gen->header.enter_debugger || gen->header.save_state)) {
 		context->sync_cycle = context->current_cycle + 1;
 	}
-	adjust_int_cycle(context, v_context);
+	adjust_int_cycle_pico(context, v_context);
 	if (gen->reset_cycle < context->target_cycle) {
 		context->target_cycle = gen->reset_cycle;
 	}
@@ -809,10 +906,12 @@
 static m68k_context *int_ack(m68k_context *context)
 {
 	genesis_context * gen = context->system;
-	vdp_context * v_context = gen->vdp;
-	//printf("acknowledging %d @ %d:%d, vcounter: %d, hslot: %d\n", context->int_ack, context->current_cycle, v_context->cycles, v_context->vcounter, v_context->hslot);
-	vdp_run_context(v_context, context->current_cycle);
-	vdp_int_ack(v_context);
+	if (gen->header.type != SYSTEM_PICO || context->int_num > 4 || context->int_num < 3) {
+		vdp_context * v_context = gen->vdp;
+		//printf("acknowledging %d @ %d:%d, vcounter: %d, hslot: %d\n", context->int_ack, context->current_cycle, v_context->cycles, v_context->vcounter, v_context->hslot);
+		vdp_run_context(v_context, context->current_cycle);
+		vdp_int_ack(v_context);
+	}
 
 	//the Genesis responds to these exclusively with !VPA which means its a slow
 	//6800 operation. documentation says these can take between 10 and 19 cycles.
@@ -909,7 +1008,11 @@
 			} else {
 				context->sync_cycle = gen->frame_end = vdp_cycles_to_frame_end(v_context);
 				//printf("Set sync cycle to: %d @ %d, vcounter: %d, hslot: %d\n", context->sync_cycle, context->current_cycle, v_context->vcounter, v_context->hslot);
-				adjust_int_cycle(context, v_context);
+				if (gen->header.type == SYSTEM_PICO) {
+					adjust_int_cycle_pico(context, v_context);
+				} else {
+					adjust_int_cycle(context, v_context);
+				}
 			}
 		} else {
 			fatal_error("Illegal write to HV Counter port %X\n", vdp_port);
@@ -1239,7 +1342,24 @@
 
 static void* pico_io_write_w(uint32_t location, void *vcontext, uint16_t value)
 {
-	printf("Pico IO write.w %X - %X\n", location, value);
+	uint32_t port = location & 0xFE;
+	m68k_context *context = vcontext;
+	genesis_context *gen = context->system;
+	if (port == 0x10) {
+		sync_sound_pico(gen, context->current_cycle);
+		pico_pcm_data_write(gen->adpcm, value);
+		printf("PICO ADPCM Data: %04X\n", value);
+		if (context->int_num == 3) {
+			adjust_int_cycle_pico(context, gen->vdp);
+		}
+	} else if (port == 0x12) {
+		sync_sound_pico(gen, context->current_cycle);
+		printf("PICO ADPCM Control: %04X\n", value);
+		pico_pcm_ctrl_write(gen->adpcm, value);
+		adjust_int_cycle_pico(context, gen->vdp);
+	} else {
+		return pico_io_write(location, vcontext, value);
+	}
 	return vcontext;
 }
 
@@ -1376,6 +1496,7 @@
 {
 	m68k_context *m68k = vcontext;
 	genesis_context *gen = m68k->system;
+	uint16_t tmp;
 	switch(location >> 1 & 0x7F)
 	{
 	case 0:
@@ -1394,9 +1515,14 @@
 		return gen->pico_page;
 	case 8:
 		//printf("uPD7759 data read @ %u\n", m68k->current_cycle);
-		return 0xFF;
+		sync_sound_pico(gen, m68k->current_cycle);
+		tmp = pico_pcm_data_read(gen->adpcm);
+		return (location & 1) ? tmp >> 8 : tmp;
 	case 9:
 		//printf("uPD7759 contro/status read @ %u\n", m68k->current_cycle);
+		sync_sound_pico(gen, m68k->current_cycle);
+		tmp = pico_pcm_ctrl_read(gen->adpcm);
+		return (location & 1) ? tmp >> 8 : tmp;
 		return 0;
 	default:
 		printf("Unknown Pico IO read %X @ %u\n", location, m68k->current_cycle);
@@ -1408,6 +1534,14 @@
 {
 	m68k_context *m68k = vcontext;
 	genesis_context *gen = m68k->system;
+	uint32_t port = location & 0xFE;
+	if (port == 0x10) {
+		sync_sound_pico(gen, m68k->current_cycle);
+		return pico_pcm_data_read(gen->adpcm);
+	} else if (port == 0x12) {
+		sync_sound_pico(gen, m68k->current_cycle);
+		return pico_pcm_ctrl_read(gen->adpcm);
+	}
 	uint16_t value = pico_io_read(location, vcontext);
 	return value | (value << 8);
 }
@@ -1778,7 +1912,11 @@
 			insert_breakpoint(gen->m68k, pc, gen->header.debugger_type == DEBUGGER_NATIVE ? debugger : gdb_debug_enter);
 #endif
 		}
-		adjust_int_cycle(gen->m68k, gen->vdp);
+		if (gen->header.type == SYSTEM_PICO) {
+			adjust_int_cycle_pico(gen->m68k, gen->vdp);
+		} else {
+			adjust_int_cycle(gen->m68k, gen->vdp);
+		}
 		start_68k_context(gen->m68k, pc);
 	} else {
 		if (gen->header.enter_debugger) {
@@ -1961,7 +2099,10 @@
 		free(gen->z80);
 		free(gen->zram);
 	}
-	if (gen->header.type != SYSTEM_PICO) {
+	if (gen->header.type == SYSTEM_PICO) {
+		pico_pcm_free(gen->adpcm);
+		free(gen->adpcm);
+	} else {
 		ym_free(gen->ym);
 	}
 	psg_free(gen->psg);
@@ -2226,7 +2367,9 @@
 	} else if (debug_view == DEBUG_OSCILLOSCOPE) {
 		if (gen->psg->scope) {
 			oscilloscope *scope = gen->psg->scope;
-			if (gen->header.type != SYSTEM_PICO) {
+			if (gen->header.type == SYSTEM_PICO) {
+				gen->adpcm->scope = NULL;
+			} else {
 				gen->ym->scope = NULL;
 			}
 			gen->psg->scope = NULL;
@@ -2237,7 +2380,9 @@
 			scope_close(scope);
 		} else {
 			oscilloscope *scope = create_oscilloscope();
-			if (gen->header.type != SYSTEM_PICO) {
+			if (gen->header.type == SYSTEM_PICO) {
+				pico_pcm_enable_scope(gen->adpcm, scope, gen->normal_clock);
+			} else {
 				ym_enable_scope(gen->ym, scope, gen->normal_clock);
 			}
 			psg_enable_scope(gen->psg, scope, gen->normal_clock);
@@ -2913,6 +3058,10 @@
 	
 	gen->psg = calloc(1, sizeof(psg_context));
 	psg_init(gen->psg, gen->master_clock, MCLKS_PER_PSG);
+	
+	gen->adpcm = calloc(1, sizeof(pico_pcm));
+	pico_pcm_init(gen->adpcm, gen->master_clock, 42);
+	
 	gen->work_ram = calloc(2, RAM_WORDS);
 	if (!strcmp("random", tern_find_path_default(config, "system\0ram_init\0", (tern_val){.ptrval = "zero"}, TVAL_PTR).ptrval))
 	{
--- a/genesis.h	Tue Feb 06 06:34:49 2024 -0800
+++ b/genesis.h	Tue Feb 06 21:47:11 2024 -0800
@@ -17,6 +17,7 @@
 #include "ym2612.h"
 #include "vdp.h"
 #include "psg.h"
+#include "pico_pcm.h"
 #include "io.h"
 #include "romdb.h"
 #include "arena.h"
@@ -31,6 +32,7 @@
 	vdp_context     *vdp;
 	ym2612_context  *ym;
 	psg_context     *psg;
+	pico_pcm        *adpcm;
 	uint16_t        *cart;
 	uint16_t        *lock_on;
 	uint16_t        *work_ram;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pico_pcm.c	Tue Feb 06 21:47:11 2024 -0800
@@ -0,0 +1,294 @@
+#include "pico_pcm.h"
+#include "backend.h"
+
+#define PCM_RESET   0x8000
+#define PCM_INT_EN  0x4000
+#define PCM_ENABLED 0x0800
+#define PCM_FILTER  0x00C0
+#define PCM_VOLUME  0x0007
+
+void pico_pcm_reset(pico_pcm *pcm)
+{
+	pcm->fifo_read = sizeof(pcm->fifo);
+	pcm->fifo_write = 0;
+	pcm->adpcm_state = 0;
+	pcm->output = 0;
+	pcm->nibble_store = 0;
+	pcm->counter = 0;
+	pcm->samples = 0;
+	pcm->rate = 0;
+	pcm->ctrl &= 0x7FFF;
+}
+
+void pico_pcm_init(pico_pcm *pcm, uint32_t master_clock, uint32_t divider)
+{
+	pcm->audio = render_audio_source("PICO ADPCM", master_clock, divider * 4, 1);
+	pcm->scope = NULL;
+	pcm->scope_channel = 0;
+	pcm->clock_inc = divider * 4;
+	pico_pcm_reset(pcm);
+}
+
+void pico_pcm_free(pico_pcm *pcm)
+{
+	render_free_source(pcm->audio);
+}
+
+void pico_pcm_enable_scope(pico_pcm *pcm, oscilloscope *scope, uint32_t master_clock)
+{
+#ifndef IS_LIB
+	pcm->scope = scope;
+	pcm->scope_channel = scope_add_channel(scope, "PICO ADPCM", master_clock / pcm->clock_inc);
+#endif
+}
+
+static uint8_t pcm_fifo_read(pico_pcm *pcm)
+{
+	if (pcm->fifo_read == sizeof(pcm->fifo)) {
+		return 0;
+	}
+	uint8_t ret = pcm->fifo[pcm->fifo_read++];
+	pcm->fifo_read &= sizeof(pcm->fifo) - 1;
+	if (pcm->fifo_read == pcm->fifo_write) {
+		pcm->fifo_read = sizeof(pcm->fifo);
+	}
+	return ret;
+}
+
+int16_t upd7755_calc_sample(uint8_t sample, uint8_t *state)
+{
+	//Tables from MAME
+	static const int16_t sample_delta[256] = {
+		0,  0,  1,  2,  3,   5,   7,  10,  0,   0,  -1,  -2,  -3,   -5,   -7,  -10,
+		0,  1,  2,  3,  4,   6,   8,  13,  0,  -1,  -2,  -3,  -4,   -6,   -8,  -13,
+		0,  1,  2,  4,  5,   7,  10,  15,  0,  -1,  -2,  -4,  -5,   -7,  -10,  -15,
+		0,  1,  3,  4,  6,   9,  13,  19,  0,  -1,  -3,  -4,  -6,   -9,  -13,  -19,
+		0,  2,  3,  5,  8,  11,  15,  23,  0,  -2,  -3,  -5,  -8,  -11,  -15,  -23,
+		0,  2,  4,  7, 10,  14,  19,  29,  0,  -2,  -4,  -7, -10,  -14,  -19,  -29,
+		0,  3,  5,  8, 12,  16,  22,  33,  0,  -3,  -5,  -8, -12,  -16,  -22,  -33,
+		1,  4,  7, 10, 15,  20,  29,  43, -1,  -4,  -7, -10, -15,  -20,  -29,  -43,
+		1,  4,  8, 13, 18,  25,  35,  53, -1,  -4,  -8, -13, -18,  -25,  -35,  -53,
+		1,  6, 10, 16, 22,  31,  43,  64, -1,  -6, -10, -16, -22,  -31,  -43,  -64,
+		2,  7, 12, 19, 27,  37,  51,  76, -2,  -7, -12, -19, -27,  -37,  -51,  -76,
+		2,  9, 16, 24, 34,  46,  64,  96, -2,  -9, -16, -24, -34,  -46,  -64,  -96,
+		3, 11, 19, 29, 41,  57,  79, 117, -3, -11, -19, -29, -41,  -57,  -79, -117,
+		4, 13, 24, 36, 50,  69,  96, 143, -4, -13, -24, -36, -50,  -69,  -96, -143,
+		4, 16, 29, 44, 62,  85, 118, 175, -4, -16, -29, -44, -62,  -85, -118, -175,
+		6, 20, 36, 54, 76, 104, 144, 214, -6, -20, -36, -54, -76, -104, -144, -214
+	};
+	static const int state_delta[16] = {-1, -1, 0, 0, 1, 2, 2, 3, -1, -1, 0, 0, 1, 2, 2, 3};
+	int16_t ret = sample_delta[(*state << 4) + sample];
+	int diff = state_delta[*state];
+	if (diff >= 0 || *state > 0) {
+		*state += diff;
+		if (*state > 15) {
+			*state = 15;
+		}
+	}
+	return ret;
+}
+
+void pico_pcm_run(pico_pcm *pcm, uint32_t cycle)
+{
+	while (pcm->cycle < cycle)
+	{
+		pcm->cycle += pcm->clock_inc;
+		//TODO: Figure out actual attenuation
+		int16_t shift = pcm->ctrl & PCM_VOLUME;
+#ifndef IS_LIB
+		if (pcm->scope) {
+			scope_add_sample(pcm->scope, pcm->scope_channel, (pcm->output >> shift) * 128, 0);
+		}
+#endif
+		render_put_mono_sample(pcm->audio, (pcm->output >> shift) * 128);
+		if (!(pcm->ctrl & PCM_ENABLED)) {
+			continue;
+		}
+		if (pcm->counter) {
+			pcm->counter--;
+		} else if (pcm->samples) {
+			pcm->samples--;
+			uint8_t sample;
+			if (pcm->nibble_store) {
+				sample = pcm->nibble_store & 0xF;
+				pcm->nibble_store = 0;
+			} else {
+				uint8_t byte = pcm_fifo_read(pcm);
+				sample = byte >> 4;
+				pcm->nibble_store = 0x80 | (byte & 0xF);
+			}
+			uint8_t old_state = pcm->adpcm_state;
+			pcm->output += upd7755_calc_sample(sample, &pcm->adpcm_state);
+			if (pcm->output > 255) {
+				pcm->output = 255;
+			} else if (pcm->output < -256) {
+				pcm->output = -256;
+			}
+			//printf("Sample %d, old_state %d, new_state %d, output %d\n", sample, old_state, pcm->adpcm_state, pcm->output);
+			pcm->counter = pcm->rate;
+		} else {
+			uint8_t cmd = pcm_fifo_read(pcm);
+			if (cmd) {
+				pcm->ctrl |= 0x8000;
+			} else {
+				pcm->ctrl &= 0x7FFF;
+			}
+			switch (cmd & 0xC0)
+			{
+			case 0:
+				pcm->output = 0;
+				pcm->adpcm_state = 0;
+				pcm->counter = (cmd & 0x3F) * 160;
+				break;
+			case 0x40:
+				pcm->rate = (cmd & 0x3F);
+				pcm->samples = 256;
+				break;
+			case 0x80:
+				pcm->rate = (cmd & 0x3F);
+				//FIXME: this probably does not happen instantly
+				pcm->samples = pcm_fifo_read(pcm) + 1;
+				break;
+			case 0xC0:
+				//FIXME: this probably does not happen instantly
+				//TODO: Does repeat mode even work on this chip?
+				//      Does it work on a uPD7759 in slave mode?
+				//      Is this correct behavior if it does work?
+				pcm->counter = pcm->rate = pcm_fifo_read(pcm) & 0x3F;
+				pcm->samples = (pcm_fifo_read(pcm) + 1) * ((cmd & 7) + 1);
+				break;
+			}
+		}
+	}
+}
+
+// RI??E???FF???VVV
+// R: 1 = Reset request, 0 = normal operation
+// I: 1 = interrupts enabled, 0 = disabled
+// E: 1 = Enabled? Sega code always sets this to 1 outside of reset
+// F: Low-pass Filter 1 = 6 kHz, 2 = 12 kHz 3 = 16 kHz
+// V: volume, probably attenuation value since converter defaults to "0"
+void pico_pcm_ctrl_write(pico_pcm *pcm, uint16_t value)
+{
+	if (value & PCM_RESET) {
+		pico_pcm_reset(pcm);
+	}
+	pcm->ctrl &= 0x8000;
+	pcm->ctrl |= value & ~PCM_RESET;
+	//TODO: update low-pass filter
+}
+
+void pico_pcm_data_write(pico_pcm *pcm, uint16_t value)
+{
+	if (pcm->fifo_read == sizeof(pcm->fifo)) {
+		pcm->fifo_read = pcm->fifo_write;
+	}
+	pcm->fifo[pcm->fifo_write++] = value >> 8;
+	pcm->fifo_write &= sizeof(pcm->fifo)-1;
+	pcm->fifo[pcm->fifo_write++] = value;
+	pcm->fifo_write &= sizeof(pcm->fifo)-1;
+}
+
+uint16_t pico_pcm_ctrl_read(pico_pcm *pcm)
+{
+	return pcm->ctrl;
+}
+
+uint16_t pico_pcm_data_read(pico_pcm *pcm)
+{
+	if (pcm->fifo_read == sizeof(pcm->fifo)) {
+		return sizeof(pcm->fifo) - 1;
+	}
+	return (pcm->fifo_read - pcm->fifo_write) & (sizeof(pcm->fifo)-1);
+}
+
+#define FIFO_THRESHOLD 48
+uint32_t pico_pcm_next_int(pico_pcm *pcm)
+{
+	if (!(pcm->ctrl & PCM_INT_EN)) {
+		return CYCLE_NEVER;
+	}
+	uint32_t fifo_bytes;
+	if (pcm->fifo_read == sizeof(pcm->fifo)) {
+		fifo_bytes = 0;
+	} else if (pcm->fifo_read == pcm->fifo_write) {
+		fifo_bytes = sizeof(pcm->fifo);
+	} else {
+		fifo_bytes = (pcm->fifo_write - pcm->fifo_read) & (sizeof(pcm->fifo) - 1);
+	}
+	if (fifo_bytes < FIFO_THRESHOLD) {
+		return pcm->cycle;
+	}
+	uint32_t cycles_to_threshold = pcm->counter + 1;
+	if (pcm->samples) {
+		uint16_t samples = pcm->samples;
+		if (pcm->nibble_store) {
+			cycles_to_threshold += pcm->rate + 1;
+			samples--;
+		}
+		uint16_t bytes = (samples >> 1) + (samples & 1);
+		if (bytes > (fifo_bytes - FIFO_THRESHOLD)) {
+			cycles_to_threshold += (fifo_bytes - FIFO_THRESHOLD + 1) * (pcm->rate + 1) * 2;
+			fifo_bytes = 0;
+		} else {
+			cycles_to_threshold += bytes * (pcm->rate + 1) * 2;
+			fifo_bytes -= bytes;
+		}
+	}
+	uint8_t fifo_read = pcm->fifo_read;
+	uint8_t cmd = 0;
+	while (fifo_bytes >= FIFO_THRESHOLD)
+	{
+		if (cmd) {
+			switch(cmd & 0xC0)
+			{
+			case 0:
+				cycles_to_threshold += 640 * (cmd & 0x3F) + 1;
+				break;
+			case 0x40:
+				cycles_to_threshold += (fifo_bytes - FIFO_THRESHOLD + 1) * ((cmd & 0x3F) + 1) * 2;
+				fifo_bytes = 0;
+				break;
+			case 0x80: {
+				uint32_t samples = pcm->fifo[fifo_read++];
+				fifo_bytes--;
+				fifo_read &= sizeof(pcm->fifo) - 1;
+				if (fifo_bytes < FIFO_THRESHOLD) {
+					break;
+				}
+				uint32_t bytes = (samples +1) >> 1;
+				if (bytes > (fifo_bytes - FIFO_THRESHOLD)) {
+					cycles_to_threshold += (fifo_bytes - FIFO_THRESHOLD + 1) * ((cmd & 0x3F) + 1) * 2;
+					fifo_bytes = 0;
+				}
+				break; }
+			case 0xC0: {
+				uint32_t rate = pcm->fifo[fifo_read++] & 0x3F;
+				fifo_bytes--;
+				fifo_read &= sizeof(pcm->fifo) - 1;
+				uint32_t samples = pcm->fifo[fifo_read++] & 0x3F;
+				fifo_bytes--;
+				fifo_read &= sizeof(pcm->fifo) - 1;
+				if (fifo_bytes < FIFO_THRESHOLD) {
+					break;
+				}
+				samples++;
+				samples *= (cmd & 7) + 1;
+				uint32_t bytes = (samples + 1) >> 1;
+				if (bytes > (fifo_bytes - FIFO_THRESHOLD)) {
+					cycles_to_threshold += (fifo_bytes - FIFO_THRESHOLD + 1) * ((cmd & 0x3F) + 1) * 2;
+					fifo_bytes = 0;
+				}
+				break; }
+			}
+			cmd = 0;
+		} else {
+			cycles_to_threshold++;
+			cmd = pcm->fifo[fifo_read++];
+			fifo_bytes--;
+			fifo_read &= sizeof(pcm->fifo) - 1;
+		}
+	}
+	
+	return pcm->cycle + cycles_to_threshold * pcm->clock_inc;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pico_pcm.h	Tue Feb 06 21:47:11 2024 -0800
@@ -0,0 +1,38 @@
+#ifndef PICO_PCM_H_
+#define PICO_PCM_H_
+
+#include <stdint.h>
+#include "render_audio.h"
+#include "oscilloscope.h"
+
+typedef struct {
+	audio_source *audio;
+	oscilloscope *scope;
+	uint32_t     clock_inc;
+	uint32_t     cycle;
+	uint16_t     ctrl;
+	
+	uint16_t     counter;
+	uint16_t     rate;
+	uint16_t     samples;
+	int16_t      output;
+	
+	uint8_t      fifo[0x40];
+	uint8_t      fifo_read;
+	uint8_t      fifo_write;
+	uint8_t      adpcm_state;
+	uint8_t      nibble_store;
+	uint8_t      scope_channel;
+} pico_pcm;
+
+void pico_pcm_init(pico_pcm *pcm, uint32_t master_clock, uint32_t divider);
+void pico_pcm_free(pico_pcm *pcm);
+void pico_pcm_enable_scope(pico_pcm *pcm, oscilloscope *scope, uint32_t master_clock);
+void pico_pcm_run(pico_pcm *pcm, uint32_t cycle);
+void pico_pcm_ctrl_write(pico_pcm *pcm, uint16_t value);
+void pico_pcm_data_write(pico_pcm *pcm, uint16_t value);
+uint16_t pico_pcm_ctrl_read(pico_pcm *pcm);
+uint16_t pico_pcm_data_read(pico_pcm *pcm);
+uint32_t pico_pcm_next_int(pico_pcm *pcm);
+
+#endif //PICO_PCM_H_