changeset 2558:3f58fec775df

Initial work on YMF262 (aka OPL3) emulation
author Michael Pavone <pavone@retrodev.com>
date Sun, 19 Jan 2025 00:31:16 -0800
parents 75dd7536c467
children e534423bd20d
files Makefile genesis.c genesis.h vgm.c vgm.h ym2612.c ym2612.h ym_common.c ym_common.h ymf262.c ymf262.h
diffstat 11 files changed, 530 insertions(+), 196 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Thu Jan 16 22:42:09 2025 -0800
+++ b/Makefile	Sun Jan 19 00:31:16 2025 -0800
@@ -218,7 +218,7 @@
 endif
 endif
 endif
-AUDIOOBJS=ym2612.o psg.o wave.o flac.o vgm.o event_log.o render_audio.o rf5c164.o
+AUDIOOBJS=ym2612.o ymf262.o ym_common.o psg.o wave.o flac.o vgm.o event_log.o render_audio.o rf5c164.o
 CONFIGOBJS=config.o tern.o util.o paths.o
 NUKLEAROBJS=$(FONT) $(CHOOSER) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o
 RENDEROBJS=ppm.o controller_info.o
--- a/genesis.c	Thu Jan 16 22:42:09 2025 -0800
+++ b/genesis.c	Sun Jan 19 00:31:16 2025 -0800
@@ -709,13 +709,16 @@
 		psg_run(gen->psg, cur_target);
 		pico_pcm_run(gen->adpcm, cur_target);
 		if (gen->ymz) {
+			//FIXME: These have a separate crystal
 			ymz263b_run(gen->ymz, cur_target);
+			ymf262_run(gen->opl, cur_target);
 		}
 	}
 	psg_run(gen->psg, target);
 	pico_pcm_run(gen->adpcm, target);
 	if (gen->ymz) {
 		ymz263b_run(gen->ymz, target);
+		ymf262_run(gen->opl, target);
 	}
 }
 
@@ -872,6 +875,7 @@
 			gen->adpcm->cycle -= deduction;
 			if (gen->ymz) {
 				gen->ymz->cycle -= deduction;
+				ymf262_adjust_cycles(gen->opl, deduction);
 			}
 			if (gen->reset_cycle != CYCLE_NEVER) {
 				gen->reset_cycle -= deduction;
@@ -1603,6 +1607,7 @@
 	uint8_t ret;
 	m68k_context *m68k = vcontext;
 	genesis_context *gen = m68k->system;
+	//FIXME: Copera sound hardware has a separate clock
 	switch (location & 0xFF)
 	{
 	case 1:
@@ -1618,6 +1623,10 @@
 		ret = ymz263b_data_read(gen->ymz, location & 4);
 		printf("Copera YMZ263 Data read - %X: %X\n", location, ret);
 		return ret;
+	case 0x28:
+		ret = ymf262_read_status(gen->opl, m68k->cycles, 0);
+		printf("Copera YMF262 Status read - %X: %X\n", location, ret);
+		return ret;
 	default:
 		printf("Unhandled Copera 8-bit read %X\n", location);
 		return 0xFF;
@@ -1634,6 +1643,7 @@
 {
 	m68k_context *m68k = vcontext;
 	genesis_context *gen = m68k->system;
+	//FIXME: Copera sound hardware has a separate clock
 	switch (location & 0xFF)
 	{
 	case 1:
@@ -1652,9 +1662,17 @@
 	case 0x24:
 	case 0x34:
 		printf("Copera YMF262 Address Part #%d write - %X: %X\n", ((location >> 4) & 1) + 1, location, value);
+		ymf262_run(gen->opl, m68k->cycles);
+		if (location & 0x10) {
+			ymf262_address_write_part2(gen->opl, value);
+		} else {
+			ymf262_address_write_part1(gen->opl, value);
+		}
 		break;
 	case 0x28:
 		printf("Copera YMF262 Data write - %X: %X\n", location, value);
+		ymf262_run(gen->opl, m68k->cycles);
+		ymf262_data_write(gen->opl, value);
 		break;
 	case 0x40:
 		//Bit 4 = SCI
@@ -1906,6 +1924,7 @@
 		}
 		if (context->ymz) {
 			ymz263b_adjust_master_clock(context->ymz, context->master_clock);
+			ymf262_adjust_master_clock(context->opl, context->master_clock);
 		}
 		pico_pcm_adjust_master_clock(context->adpcm, context->master_clock);
 	}
@@ -2250,6 +2269,7 @@
 		if (gen->ymz) {
 			ymz263b_free(gen->ymz);
 			free(gen->ymz);
+			ymf262_free(gen->opl);
 		}
 	} else {
 		ym_free(gen->ym);
@@ -2495,6 +2515,9 @@
 			ym_vgm_log(gen->ym, gen->normal_clock, vgm);
 		} else {
 			sync_sound_pico(gen, vgm->last_cycle);
+			if (gen->opl) {
+				ymf262_vgm_log(gen->opl, gen->normal_clock, vgm);
+			}
 		}
 		psg_vgm_log(gen->psg, gen->normal_clock, vgm);
 		gen->header.vgm_logging = 1;
@@ -3230,7 +3253,10 @@
 		//This divider is just a guess, PCB diagram in MAME shows no other crystal
 		//Datasheet says the typical clock is 16.9344 MHz
 		//Master clock / 3 is 17.897725 MHz which is reasonably close
+		//FIXME: Copera does actually have a separate crystal for these
 		ymz263b_init(gen->ymz, gen->master_clock, 3);
+		gen->opl = calloc(1, sizeof(*gen->opl));
+		ymf262_init(gen->opl, gen->master_clock, 3, ym_opts);
 	}
 	
 	gen->work_ram = calloc(2, RAM_WORDS);
