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