--- a/genesis.h	Thu Jan 16 22:42:09 2025 -0800
+++ b/genesis.h	Sun Jan 19 00:31:16 2025 -0800
@@ -16,6 +16,7 @@
 #include "z80_to_x86.h"
 #endif
 #include "ym2612.h"
+#include "ymf262.h"
 #include "vdp.h"
 #include "psg.h"
 #include "pico_pcm.h"
@@ -36,6 +37,7 @@
 	psg_context     *psg;
 	pico_pcm        *adpcm;
 	ymz263b         *ymz;
+	ymf262_context  *opl;
 	uint16_t        *cart;
 	uint16_t        *lock_on;
 	uint16_t        *work_ram;
--- a/vgm.c	Thu Jan 16 22:42:09 2025 -0800
+++ b/vgm.c	Sun Jan 19 00:31:16 2025 -0800
@@ -15,11 +15,8 @@
 	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->header_size = sizeof(vgm_header);
+	fseek(f, writer->header_size, SEEK_CUR);
 	writer->master_clock = clock;
 	writer->last_cycle = cycle;
 	
@@ -114,6 +111,33 @@
 	fwrite(cmd, 1, sizeof(cmd), writer->f);
 }
 
+void vgm_ymf262_init(vgm_writer *writer, uint32_t clock)
+{
+	if (writer->header.version < 0x151) {
+		writer->header.version = 0x151;
+	}
+	uint32_t min_size = sizeof(vgm_header) + offsetof(vgm_extended_header, ymf278b_clk);
+	if (writer->header_size < min_size) {
+		fseek(writer->f, min_size - writer->header_size, SEEK_CUR);
+		writer->header_size = min_size;
+	}
+	writer->ext.ymf262_clk = clock;
+}
+
+void vgm_ymf262_part1_write(vgm_writer *writer, uint32_t cycle, uint8_t reg, uint8_t value)
+{
+	add_wait(writer, cycle);
+	uint8_t cmd[3] = {CMD_YMF262_0, reg, value};
+	fwrite(cmd, 1, sizeof(cmd), writer->f);
+}
+
+void vgm_ymf262_part2_write(vgm_writer *writer, uint32_t cycle, uint8_t reg, uint8_t value)
+{
+	add_wait(writer, cycle);
+	uint8_t cmd[3] = {CMD_YMF262_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) {
@@ -130,7 +154,14 @@
 	fwrite(&cmd, 1, sizeof(cmd), writer->f);
 	writer->header.eof_offset = ftell(writer->f) - offsetof(vgm_header, eof_offset);
 	fseek(writer->f, SEEK_SET, 0);
+	uint32_t extra_size = writer->header_size - sizeof(writer->header);
+	if (extra_size) {
+		writer->header.data_offset += extra_size;
+	}
 	fwrite(&writer->header, sizeof(writer->header), 1, writer->f);
+	if (extra_size) {
+		fwrite(&writer->ext, extra_size, 1, writer->f);
+	}
 	fclose(writer->f);
 	free(writer);
-}
\ No newline at end of file
+}
--- a/vgm.h	Thu Jan 16 22:42:09 2025 -0800
+++ b/vgm.h	Sun Jan 19 00:31:16 2025 -0800
@@ -126,10 +126,12 @@
 
 typedef struct {
 	vgm_header header;
+	vgm_extended_header ext;
 	FILE       *f;
 	uint32_t   master_clock;
 	uint32_t   last_cycle;
 	uint32_t   extra_delta;
+	uint32_t   header_size;
 } vgm_writer;
 
 vgm_writer *vgm_write_open(char *filename, uint32_t rate, uint32_t clock, uint32_t cycle);
@@ -139,6 +141,9 @@
 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_ymf262_init(vgm_writer *writer, uint32_t clock);
+void vgm_ymf262_part1_write(vgm_writer *writer, uint32_t cycle, uint8_t reg, uint8_t value);
+void vgm_ymf262_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);
 
--- a/ym2612.c	Thu Jan 16 22:42:09 2025 -0800
+++ b/ym2612.c	Sun Jan 19 00:31:16 2025 -0800
@@ -4,8 +4,6 @@
  BlastEm is free software distributed under the terms of the GNU General Public License version 3 or greater. See COPYING for full license text.
 */
 #include <string.h>
-#include <math.h>
-#include <stdio.h>
 #include <stdlib.h>
 #include "ym2612.h"
 #include "render.h"
@@ -47,57 +45,12 @@
 	PHASE_RELEASE
 };
 
-uint8_t did_tbl_init = 0;
-//According to Nemesis, real hardware only uses a 256 entry quarter sine table; however,
-//memory is cheap so using a half sine table will probably save some cycles
-//a full sine table would be nice, but negative numbers don't get along with log2
-#define SINE_TABLE_SIZE 512
-static uint16_t sine_table[SINE_TABLE_SIZE];
-//Similar deal here with the power table for log -> linear conversion
-//According to Nemesis, real hardware only uses a 256 entry table for the fractional part
-//and uses the whole part as a shift amount.
-#define POW_TABLE_SIZE (1 << 13)
-static uint16_t pow_table[POW_TABLE_SIZE];
+static int16_t ams_shift[] = {8, 1, -1, -2};
+static uint8_t lfo_timer_values[] = {108, 77, 71, 67, 62, 44, 8, 5};
 
-static uint16_t rate_table_base[] = {
-	//main portion
-	0,1,0,1,0,1,0,1,
-	0,1,0,1,1,1,0,1,
-	0,1,1,1,0,1,1,1,
-	0,1,1,1,1,1,1,1,
-	//top end
-	1,1,1,1,1,1,1,1,
-	1,1,1,2,1,1,1,2,
-	1,2,1,2,1,2,1,2,
-	1,2,2,2,1,2,2,2,
-};
-
-static uint16_t rate_table[64*8];
-
-static uint8_t lfo_timer_values[] = {108, 77, 71, 67, 62, 44, 8, 5};
-static uint8_t lfo_pm_base[][8] = {
-	{0,   0,   0,   0,   0,   0,   0,   0},
-	{0,   0,   0,   0,   4,   4,   4,   4},
-	{0,   0,   0,   4,   4,   4,   8,   8},
-	{0,   0,   4,   4,   8,   8, 0xc, 0xc},
-	{0,   0,   4,   8,   8,   8, 0xc,0x10},
-	{0,   0,   8, 0xc,0x10,0x10,0x14,0x18},
-	{0,   0,0x10,0x18,0x20,0x20,0x28,0x30},
-	{0,   0,0x20,0x30,0x40,0x40,0x50,0x60}
-};
-static int16_t lfo_pm_table[128 * 32 * 8];
-
-int16_t ams_shift[] = {8, 1, -1, -2};
-
-#define MAX_ENVELOPE 0xFFC
 #define YM_DIVIDER 2
 #define CYCLE_NEVER 0xFFFFFFFF
 
-static uint16_t round_fixed_point(double value, int dec_bits)
-{
-	return value * (1 << dec_bits) + 0.5;
-}
-
 static FILE * debug_file = NULL;
 static uint32_t first_key_on=0;
 
@@ -108,7 +61,7 @@
 	if (!log_context) {
 		return;
 	}
-	for (int i = 0; i < NUM_CHANNELS; i++) {
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++) {
 		if (log_context->channels[i].logfile) {
 			wave_finalize(log_context->channels[i].logfile);
 		}
@@ -118,7 +71,7 @@
 
 void ym_adjust_master_clock(ym2612_context * context, uint32_t master_clock)
 {
-	render_audio_adjust_clock(context->audio, master_clock, context->clock_inc * NUM_OPERATORS);
+	render_audio_adjust_clock(context->audio, master_clock, context->clock_inc * OPN2_NUM_OPERATORS);
 }
 
 void ym_adjust_cycles(ym2612_context *context, uint32_t deduction)
@@ -142,11 +95,6 @@
 	}
 }
 
-#ifdef __ANDROID__
-#define log2(x) (log(x)/log(2))
-#endif
-
-
 #define TIMER_A_MAX 1023
 #define TIMER_B_MAX 255
 
@@ -155,9 +103,9 @@
 	memset(context->part1_regs, 0, sizeof(context->part1_regs));
 	memset(context->part2_regs, 0, sizeof(context->part2_regs));
 	memset(context->operators, 0, sizeof(context->operators));
-	FILE* savedlogs[NUM_CHANNELS];
-	uint8_t saved_scope_channel[NUM_CHANNELS];
-	for (int i = 0; i < NUM_CHANNELS; i++)
+	FILE* savedlogs[OPN2_NUM_CHANNELS];
+	uint8_t saved_scope_channel[OPN2_NUM_CHANNELS];
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++)
 	{
 		savedlogs[i] = context->channels[i].logfile;
 		saved_scope_channel[i] = context->channels[i].scope_channel;
@@ -178,7 +126,7 @@
 	//TODO: Reset LFO state
 
 	//some games seem to expect that the LR flags start out as 1
-	for (int i = 0; i < NUM_CHANNELS; i++) {
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++) {
 		context->channels[i].lr = 0xC0;
 		context->channels[i].logfile = savedlogs[i];
 		context->channels[i].scope_channel = saved_scope_channel[i];
@@ -189,7 +137,7 @@
 		}
 	}
 	context->write_cycle = CYCLE_NEVER;
-	for (int i = 0; i < NUM_OPERATORS; i++) {
+	for (int i = 0; i < OPN2_NUM_OPERATORS; i++) {
 		context->operators[i].envelope = MAX_ENVELOPE;
 		context->operators[i].env_phase = PHASE_RELEASE;
 	}
@@ -202,13 +150,13 @@
 	memset(context, 0, sizeof(*context));
 	context->clock_inc = clock_div * 6;
 	context->busy_cycles = BUSY_CYCLES * context->clock_inc;
-	context->audio = render_audio_source("YM2612", master_clock, context->clock_inc * NUM_OPERATORS, 2);
+	context->audio = render_audio_source("YM2612", master_clock, context->clock_inc * OPN2_NUM_OPERATORS, 2);
 	//TODO: pick a randomish high initial value and lower it over time
 	context->invalid_status_decay = 225000 * context->clock_inc;
 	context->status_address_mask = (options & YM_OPT_3834) ? 0 : 3;
 
 	//some games seem to expect that the LR flags start out as 1
-	for (int i = 0; i < NUM_CHANNELS; i++) {
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++) {
 		if (options & YM_OPT_WAVE_LOG) {
 			char fname[64];
 			sprintf(fname, "ym_channel_%d.wav", i);
@@ -217,7 +165,7 @@
 				fprintf(stderr, "Failed to open WAVE log file %s for writing\n", fname);
 				continue;
 			}
-			if (!wave_init(f, master_clock / (context->clock_inc * NUM_OPERATORS), 16, 1)) {
+			if (!wave_init(f, master_clock / (context->clock_inc * OPN2_NUM_OPERATORS), 16, 1)) {
 				fclose(f);
 				context->channels[i].logfile = NULL;
 			}
@@ -230,63 +178,7 @@
 			registered_finalize = 1;
 		}
 	}
-	if (!did_tbl_init) {
-		//populate sine table
-		for (int32_t i = 0; i < 512; i++) {
-			double sine = sin( ((double)(i*2+1) / SINE_TABLE_SIZE) * M_PI_2 );
-
-			//table stores 4.8 fixed pointed representation of the base 2 log
-			sine_table[i] = round_fixed_point(-log2(sine), 8);
-		}
-		//populate power table
-		for (int32_t i = 0; i < POW_TABLE_SIZE; i++) {
-			double linear = pow(2, -((double)((i & 0xFF)+1) / 256.0));
-			int32_t tmp = round_fixed_point(linear, 11);
-			int32_t shift = (i >> 8) - 2;
-			if (shift < 0) {
-				tmp <<= 0-shift;
-			} else {
-				tmp >>= shift;
-			}
-			pow_table[i] =  tmp;
-		}
-		//populate envelope generator rate table, from small base table
-		for (int rate = 0; rate < 64; rate++) {
-			for (int cycle = 0; cycle < 8; cycle++) {
-				uint16_t value;
-				if (rate < 2) {
-					value = 0;
-				} else if (rate >= 60) {
-					value = 8;
-				} else if (rate < 8) {
-					value = rate_table_base[((rate & 6) == 6 ? 16 : 0) + cycle];
-				} else if (rate < 48) {
-					value = rate_table_base[(rate & 0x3) * 8 + cycle];
-				} else {
-					value = rate_table_base[32 + (rate & 0x3) * 8 + cycle] << ((rate - 48) >> 2);
-				}
-				rate_table[rate * 8 + cycle] = value;
-			}
-		}
-		//populate LFO PM table from small base table
-		//seems like there must be a better way to derive this
-		for (int freq = 0; freq < 128; freq++) {
-			for (int pms = 0; pms < 8; pms++) {
-				for (int step = 0; step < 32; step++) {
-					int16_t value = 0;
-					for (int bit = 0x40, shift = 0; bit > 0; bit >>= 1, shift++) {
-						if (freq & bit) {
-							value += lfo_pm_base[pms][(step & 0x8) ? 7-step & 7 : step & 7] >> shift;
-						}
-					}
-					if (step & 0x10) {
-						value = -value;
-					}
-					lfo_pm_table[freq * 256 + pms * 32 + step] = value;
-				}
-			}
-		}
-	}
+	ym_init_tables();
 	ym_reset(context);
 	ym_enable_zero_offset(context, 1);
 }
@@ -430,7 +322,7 @@
 		context->lfo_am_step = context->lfo_pm_step = 0;
 	}
 	if (context->lfo_pm_step != old_pm_step) {
-		for (int chan = 0; chan < NUM_CHANNELS; chan++)
+		for (int chan = 0; chan < OPN2_NUM_CHANNELS; chan++)
 		{
 			if (context->channels[chan].pms) {
 				for (int op = chan * 4; op < (chan + 1) * 4; op++)
@@ -565,17 +457,7 @@
 		if (env > MAX_ENVELOPE) {
 			env = MAX_ENVELOPE;
 		}
-		if (first_key_on) {
-			dfprintf(debug_file, "op %d, base phase: %d, mod: %d, sine: %d, out: %d\n", op, phase, mod, sine_table[(phase+mod) & 0x1FF], pow_table[sine_table[phase & 0x1FF] + env]);
-		}
-		//if ((channel != 0 && channel != 4) || chan->algorithm != 5) {
-			phase += mod;
-		//}
-
-		int16_t output = pow_table[sine_table[phase & 0x1FF] + env];
-		if (phase & 0x200) {
-			output = -output;
-		}
+		int16_t output = ym_sine(phase, mod, env);
 		if (op % 4 == 0) {
 			chan->op1_old = operator->output;
 		} else if (op % 4 == 2) {
@@ -625,7 +507,7 @@
 void ym_output_sample(ym2612_context *context)
 {
 	int16_t left = 0, right = 0;
-	for (int i = 0; i < NUM_CHANNELS; i++) {
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++) {
 		int16_t value = context->channels[i].output;
 		if (value >= 0) {
 			value += context->zero_offset;
@@ -683,7 +565,7 @@
 			ym_channel * channel = context->channels + op/4;
 			ym_run_envelope(context, channel, operator);
 			context->current_env_op++;
-			if (context->current_env_op == NUM_OPERATORS) {
+			if (context->current_env_op == OPN2_NUM_OPERATORS) {
 				context->current_env_op = 0;
 				context->env_counter++;
 			}
@@ -692,7 +574,7 @@
 		//Update Phase Generator
 		ym_run_phase(context, context->current_op / 4, context->current_op);
 		context->current_op++;
-		if (context->current_op == NUM_OPERATORS) {
+		if (context->current_op == OPN2_NUM_OPERATORS) {
 			context->current_op = 0;
 			ym_output_sample(context);
 		}
@@ -963,7 +845,7 @@
 		}
 	} else if (context->selected_reg < 0xA0) {
 		//part
-		uint8_t op = context->selected_part ? (NUM_OPERATORS/2) : 0;
+		uint8_t op = context->selected_part ? (OPN2_NUM_OPERATORS/2) : 0;
 		//channel in part
 		if ((context->selected_reg & 0x3) != 0x3) {
 			op += 4 * (context->selected_reg & 0x3) + ((context->selected_reg & 0xC) / 4);
@@ -1276,7 +1158,7 @@
 {
 	save_buffer8(buf, context->part1_regs, YM_PART1_REGS);
 	save_buffer8(buf, context->part2_regs, YM_PART2_REGS);
-	for (int i = 0; i < NUM_OPERATORS; i++)
+	for (int i = 0; i < OPN2_NUM_OPERATORS; i++)
 	{
 		save_int32(buf, context->operators[i].phase_counter);
 		save_int16(buf, context->operators[i].envelope);
@@ -1284,7 +1166,7 @@
 		save_int8(buf, context->operators[i].env_phase);
 		save_int8(buf, context->operators[i].inverted);
 	}
-	for (int i = 0; i < NUM_CHANNELS; i++)
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++)
 	{
 		save_int16(buf, context->channels[i].output);
 		save_int16(buf, context->channels[i].op1_old);
@@ -1347,7 +1229,7 @@
 			ym_data_write(context, temp_regs[i]);
 		}
 	}
-	for (int i = 0; i < NUM_OPERATORS; i++)
+	for (int i = 0; i < OPN2_NUM_OPERATORS; i++)
 	{
 		context->operators[i].phase_counter = load_int32(buf);
 		context->operators[i].envelope = load_int16(buf);
@@ -1358,7 +1240,7 @@
 		}
 		context->operators[i].inverted = load_int8(buf) != 0 ? SSG_INVERT : 0;
 	}
-	for (int i = 0; i < NUM_CHANNELS; i++)
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++)
 	{
 		context->channels[i].output = load_int16(buf);
 		context->channels[i].op1_old = load_int16(buf);
@@ -1379,11 +1261,11 @@
 	context->sub_timer_b = load_int8(buf);
 	context->env_counter = load_int16(buf);
 	context->current_op = load_int8(buf);
-	if (context->current_op >= NUM_OPERATORS) {
+	if (context->current_op >= OPN2_NUM_OPERATORS) {
 		context->current_op = 0;
 	}
 	context->current_env_op = load_int8(buf);
-	if (context->current_env_op >= NUM_OPERATORS) {
+	if (context->current_env_op >= OPN2_NUM_OPERATORS) {
 		context->current_env_op = 0;
 	}
 	context->lfo_counter = load_int8(buf);
@@ -1416,9 +1298,9 @@
 		"YM2612 #6"
 	};
 	context->scope = scope;
-	for (int i = 0; i < NUM_CHANNELS; i++)
+	for (int i = 0; i < OPN2_NUM_CHANNELS; i++)
 	{
-		context->channels[i].scope_channel = scope_add_channel(scope, names[i], master_clock / (context->clock_inc * NUM_OPERATORS));
+		context->channels[i].scope_channel = scope_add_channel(scope, names[i], master_clock / (context->clock_inc * OPN2_NUM_OPERATORS));
 	}
 #endif
 }
--- a/ym2612.h	Thu Jan 16 22:42:09 2025 -0800
+++ b/ym2612.h	Sun Jan 19 00:31:16 2025 -0800
@@ -6,59 +6,20 @@
 #ifndef YM2612_H_
 #define YM2612_H_
 
-#include <stdint.h>
-#include <stdio.h>
 #include "serialize.h"
+#include "ym_common.h"
 #include "render_audio.h"
 #include "vgm.h"
 #include "oscilloscope.h"
 
 #define NUM_PART_REGS (0xB7-0x30)
-#define NUM_CHANNELS 6
-#define NUM_OPERATORS (4*NUM_CHANNELS)
+#define OPN2_NUM_CHANNELS 6
+#define OPN2_NUM_OPERATORS (4*OPN2_NUM_CHANNELS)
 
 #define YM_OPT_WAVE_LOG 1
 #define YM_OPT_3834 2
 
 typedef struct {
-	int16_t  *mod_src[2];
-	uint32_t phase_counter;
-	uint32_t phase_inc;
-	uint16_t envelope;
-	int16_t  output;
-	uint16_t total_level;
-	uint16_t sustain_level;
-	uint8_t  rates[4];
-	uint8_t  key_scaling;
-	uint8_t  multiple;
-	uint8_t  detune;
-	uint8_t  am;
-	uint8_t  env_phase;
-	uint8_t  ssg;
-	uint8_t  inverted;
-	uint8_t  phase_overflow;
-} ym_operator;
-
-typedef struct {
-	FILE *   logfile;
-	uint16_t fnum;
-	int16_t  output;
-	int16_t  op1_old;
-	int16_t  op2_old;
-	uint8_t  block_fnum_latch;
-	uint8_t  block;
-	uint8_t  keycode;
-	uint8_t  algorithm;
-	uint8_t  feedback;
-	uint8_t  ams;
-	uint8_t  pms;
-	uint8_t  lr;
-	uint8_t  keyon;
-	uint8_t  scope_channel;
-	uint8_t  phase_overflow;
-} ym_channel;
-
-typedef struct {
 	uint16_t fnum;
 	uint8_t  block;
 	uint8_t  block_fnum_latch;
@@ -85,8 +46,8 @@
 	uint32_t     status_address_mask;
 	int32_t      volume_mult;
 	int32_t      volume_div;
-	ym_operator  operators[NUM_OPERATORS];
-	ym_channel   channels[NUM_CHANNELS];
+	ym_operator  operators[OPN2_NUM_OPERATORS];
+	ym_channel   channels[OPN2_NUM_CHANNELS];
 	int16_t      zero_offset;
 	uint16_t     timer_a;
 	uint16_t     timer_a_load;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ym_common.c	Sun Jan 19 00:31:16 2025 -0800
@@ -0,0 +1,193 @@
+#include <math.h>
+#include "ym_common.h"
+
+#ifdef __ANDROID__
+#define log2(x) (log(x)/log(2))
+#endif
+
+//According to Nemesis, real hardware only uses a 256 entry quarter sine table; however,
+//memory is cheap so using a half sine table will probably save some cycles
+//a full sine table would be nice, but negative numbers don't get along with log2
+#define SINE_TABLE_SIZE 512
+static uint16_t sine_table[SINE_TABLE_SIZE];
+//Similar deal here with the power table for log -> linear conversion
+//According to Nemesis, real hardware only uses a 256 entry table for the fractional part
+//and uses the whole part as a shift amount.
+#define POW_TABLE_SIZE (1 << 13)
+static uint16_t pow_table[POW_TABLE_SIZE];
+
+static uint16_t rate_table_base[] = {
+	//main portion
+	0,1,0,1,0,1,0,1,
+	0,1,0,1,1,1,0,1,
+	0,1,1,1,0,1,1,1,
+	0,1,1,1,1,1,1,1,
+	//top end
+	1,1,1,1,1,1,1,1,
+	1,1,1,2,1,1,1,2,
+	1,2,1,2,1,2,1,2,
+	1,2,2,2,1,2,2,2,
+};
+
+uint16_t rate_table[64*8];
+
+static uint8_t lfo_pm_base[][8] = {
+	{0,   0,   0,   0,   0,   0,   0,   0},
+	{0,   0,   0,   0,   4,   4,   4,   4},
+	{0,   0,   0,   4,   4,   4,   8,   8},
+	{0,   0,   4,   4,   8,   8, 0xc, 0xc},
+	{0,   0,   4,   8,   8,   8, 0xc,0x10},
+	{0,   0,   8, 0xc,0x10,0x10,0x14,0x18},
+	{0,   0,0x10,0x18,0x20,0x20,0x28,0x30},
+	{0,   0,0x20,0x30,0x40,0x40,0x50,0x60}
+};
+int16_t lfo_pm_table[128 * 32 * 8];
+
+static uint16_t round_fixed_point(double value, int dec_bits)
+{
+	return value * (1 << dec_bits) + 0.5;
+}
+
+void ym_init_tables(void)
+{
+	static uint8_t did_tbl_init;
+	if (did_tbl_init) {
+		return;
+	}
+	did_tbl_init = 1;
+	//populate sine table
+	for (int32_t i = 0; i < 512; i++) {
+		double sine = sin( ((double)(i*2+1) / SINE_TABLE_SIZE) * M_PI_2 );
+
+		//table stores 4.8 fixed pointed representation of the base 2 log
+		sine_table[i] = round_fixed_point(-log2(sine), 8);
+	}
+	//populate power table
+	for (int32_t i = 0; i < POW_TABLE_SIZE; i++) {
+		double linear = pow(2, -((double)((i & 0xFF)+1) / 256.0));
+		int32_t tmp = round_fixed_point(linear, 11);
+		int32_t shift = (i >> 8) - 2;
+		if (shift < 0) {
+			tmp <<= 0-shift;
+		} else {
+			tmp >>= shift;
+		}
+		pow_table[i] =  tmp;
+	}
+	//populate envelope generator rate table, from small base table
+	for (int rate = 0; rate < 64; rate++) {
+		for (int cycle = 0; cycle < 8; cycle++) {
+			uint16_t value;
+			if (rate < 2) {
+				value = 0;
+			} else if (rate >= 60) {
+				value = 8;
+			} else if (rate < 8) {
+				value = rate_table_base[((rate & 6) == 6 ? 16 : 0) + cycle];
+			} else if (rate < 48) {
+				value = rate_table_base[(rate & 0x3) * 8 + cycle];
+			} else {
+				value = rate_table_base[32 + (rate & 0x3) * 8 + cycle] << ((rate - 48) >> 2);
+			}
+			rate_table[rate * 8 + cycle] = value;
+		}
+	}
+	//populate LFO PM table from small base table
+	//seems like there must be a better way to derive this
+	for (int freq = 0; freq < 128; freq++) {
+		for (int pms = 0; pms < 8; pms++) {
+			for (int step = 0; step < 32; step++) {
+				int16_t value = 0;
+				for (int bit = 0x40, shift = 0; bit > 0; bit >>= 1, shift++) {
+					if (freq & bit) {
+						value += lfo_pm_base[pms][(step & 0x8) ? 7-step & 7 : step & 7] >> shift;
+					}
+				}
+				if (step & 0x10) {
+					value = -value;
+				}
+				lfo_pm_table[freq * 256 + pms * 32 + step] = value;
+			}
+		}
+	}
+}
+
+int16_t ym_sine(uint16_t phase, int16_t mod, uint16_t env)
+{
+	phase += mod;
+	if (env > MAX_ENVELOPE) {
+		env = MAX_ENVELOPE;
+	}
+	int16_t output = pow_table[sine_table[phase & 0x1FF] + env];
+	if (phase & 0x200) {
+		output = -output;
+	}
+	return output;
+}
+
+int16_t ym_opl_wave(uint16_t phase, int16_t mod, uint16_t env, uint8_t waveform)
+{
+	if (env > MAX_OPL_ENVELOPE) {
+		env = MAX_OPL_ENVELOPE;
+	}
+	
+	int16_t output;
+	switch (waveform)
+	{
+	default:
+	case 0:
+		output = pow_table[sine_table[phase & 0x1FF] + env];
+		if (phase & 0x200) {
+			output = -output;
+		}
+		break;
+	case 1:
+		if (phase & 0x200) {
+			output = 0;
+		} else {
+			output = pow_table[sine_table[phase & 0x1FF] + env];
+		}
+		break;
+	case 2:
+		output = pow_table[sine_table[phase & 0x1FF] + env];
+		break;
+	case 3:
+	if (phase & 0x100) {
+			output = 0;
+		} else {
+			output = pow_table[sine_table[phase & 0xFF] + env];
+		}
+		break;
+	case 4:
+		if (phase & 0x200) {
+			output = 0;
+		} else {
+			output = pow_table[sine_table[(phase & 0xFF) << 1] + env];
+			if (phase & 0x100) {
+				output = -output;
+			}
+		}
+		break;
+	case 5:
+		if (phase & 0x200) {
+			output = 0;
+		} else {
+			output = pow_table[sine_table[(phase & 0xFF) << 1] + env];
+		}
+		break;
+	case 6:
+		output = pow_table[env];
+		if (phase & 0x200) {
+			output = -output;
+		}
+		break;
+	case 7:
+		if (phase & 0x200) {
+			output = -pow_table[((~phase) & 0x1FF) << 3 + env];
+		} else {
+			output = pow_table[(phase & 0x1FF) << 3 + env];
+		}
+		break;
+	}
+	return output;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ym_common.h	Sun Jan 19 00:31:16 2025 -0800
@@ -0,0 +1,54 @@
+#ifndef YM_COMMON_H_
+#define YM_COMMON_H_
+#include <stdint.h>
+#include <stdio.h>
+
+#define MAX_ENVELOPE 0xFFC
+#define MAX_OPL_ENVELOPE 0xFF8
+
+typedef struct {
+	int16_t  *mod_src[2];
+	uint32_t phase_counter;
+	uint32_t phase_inc;
+	uint16_t envelope;
+	int16_t  output;
+	uint16_t total_level;
+	uint16_t sustain_level;
+	uint8_t  rates[4];
+	uint8_t  key_scaling;
+	uint8_t  multiple;
+	uint8_t  detune;
+	uint8_t  am;
+	uint8_t  env_phase;
+	uint8_t  ssg;
+	uint8_t  inverted;
+	uint8_t  phase_overflow;
+} ym_operator;
+
+typedef struct {
+	FILE *   logfile;
+	uint16_t fnum;
+	int16_t  output;
+	int16_t  op1_old;
+	int16_t  op2_old;
+	uint8_t  block_fnum_latch;
+	uint8_t  block;
+	uint8_t  keycode;
+	uint8_t  algorithm;
+	uint8_t  feedback;
+	uint8_t  ams;
+	uint8_t  pms;
+	uint8_t  lr;
+	uint8_t  keyon;
+	uint8_t  scope_channel;
+	uint8_t  phase_overflow;
+} ym_channel;
+
+extern int16_t lfo_pm_table[128 * 32 * 8];
+extern uint16_t rate_table[64*8];
+
+void ym_init_tables(void);
+int16_t ym_sine(uint16_t phase, int16_t mod, uint16_t env);
+int16_t ym_opl_wave(uint16_t phase, int16_t mod, uint16_t env, uint8_t waveform);
+
+#endif //YM_COMMON_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ymf262.c	Sun Jan 19 00:31:16 2025 -0800
@@ -0,0 +1,129 @@
+#include <stdlib.h>
+#include <string.h>
+#include "ymf262.h"
+#include "render_audio.h"
+
+void ymf262_init(ymf262_context *context, uint32_t master_clock, uint32_t clock_div, uint32_t options)
+{
+	memset(context, 0, sizeof(*context));
+	context->clock_inc = clock_div * 8;
+	context->audio = render_audio_source("YMF262", master_clock, context->clock_inc * OPL3_NUM_OPERATORS, 2);
+	ymf262_reset(context);
+}
+
+void ymf262_reset(ymf262_context *context)
+{
+	//TODO: implement me
+}
+
+void ymf262_free(ymf262_context *context)
+{
+	render_free_source(context->audio);
+	free(context);
+}
+
+void ymf262_adjust_master_clock(ymf262_context *context, uint32_t master_clock)
+{
+	render_audio_adjust_clock(context->audio, master_clock, context->clock_inc * OPL3_NUM_OPERATORS);
+}
+
+void ymf262_adjust_cycles(ymf262_context *context, uint32_t deduction)
+{
+	context->cycle -= deduction;
+}
+
+void ymf262_run(ymf262_context *context, uint32_t to_cycle)
+{
+	for (; context->cycle < to_cycle; context->cycle += context->clock_inc)
+	{
+		context->current_op++;
+		if (context->current_op == OPL3_NUM_OPERATORS) {
+			context->current_op = 0;
+			int16_t left = 0, right = 0;
+			render_put_stereo_sample(context->audio, left, right);
+		}
+	}
+}
+
+void ymf262_address_write_part1(ymf262_context *context, uint8_t address)
+{
+	context->selected_reg = address;
+	context->selected_part = 0;
+}
+void ymf262_address_write_part2(ymf262_context *context, uint8_t address)
+{
+	context->selected_reg = address;
+	context->selected_part = 0;
+}
+
+#define OPL3_NTS 0x08
+
+void ymf262_data_write(ymf262_context *context, uint8_t value)
+{
+	if (!context->selected_reg) {
+		return;
+	}
+	uint8_t old = 0;
+	if (context->selected_reg >= OPL3_PARAM_START && context->selected_reg < OPL3_PARAM_END) {
+		if (context->selected_part) {
+			old = context->part2_regs[context->selected_reg - OPL3_PARAM_START];
+			context->part2_regs[context->selected_reg - OPL3_PARAM_START] = value;
+		} else {
+			old = context->part1_regs[context->selected_reg - OPL3_PARAM_START];
+			context->part1_regs[context->selected_reg - OPL3_PARAM_START] = value;
+		}
+	} else if (context->selected_part) {
+		if (context->selected_reg <= sizeof(context->timer_test)) {
+			old = context->timer_test[context->selected_reg - 1];
+			context->timer_test[context->selected_reg - 1] = value;
+		} else if (context->selected_reg == OPL3_NTS) {
+			old = context->nts;
+			context->nts = value;
+		} else {
+			return;
+		}
+	} else {
+		switch (context->selected_reg)
+		{
+		case 0x01:
+			old = context->part2_test;
+			context->part2_test = value;
+			break;
+		case 0x04:
+			old = context->connection_sel;
+			context->connection_sel = value;
+			break;
+		case 0x05:
+			old = context->opl3_mode;
+			context->opl3_mode = value;
+			break;
+		default:
+			return;
+		}
+	}
+	if (value != old) {
+		if (context->vgm) {
+			if (context->selected_reg) {
+				vgm_ymf262_part2_write(context->vgm, context->cycle, context->selected_reg, value);
+			} else {
+				vgm_ymf262_part1_write(context->vgm, context->cycle, context->selected_reg, value);
+			}
+		}
+	}
+}
+
+void ymf262_vgm_log(ymf262_context *context, uint32_t master_clock, vgm_writer *vgm)
+{
+	vgm_ymf262_init(vgm, 8 * master_clock / context->clock_inc);
+	context->vgm = vgm;
+	//TODO: write initial state
+}
+
+uint8_t ymf262_read_status(ymf262_context *context, uint32_t cycle, uint32_t port)
+{
+	if (port) {
+		//TODO: Investigate behavior of invalid status reads
+		return 0xFF;
+	}
+	return context->status;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ymf262.h	Sun Jan 19 00:31:16 2025 -0800
@@ -0,0 +1,51 @@
+#ifndef YMF262_H_
+#define YMF262_H_
+
+#include "ym_common.h"
+#include "render_audio.h"
+#include "vgm.h"
+#include "oscilloscope.h"
+
+#define OPL3_NUM_CHANNELS 18
+#define OPL3_NUM_OPERATORS (2*OPL3_NUM_CHANNELS)
+#define OPL3_PARAM_START 0x20
+#define OPL3_PARAM_END 0xF6
+#define OPL3_PARAM_REGS (OPL3_PARAM_END - OPL3_PARAM_START)
+
+typedef struct {
+	audio_source *audio;
+	vgm_writer   *vgm;
+	oscilloscope *scope;
+    uint32_t     clock_inc;
+	uint32_t     cycle;
+	int32_t      volume_mult;
+	int32_t      volume_div;
+	ym_operator  operators[OPL3_NUM_OPERATORS];
+	ym_channel   channels[OPL3_NUM_CHANNELS];
+	uint8_t      part1_regs[OPL3_PARAM_REGS];
+	uint8_t      part2_regs[OPL3_PARAM_REGS];
+	uint8_t      timer_test[4];
+	uint8_t      nts;
+	uint8_t      connection_sel;
+	uint8_t      opl3_mode;
+	uint8_t      part2_test;
+	uint8_t      status;
+	uint8_t      current_op;
+	uint8_t      selected_reg;
+	uint8_t      selected_part;
+} ymf262_context;
+
+void ymf262_init(ymf262_context *context, uint32_t master_clock, uint32_t clock_div, uint32_t options);
+void ymf262_reset(ymf262_context *context);
+void ymf262_free(ymf262_context *context);
+void ymf262_adjust_master_clock(ymf262_context *context, uint32_t master_clock);
+void ymf262_adjust_cycles(ymf262_context *context, uint32_t deduction);
+void ymf262_run(ymf262_context *context, uint32_t to_cycle);
+void ymf262_address_write_part1(ymf262_context *context, uint8_t address);
+void ymf262_address_write_part2(ymf262_context *context, uint8_t address);
+void ymf262_data_write(ymf262_context *context, uint8_t value);
+void ymf262_vgm_log(ymf262_context *context, uint32_t master_clock, vgm_writer *vgm);
+uint8_t ymf262_read_status(ymf262_context *context, uint32_t cycle, uint32_t port);
+
+
+#endif // YMF262_H_