From 0ae0c6f703c03b12799d7207582fe6abd2527ca1 Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Wed, 18 May 2022 13:55:33 +0700 Subject: [PATCH 1/4] Add YMZ280B support --- CMakeLists.txt | 3 + src/engine/dispatchContainer.cpp | 5 + src/engine/platform/sound/ymz280b.cpp | 786 ++++++++++++++++++++++++++ src/engine/platform/sound/ymz280b.h | 106 ++++ src/engine/platform/ymz280b.cpp | 459 +++++++++++++++ src/engine/platform/ymz280b.h | 104 ++++ src/engine/sample.cpp | 31 +- src/engine/sample.h | 15 +- src/engine/song.h | 1 + src/engine/sysDef.cpp | 9 + src/engine/vgmOps.cpp | 37 +- src/gui/guiConst.cpp | 5 +- src/gui/presets.cpp | 6 + src/gui/sysConf.cpp | 1 + 14 files changed, 1523 insertions(+), 45 deletions(-) create mode 100644 src/engine/platform/sound/ymz280b.cpp create mode 100644 src/engine/platform/sound/ymz280b.h create mode 100644 src/engine/platform/ymz280b.cpp create mode 100644 src/engine/platform/ymz280b.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c34d43da9..5ef8c0c2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,6 +318,8 @@ src/engine/platform/sound/vrcvi/vrcvi.cpp src/engine/platform/sound/scc/scc.cpp +src/engine/platform/sound/ymz280b.cpp + src/engine/platform/oplAInterface.cpp src/engine/platform/ym2608Interface.cpp src/engine/platform/ym2610Interface.cpp @@ -382,6 +384,7 @@ src/engine/platform/pet.cpp src/engine/platform/vic20.cpp src/engine/platform/vrc6.cpp src/engine/platform/scc.cpp +src/engine/platform/ymz280b.cpp src/engine/platform/dummy.cpp ) diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 41adbb200..c371e9fbf 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -58,6 +58,7 @@ #include "platform/fds.h" #include "platform/mmc5.h" #include "platform/scc.h" +#include "platform/ymz280b.h" #include "platform/dummy.h" #include "../ta-log.h" #include "platform/zxbeeper.h" @@ -347,6 +348,10 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do dispatch=new DivPlatformSCC; ((DivPlatformSCC*)dispatch)->setChipModel(true); break; + case DIV_SYSTEM_YMZ280B: + dispatch=new DivPlatformYMZ280B; + ((DivPlatformYMZ280B*)dispatch)->setChipModel(280); + break; case DIV_SYSTEM_SOUND_UNIT: dispatch=new DivPlatformSoundUnit; break; diff --git a/src/engine/platform/sound/ymz280b.cpp b/src/engine/platform/sound/ymz280b.cpp new file mode 100644 index 000000000..683cb67dd --- /dev/null +++ b/src/engine/platform/sound/ymz280b.cpp @@ -0,0 +1,786 @@ +// license:BSD-3-Clause +// copyright-holders:Aaron Giles +/* + + Yamaha YMZ280B driver + by Aaron Giles + + YMZ280B 8-Channel PCMD8 PCM/ADPCM Decoder + + Features as listed in LSI-4MZ280B3 data sheet: + Voice data stored in external memory can be played back simultaneously for up to eight voices + Voice data format can be selected from 4-bit ADPCM, 8-bit PCM and 16-bit PCM + Control of voice data external memory + Up to 16M bytes of ROM or SRAM (x 8 bits, access time 150ms max) can be connected + Continuous access is possible + Loop playback between selective addresses is possible + Voice data playback frequency control + 4-bit ADPCM ................ 0.172 to 44.1kHz in 256 steps + 8-bit PCM, 16-bit PCM ...... 0.172 to 88.2kHz in 512 steps + 256 steps total level and 16 steps panpot can be set + Voice signal is output in stereo 16-bit 2's complement MSB-first format + + TODO: + - Is memory handling 100% correct? At the moment, Konami firebeat.c is the only + hardware currently emulated that uses external handlers. + It also happens to be the only one using 16-bit PCM. + + Some other drivers (eg. bishi.cpp, bfm_sc4/5.cpp) also use ROM readback. + +*/ + +#include "ymz280b.h" +#include +#include +#include + +#define MAX_SAMPLE_CHUNK 10000 + +static constexpr unsigned FRAC_BITS = 8; +static constexpr s32 FRAC_ONE = 1 << FRAC_BITS; + +/* step size index shift table */ +static constexpr int index_scale[8] = { 0x0e6, 0x0e6, 0x0e6, 0x0e6, 0x133, 0x199, 0x200, 0x266 }; + +/* lookup table for the precomputed difference */ +static int diff_lookup[16]; + + +void ymz280b_device::update_step(struct YMZ280BVoice *voice) +{ + int frequency; + + /* compute the frequency */ + if (voice->mode == 1) + frequency = voice->fnum & 0x0ff; + else + frequency = voice->fnum & 0x1ff; + voice->output_step = frequency + 1; // ((fnum + 1) * (input clock / 384)) / 256 +} + + +void ymz280b_device::update_volumes(struct YMZ280BVoice *voice) +{ + if (voice->pan == 8) + { + voice->output_left = voice->level; + voice->output_right = voice->level; + } + else if (voice->pan < 8) + { + voice->output_left = voice->level; + + /* pan 1 is hard-left, what's pan 0? for now assume same as pan 1 */ + voice->output_right = (voice->pan == 0) ? 0 : voice->level * (voice->pan - 1) / 7; + } + else + { + voice->output_left = voice->level * (15 - voice->pan) / 7; + voice->output_right = voice->level; + } +} + +/********************************************************************************************** + + compute_tables -- compute the difference tables + +***********************************************************************************************/ + +static void compute_tables() +{ + /* loop over all nibbles and compute the difference */ + for (int nib = 0; nib < 16; nib++) + { + int value = (nib & 0x07) * 2 + 1; + diff_lookup[nib] = (nib & 0x08) ? -value : value; + } +} + + + +/********************************************************************************************** + + generate_adpcm -- general ADPCM decoding routine + +***********************************************************************************************/ + +int ymz280b_device::generate_adpcm(struct YMZ280BVoice *voice, s16 *buffer, int samples) +{ + int position = voice->position; + int signal = voice->signal; + int step = voice->step; + int val; + + /* two cases: first cases is non-looping */ + if (!voice->looping) + { + /* loop while we still have samples to generate */ + while (samples) + { + /* compute the new amplitude and update the current step */ + val = m_ext_mem[position / 2] >> ((~position & 1) << 2); + signal += (step * diff_lookup[val & 15]) / 8; + + /* clamp to the maximum */ + if (signal > 32767) + signal = 32767; + else if (signal < -32768) + signal = -32768; + + /* adjust the step size and clamp */ + step = (step * index_scale[val & 7]) >> 8; + if (step > 0x6000) + step = 0x6000; + else if (step < 0x7f) + step = 0x7f; + + /* output to the buffer, scaling by the volume */ + *buffer++ = signal; + samples--; + + /* next! */ + position++; + if (position >= voice->stop) + { + voice->ended = true; + break; + } + } + } + + /* second case: looping */ + else + { + /* loop while we still have samples to generate */ + while (samples) + { + /* compute the new amplitude and update the current step */ + val = m_ext_mem[position / 2] >> ((~position & 1) << 2); + signal += (step * diff_lookup[val & 15]) / 8; + + /* clamp to the maximum */ + if (signal > 32767) + signal = 32767; + else if (signal < -32768) + signal = -32768; + + /* adjust the step size and clamp */ + step = (step * index_scale[val & 7]) >> 8; + if (step > 0x6000) + step = 0x6000; + else if (step < 0x7f) + step = 0x7f; + + /* output to the buffer, scaling by the volume */ + *buffer++ = signal; + samples--; + + /* next! */ + position++; + if (position == voice->loop_start && voice->loop_count == 0) + { + voice->loop_signal = signal; + voice->loop_step = step; + } + if (position >= voice->loop_end) + { + if (voice->keyon) + { + position = voice->loop_start; + signal = voice->loop_signal; + step = voice->loop_step; + voice->loop_count++; + } + } + if (position >= voice->stop) + { + voice->ended = true; + break; + } + } + } + + /* update the parameters */ + voice->position = position; + voice->signal = signal; + voice->step = step; + + return samples; +} + + + +/********************************************************************************************** + + generate_pcm8 -- general 8-bit PCM decoding routine + +***********************************************************************************************/ + +int ymz280b_device::generate_pcm8(struct YMZ280BVoice *voice, s16 *buffer, int samples) +{ + int position = voice->position; + int val; + + /* two cases: first cases is non-looping */ + if (!voice->looping) + { + /* loop while we still have samples to generate */ + while (samples) + { + /* fetch the current value */ + val = m_ext_mem[position / 2]; + + /* output to the buffer, scaling by the volume */ + *buffer++ = (s8)val * 256; + samples--; + + /* next! */ + position += 2; + if (position >= voice->stop) + { + voice->ended = true; + break; + } + } + } + + /* second case: looping */ + else + { + /* loop while we still have samples to generate */ + while (samples) + { + /* fetch the current value */ + val = m_ext_mem[position / 2]; + + /* output to the buffer, scaling by the volume */ + *buffer++ = (s8)val * 256; + samples--; + + /* next! */ + position += 2; + if (position >= voice->loop_end) + { + if (voice->keyon) + position = voice->loop_start; + } + if (position >= voice->stop) + { + voice->ended = true; + break; + } + } + } + + /* update the parameters */ + voice->position = position; + + return samples; +} + + + +/********************************************************************************************** + + generate_pcm16 -- general 16-bit PCM decoding routine + +***********************************************************************************************/ + +int ymz280b_device::generate_pcm16(struct YMZ280BVoice *voice, s16 *buffer, int samples) +{ + int position = voice->position; + int val; + + /* two cases: first cases is non-looping */ + if (!voice->looping) + { + /* loop while we still have samples to generate */ + while (samples) + { + /* fetch the current value */ + val = (s16)((m_ext_mem[position / 2 + 1] << 8) + m_ext_mem[position / 2 + 0]); + + /* output to the buffer, scaling by the volume */ + *buffer++ = val; + samples--; + + /* next! */ + position += 4; + if (position >= voice->stop) + { + voice->ended = true; + break; + } + } + } + + /* second case: looping */ + else + { + /* loop while we still have samples to generate */ + while (samples) + { + /* fetch the current value */ + val = (s16)((m_ext_mem[position / 2 + 1] << 8) + m_ext_mem[position / 2 + 0]); + + /* output to the buffer, scaling by the volume */ + *buffer++ = val; + samples--; + + /* next! */ + position += 4; + if (position >= voice->loop_end) + { + if (voice->keyon) + position = voice->loop_start; + } + if (position >= voice->stop) + { + voice->ended = true; + break; + } + } + } + + /* update the parameters */ + voice->position = position; + + return samples; +} + + + + +//------------------------------------------------- +// sound_stream_update - handle a stream update +//------------------------------------------------- + +void ymz280b_device::sound_stream_update(s16 **outputs, int samples) +{ + int v; + + /* loop over voices */ + for (v = 0; v < 8; v++) + { + struct YMZ280BVoice *voice = &m_voice[v]; + s16 prev = voice->last_sample; + s16 curr = voice->curr_sample; + s16 *curr_data = m_scratch.get(); + s16 *ldest = outputs[v*2]; + s16 *rdest = outputs[v*2+1]; + s32 sampindex = 0; + u32 new_samples, samples_left; + u32 final_pos; + int remaining = samples; + int lvol = voice->output_left; + int rvol = voice->output_right; + + /* quick out if we're not playing and we're at 0 */ + if (!voice->playing && curr == 0 && prev == 0) + { + memset(ldest, 0, samples * sizeof(s16)); + memset(rdest, 0, samples * sizeof(s16)); + /* make sure next sound plays immediately */ + voice->output_pos = FRAC_ONE; + continue; + } + + /* finish off the current sample */ + /* interpolate */ + while (remaining > 0 && voice->output_pos < FRAC_ONE) + { + s32 interp_sample = ((s32(prev) * (FRAC_ONE - voice->output_pos)) + (s32(curr) * voice->output_pos)) >> FRAC_BITS; + ldest[sampindex] = (s16)(interp_sample * lvol / 256); + rdest[sampindex] = (s16)(interp_sample * rvol / 256); + sampindex++; + voice->output_pos += voice->output_step; + remaining--; + } + + /* if we're over, continue; otherwise, we're done */ + if (voice->output_pos >= FRAC_ONE) + voice->output_pos -= FRAC_ONE; + else + continue; + + /* compute how many new samples we need */ + final_pos = voice->output_pos + remaining * voice->output_step; + new_samples = (final_pos + FRAC_ONE) >> FRAC_BITS; + if (new_samples > MAX_SAMPLE_CHUNK) + new_samples = MAX_SAMPLE_CHUNK; + samples_left = new_samples; + + /* generate them into our buffer */ + switch (voice->playing << 7 | voice->mode) + { + case 0x81: samples_left = generate_adpcm(voice, m_scratch.get(), new_samples); break; + case 0x82: samples_left = generate_pcm8(voice, m_scratch.get(), new_samples); break; + case 0x83: samples_left = generate_pcm16(voice, m_scratch.get(), new_samples); break; + default: samples_left = 0; memset(m_scratch.get(), 0, new_samples * sizeof(m_scratch[0])); break; + } + + if (samples_left || voice->ended) + { + voice->ended = false; + + /* if there are leftovers, ramp back to 0 */ + int base = new_samples - samples_left; + int i, t = (base == 0) ? curr : m_scratch[base - 1]; + for (i = 0; i < samples_left; i++) + { + if (t < 0) t = -((-t * 15) >> 4); + else if (t > 0) t = (t * 15) >> 4; + m_scratch[base + i] = t; + } + + /* if we hit the end and IRQs are enabled, signal it */ + if (base != 0) + { + voice->playing = 0; + + /* set update_irq_state_timer. IRQ is signaled on next CPU execution. */ + voice->irq_schedule = 1; + } + } + + /* advance forward one sample */ + prev = curr; + curr = *curr_data++; + + /* then sample-rate convert with linear interpolation */ + while (remaining > 0) + { + /* interpolate */ + while (remaining > 0 && voice->output_pos < FRAC_ONE) + { + int interp_sample = ((s32(prev) * (FRAC_ONE - voice->output_pos)) + (s32(curr) * voice->output_pos)) >> FRAC_BITS; + ldest[sampindex] = (s16)(interp_sample * lvol / 256); + rdest[sampindex] = (s16)(interp_sample * rvol / 256); + sampindex++; + voice->output_pos += voice->output_step; + remaining--; + } + + /* if we're over, grab the next samples */ + if (voice->output_pos >= FRAC_ONE) + { + voice->output_pos -= FRAC_ONE; + prev = curr; + curr = *curr_data++; + } + } + + /* remember the last samples */ + voice->last_sample = prev; + voice->curr_sample = curr; + } +} + + + +//------------------------------------------------- +// device_start - device-specific startup +//------------------------------------------------- + +void ymz280b_device::device_start(u8 *ext_mem) +{ + m_ext_mem = ext_mem; + + /* compute ADPCM tables */ + compute_tables(); + + /* allocate memory */ + assert(MAX_SAMPLE_CHUNK < 0x10000); + m_scratch = std::make_unique(MAX_SAMPLE_CHUNK); + + for (auto & elem : m_voice) + { + update_step(&elem); + } +} + +//------------------------------------------------- +// device_reset - device-specific reset +//------------------------------------------------- + +void ymz280b_device::device_reset() +{ + /* initial clear registers */ + for (int i = 0xff; i >= 0; i--) + { + m_current_register = i; + write_to_register(0); + } + + m_current_register = 0; + m_status_register = 0; + m_ext_mem_address = 0; + + /* clear other voice parameters */ + for (auto &elem : m_voice) + { + struct YMZ280BVoice *voice = &elem; + + voice->curr_sample = 0; + voice->last_sample = 0; + voice->output_pos = FRAC_ONE; + voice->playing = 0; + } +} + +/********************************************************************************************** + + write_to_register -- handle a write to the current register + +***********************************************************************************************/ + +void ymz280b_device::write_to_register(int data) +{ + struct YMZ280BVoice *voice; + int i; + + /* lower registers follow a pattern */ + if (m_current_register < 0x80) + { + voice = &m_voice[(m_current_register >> 2) & 7]; + + switch (m_current_register & 0xe3) + { + case 0x00: /* pitch low 8 bits */ + voice->fnum = (voice->fnum & 0x100) | (data & 0xff); + update_step(voice); + break; + + case 0x01: /* pitch upper 1 bit, loop, key on, mode */ + voice->fnum = (voice->fnum & 0xff) | ((data & 0x01) << 8); + voice->looping = (data & 0x10) >> 4; + if ((data & 0x60) == 0) data &= 0x7f; /* ignore mode setting and set to same state as KON=0 */ + else voice->mode = (data & 0x60) >> 5; + if (!voice->keyon && (data & 0x80) && m_keyon_enable) + { + voice->playing = 1; + voice->position = voice->start; + voice->signal = voice->loop_signal = 0; + voice->step = voice->loop_step = 0x7f; + voice->loop_count = 0; + + /* if update_irq_state_timer is set, cancel it. */ + voice->irq_schedule = 0; + } + else if (voice->keyon && !(data & 0x80)) + { + voice->playing = 0; + + /* if update_irq_state_timer is set, cancel it. */ + voice->irq_schedule = 0; + } + voice->keyon = (data & 0x80) >> 7; + update_step(voice); + break; + + case 0x02: /* total level */ + voice->level = data; + update_volumes(voice); + break; + + case 0x03: /* pan */ + voice->pan = data & 0x0f; + update_volumes(voice); + break; + + case 0x20: /* start address high */ + voice->start = (voice->start & (0x00ffff << 1)) | (data << 17); + break; + + case 0x21: /* loop start address high */ + voice->loop_start = (voice->loop_start & (0x00ffff << 1)) | (data << 17); + break; + + case 0x22: /* loop end address high */ + voice->loop_end = (voice->loop_end & (0x00ffff << 1)) | (data << 17); + break; + + case 0x23: /* stop address high */ + voice->stop = (voice->stop & (0x00ffff << 1)) | (data << 17); + break; + + case 0x40: /* start address middle */ + voice->start = (voice->start & (0xff00ff << 1)) | (data << 9); + break; + + case 0x41: /* loop start address middle */ + voice->loop_start = (voice->loop_start & (0xff00ff << 1)) | (data << 9); + break; + + case 0x42: /* loop end address middle */ + voice->loop_end = (voice->loop_end & (0xff00ff << 1)) | (data << 9); + break; + + case 0x43: /* stop address middle */ + voice->stop = (voice->stop & (0xff00ff << 1)) | (data << 9); + break; + + case 0x60: /* start address low */ + voice->start = (voice->start & (0xffff00 << 1)) | (data << 1); + break; + + case 0x61: /* loop start address low */ + voice->loop_start = (voice->loop_start & (0xffff00 << 1)) | (data << 1); + break; + + case 0x62: /* loop end address low */ + voice->loop_end = (voice->loop_end & (0xffff00 << 1)) | (data << 1); + break; + + case 0x63: /* stop address low */ + voice->stop = (voice->stop & (0xffff00 << 1)) | (data << 1); + break; + + default: + if (data != 0) + printf("YMZ280B: unknown register write %02X = %02X\n", m_current_register, data); + break; + } + } + + /* upper registers are special */ + else + { + switch (m_current_register) + { + /* DSP related (not implemented yet) */ + case 0x80: // d0-2: DSP Rch, d3: enable Rch (0: yes, 1: no), d4-6: DSP Lch, d7: enable Lch (0: yes, 1: no) + case 0x81: // d0: enable control of $82 (0: yes, 1: no) + case 0x82: // DSP data + printf("YMZ280B: DSP register write %02X = %02X\n", m_current_register, data); + break; + + case 0x84: /* ROM readback / RAM write (high) */ + m_ext_mem_address_hi = data << 16; + break; + + case 0x85: /* ROM readback / RAM write (middle) */ + m_ext_mem_address_mid = data << 8; + break; + + case 0x86: /* ROM readback / RAM write (low) -> update latch */ + m_ext_mem_address = m_ext_mem_address_hi | m_ext_mem_address_mid | data; + if (m_ext_mem_enable) + m_ext_readlatch = m_ext_mem[m_ext_mem_address]; + break; + + case 0x87: /* RAM write */ + if (m_ext_mem_enable) + { + m_ext_mem[m_ext_mem_address] = data; + m_ext_mem_address = (m_ext_mem_address + 1) & 0xffffff; + } + break; + + case 0xfe: /* IRQ mask */ + m_irq_mask = data; + break; + + case 0xff: /* IRQ enable, test, etc */ + m_ext_mem_enable = (data & 0x40) >> 6; + m_irq_enable = (data & 0x10) >> 4; + + if (m_keyon_enable && !(data & 0x80)) + { + for (i = 0; i < 8; i++) + { + m_voice[i].playing = 0; + + /* if update_irq_state_timer is set, cancel it. */ + m_voice[i].irq_schedule = 0; + } + } + else if (!m_keyon_enable && (data & 0x80)) + { + for (i = 0; i < 8; i++) + { + if (m_voice[i].keyon && m_voice[i].looping) + m_voice[i].playing = 1; + } + } + m_keyon_enable = (data & 0x80) >> 7; + break; + + default: + if (data != 0) + printf("YMZ280B: unknown register write %02X = %02X\n", m_current_register, data); + break; + } + } +} + + + +/********************************************************************************************** + + compute_status -- determine the status bits + +***********************************************************************************************/ + +int ymz280b_device::compute_status() +{ + u8 result; + + result = m_status_register; + + /* clear the IRQ state */ + m_status_register = 0; + return result; +} + + + +/********************************************************************************************** + + read/write -- handle external accesses + +***********************************************************************************************/ + +u8 ymz280b_device::read(offs_t offset) +{ + if ((offset & 1) == 0) + { + if (!m_ext_mem_enable) + return 0xff; + + /* read from external memory */ + u8 ret = m_ext_readlatch; + m_ext_readlatch = m_ext_mem[m_ext_mem_address]; + m_ext_mem_address = (m_ext_mem_address + 1) & 0xffffff; + return ret; + } + else + return compute_status(); +} + + +void ymz280b_device::write(offs_t offset, u8 data) +{ + if ((offset & 1) == 0) + m_current_register = data; + else + { + write_to_register(data); + } +} + +ymz280b_device::ymz280b_device() + : m_current_register(0) + , m_status_register(0) + , m_irq_state(0) + , m_irq_mask(0) + , m_irq_enable(0) + , m_keyon_enable(0) + , m_ext_mem_enable(0) + , m_ext_readlatch(0) + , m_ext_mem_address_hi(0) + , m_ext_mem_address_mid(0) + , m_ext_mem_address(0) +{ + memset(m_voice, 0, sizeof(m_voice)); +} diff --git a/src/engine/platform/sound/ymz280b.h b/src/engine/platform/sound/ymz280b.h new file mode 100644 index 000000000..422b816c3 --- /dev/null +++ b/src/engine/platform/sound/ymz280b.h @@ -0,0 +1,106 @@ +// license:BSD-3-Clause +// copyright-holders:Aaron Giles +/********************************************************************************************** + * + * Yamaha YMZ280B driver + * by Aaron Giles + * + **********************************************************************************************/ + +#include + +#ifndef MAME_SOUND_YMZ280B_H +#define MAME_SOUND_YMZ280B_H + +#pragma once + +namespace ymz280b +{ + typedef unsigned char u8; + typedef signed char s8; + typedef unsigned short u16; + typedef signed short s16; + typedef unsigned int u32; + typedef signed int s32; + typedef signed int offs_t; +} + +using namespace ymz280b; +class ymz280b_device +{ +public: + ymz280b_device(); + + u8 read(offs_t offset); + void write(offs_t offset, u8 data); + + void device_start(u8 *ext_mem); + void device_reset(); + void device_update(); + + void sound_stream_update(s16 **outputs, int samples); + +private: + /* struct describing a single playing ADPCM voice */ + struct YMZ280BVoice + { + u8 playing; /* 1 if we are actively playing */ + bool ended; /* indicate voice has ended in case samples_left is 0 */ + + u8 keyon; /* 1 if the key is on */ + u8 looping; /* 1 if looping is enabled */ + u8 mode; /* current playback mode */ + u16 fnum; /* frequency */ + u8 level; /* output level */ + u8 pan; /* panning */ + + u32 start; /* start address, in nibbles */ + u32 stop; /* stop address, in nibbles */ + u32 loop_start; /* loop start address, in nibbles */ + u32 loop_end; /* loop end address, in nibbles */ + u32 position; /* current position, in nibbles */ + + s32 signal; /* current ADPCM signal */ + s32 step; /* current ADPCM step */ + + s32 loop_signal; /* signal at loop start */ + s32 loop_step; /* step at loop start */ + u32 loop_count; /* number of loops so far */ + + s32 output_left; /* output volume (left) */ + s32 output_right; /* output volume (right) */ + s32 output_step; /* step value for frequency conversion */ + s32 output_pos; /* current fractional position */ + s16 last_sample; /* last sample output */ + s16 curr_sample; /* current sample target */ + u8 irq_schedule; /* 1 if the IRQ state is updated by timer */ + }; + + void update_step(struct YMZ280BVoice *voice); + void update_volumes(struct YMZ280BVoice *voice); + int generate_adpcm(struct YMZ280BVoice *voice, s16 *buffer, int samples); + int generate_pcm8(struct YMZ280BVoice *voice, s16 *buffer, int samples); + int generate_pcm16(struct YMZ280BVoice *voice, s16 *buffer, int samples); + void write_to_register(int data); + int compute_status(); + + // internal state + struct YMZ280BVoice m_voice[8]; /* the 8 voices */ + u8 m_current_register; /* currently accessible register */ + u8 m_status_register; /* current status register */ + u8 m_irq_state; /* current IRQ state */ + u8 m_irq_mask; /* current IRQ mask */ + u8 m_irq_enable; /* current IRQ enable */ + u8 m_keyon_enable; /* key on enable */ + u8 m_ext_mem_enable; /* external memory enable */ + u8 m_ext_readlatch; /* external memory prefetched data */ + u32 m_ext_mem_address_hi; + u32 m_ext_mem_address_mid; + u32 m_ext_mem_address; /* where the CPU can read the ROM */ + + u8 *m_ext_mem; + + std::unique_ptr m_scratch; +}; + +#endif // MAME_SOUND_YMZ280B_H diff --git a/src/engine/platform/ymz280b.cpp b/src/engine/platform/ymz280b.cpp new file mode 100644 index 000000000..33e8189ee --- /dev/null +++ b/src/engine/platform/ymz280b.cpp @@ -0,0 +1,459 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "ymz280b.h" +#include "../engine.h" +#include "../../ta-log.h" +#include +#include + +#define CHIP_FREQBASE 98304 + +#define rWrite(a,v) {if(!skipRegisterWrites) {ymz280b.write(0,a); ymz280b.write(1,v); regPool[a]=v; if(dumpWrites) addWrite(a,v); }} + +const char* regCheatSheetYMZ280B[]={ + "CHx_Freq", "00+x*4", + "CHx_Control", "01+x*4", + "CHx_Volume", "02+x*4", + "CHx_Panning", "03+x*4", + "CHx_StartH", "20+x*4", + "CHx_LoopStartH", "21+x*4", + "CHx_LoopEndH", "22+x*4", + "CHx_EndH", "23+x*4", + "CHx_StartM", "40+x*4", + "CHx_LoopStartM", "41+x*4", + "CHx_LoopEndM", "42+x*4", + "CHx_EndM", "43+x*4", + "CHx_StartL", "60+x*4", + "CHx_LoopStartL", "61+x*4", + "CHx_LoopEndL", "62+x*4", + "CHx_EndL", "63+x*4", + "DSP_Channel", "80", + "DSP_Enable", "81", + "DSP_Data", "82", + "RAM_AddrH", "84", + "RAM_AddrM", "85", + "RAM_AddrL", "86", + "RAM_Data", "87", + "IRQ_Enable", "E0", + "Enable", "FF", + NULL +}; + +const char** DivPlatformYMZ280B::getRegisterSheet() { + return regCheatSheetYMZ280B; +} + +const char* DivPlatformYMZ280B::getEffectName(unsigned char effect) { + return NULL; +} + +void DivPlatformYMZ280B::acquire(short* bufL, short* bufR, size_t start, size_t len) { + short buf[16][256]; + short *bufPtrs[16]={ + buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7], + buf[8],buf[9],buf[10],buf[11],buf[12],buf[13],buf[14],buf[15] + }; + int dataL,dataR; + size_t pos=start; + while (len > 0) { + size_t blockLen = MIN(len, 256); + ymz280b.sound_stream_update(bufPtrs, blockLen); + for (int i=0; idata[oscBuf[j]->needle++]=(short)(((int)buf[j*2][i]+buf[j*2+1][i])/2); + } + bufL[pos]=(short)(dataL/8); + bufR[pos]=(short)(dataR/8); + pos++; + } + len-=blockLen; + } +} + +void DivPlatformYMZ280B::tick(bool sysTick) { + for (int i=0; i<8; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=((chan[i].vol&0xff)*chan[i].std.vol.val)>>6; + writeOutVol(i); + } + if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); + } else { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+chan[i].std.arp.val); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-2048,2048); + } else { + chan[i].pitch2=chan[i].std.pitch.val; + } + chan[i].freqChanged=true; + } + if (chan[i].std.panL.had) { // panning + chan[i].panning=MIN((chan[i].std.panL.val*15/16+15)/2+1,15); + rWrite(0x03+i*4,chan[i].panning); + } + if (chan[i].setPos) { + // force keyon + chan[i].keyOn=true; + chan[i].setPos=false; + } else { + chan[i].audPos=0; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + DivSample* s=parent->getSample(chan[i].sample); + unsigned char ctrl; + switch (s->depth) { + case 2: ctrl=0x20; break; + case 8: ctrl=0x40; break; + case 16: ctrl=0x60; break; + default: ctrl=0; + } + double off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; + chan[i].freq=(int)(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE))-1; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>511) chan[i].freq=511; + // ADPCM has half the range + if (s->depth==2 && chan[i].freq>255) chan[i].freq=255; + ctrl|=(chan[i].active?0x80:0)|((s->loopStart>=0)?0x10:0)|(chan[i].freq>>8); + if (chan[i].keyOn) { + unsigned int start=s->offYMZ280B; + unsigned int loop=0; + unsigned int end=start+s->getCurBufLen(); + if (chan[i].audPos>0) { + switch (s->depth) { + case 2: start+=chan[i].audPos/2; break; + case 8: start+=chan[i].audPos; break; + case 16: start+=chan[i].audPos*2; break; + } + } + if (s->loopStart>=0) { + switch (s->depth) { + case 2: loop=start+s->loopStart/2; break; + case 8: loop=start+s->loopStart; break; + case 16: loop=start+s->loopStart*2; break; + } + } + rWrite(0x01+i*4,ctrl&~0x80); // force keyoff first + rWrite(0x20+i*4,(start>>16)&0xff); + rWrite(0x21+i*4,(loop>>16)&0xff); + rWrite(0x22+i*4,(end>>16)&0xff); + rWrite(0x23+i*4,(end>>16)&0xff); + rWrite(0x40+i*4,(start>>8)&0xff); + rWrite(0x41+i*4,(loop>>8)&0xff); + rWrite(0x42+i*4,(end>>8)&0xff); + rWrite(0x43+i*4,(end>>8)&0xff); + rWrite(0x60+i*4,start&0xff); + rWrite(0x61+i*4,loop&0xff); + rWrite(0x62+i*4,end&0xff); + rWrite(0x63+i*4,end&0xff); + if (!chan[i].std.vol.had) { + chan[i].outVol=chan[i].vol; + writeOutVol(i); + } + chan[i].keyOn=false; + } + if (chan[i].keyOff) { + chan[i].keyOff=false; + } + if (chan[i].freqChanged) { + rWrite(0x00+i*4,chan[i].freq&0xff); + chan[i].freqChanged=false; + } + rWrite(0x01+i*4,ctrl); + } + } +} + +int DivPlatformYMZ280B::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA); + chan[c.chan].sample=ins->amiga.getSample(c.value); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + } + if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { + chan[c.chan].sample=-1; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].macroInit(ins); + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].sample=-1; + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].macroInit(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + writeOutVol(c.chan); + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PANNING: + chan[c.chan].panning=MIN(parent->convertPanSplitToLinearLR(c.value,c.value2,15)+1,15); + rWrite(0x03+c.chan*4,chan[c.chan].panning); + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_FREQUENCY(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val-12):(0))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + } + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_SAMPLE_POS: + chan[c.chan].audPos=c.value; + chan[c.chan].setPos=true; + break; + case DIV_CMD_GET_VOLMAX: + return 255; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformYMZ280B::writeOutVol(int ch) { + unsigned char val=isMuted[ch]?0:chan[ch].outVol; + rWrite(0x02+ch*4,val); +} + +void DivPlatformYMZ280B::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + writeOutVol(ch); +} + +void DivPlatformYMZ280B::forceIns() { + for (int i=0; i<8; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + chan[i].sample=-1; + } +} + +void* DivPlatformYMZ280B::getChanState(int ch) { + return &chan[ch]; +} + +DivDispatchOscBuffer* DivPlatformYMZ280B::getOscBuffer(int ch) { + return oscBuf[ch]; +} + +void DivPlatformYMZ280B::reset() { + memset(regPool,0,256); + ymz280b.device_reset(); + rWrite(0xff,0x80); // enable keyon + for (int i=0; i<8; i++) { + chan[i]=DivPlatformYMZ280B::Channel(); + chan[i].std.setEngine(parent); + rWrite(0x02+i*4,255); + rWrite(0x03+i*4,8); + } +} + +bool DivPlatformYMZ280B::isStereo() { + return true; +} + +void DivPlatformYMZ280B::notifyInsChange(int ins) { + for (int i=0; i<8; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } +} + +void DivPlatformYMZ280B::notifyWaveChange(int wave) { + // TODO when wavetables are added + // TODO they probably won't be added unless the samples reside in RAM +} + +void DivPlatformYMZ280B::notifyInsDeletion(void* ins) { + for (int i=0; i<8; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformYMZ280B::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformYMZ280B::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +unsigned char* DivPlatformYMZ280B::getRegisterPool() { + return regPool; +} + +int DivPlatformYMZ280B::getRegisterPoolSize() { + return 256; +} + +float DivPlatformYMZ280B::getPostAmp() { + // according to MAME core's mixing + return 4.0f; +} + +const void* DivPlatformYMZ280B::getSampleMem(int index) { + return index == 0 ? sampleMem : NULL; +} + +size_t DivPlatformYMZ280B::getSampleMemCapacity(int index) { + return index == 0 ? 16777216 : 0; +} + +size_t DivPlatformYMZ280B::getSampleMemUsage(int index) { + return index == 0 ? sampleMemLen : 0; +} + +void DivPlatformYMZ280B::renderSamples() { + memset(sampleMem,0,getSampleMemCapacity()); + + size_t memPos=0; + for (int i=0; isong.sampleLen; i++) { + DivSample* s=parent->song.sample[i]; + int length=s->getCurBufLen(); + unsigned char* src=(unsigned char*)s->getCurBuf(); + int actualLength=MIN(getSampleMemCapacity()-memPos-length,length); + if (actualLength>0) { + memcpy(&sampleMem[memPos],src,actualLength); + s->offYMZ280B=memPos; + memPos+=length; + } + if (actualLengthrate=rate; + } + return 8; +} + +void DivPlatformYMZ280B::quit() { + delete[] sampleMem; + for (int i=0; i<8; i++) { + delete oscBuf[i]; + } +} diff --git a/src/engine/platform/ymz280b.h b/src/engine/platform/ymz280b.h new file mode 100644 index 000000000..65d2aa48e --- /dev/null +++ b/src/engine/platform/ymz280b.h @@ -0,0 +1,104 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _YMZ280B_H +#define _QSOUND_H + +#include "../dispatch.h" +#include +#include "../macroInt.h" +#include "sound/ymz280b.h" + +class DivPlatformYMZ280B: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, pitch2; + unsigned int audPos; + int sample, wave, ins; + int note; + int panning; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, setPos; + int vol, outVol; + DivMacroInt std; + void macroInit(DivInstrument* which) { + std.init(which); + pitch2=0; + } + Channel(): + freq(0), + baseFreq(0), + pitch(0), + pitch2(0), + audPos(0), + sample(-1), + ins(-1), + note(0), + panning(8), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + setPos(false), + vol(255), + outVol(255) {} + }; + Channel chan[8]; + DivDispatchOscBuffer* oscBuf[8]; + bool isMuted[8]; + int chipType; + + unsigned char* sampleMem; + size_t sampleMemLen; + ymz280b_device ymz280b; + unsigned char regPool[256]; + friend void putDispatchChan(void*,int,int); + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + DivDispatchOscBuffer* getOscBuffer(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + float getPostAmp(); + bool isStereo(); + void setChipModel(int type); + void notifyInsChange(int ins); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + const void* getSampleMem(int index = 0); + size_t getSampleMemCapacity(int index = 0); + size_t getSampleMemUsage(int index = 0); + void renderSamples(); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + private: + void writeOutVol(int ch); +}; + +#endif diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index e8bc0f303..7847a6621 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -93,17 +93,12 @@ bool DivSample::initInternal(unsigned char d, int count) { dataDPCM=new unsigned char[lengthDPCM]; memset(dataDPCM,0,lengthDPCM); break; - case 2: // AICA ADPCM - if (dataAICA!=NULL) delete[] dataAICA; - lengthAICA=(count+1)/2; - dataAICA=new unsigned char[(lengthAICA+255)&(~0xff)]; - memset(dataAICA,0,(lengthAICA+255)&(~0xff)); - break; - case 3: // YMZ ADPCM + case 2: // YMZ ADPCM if (dataZ!=NULL) delete[] dataZ; lengthZ=(count+1)/2; - dataZ=new unsigned char[(lengthZ+255)&(~0xff)]; - memset(dataZ,0,(lengthZ+255)&(~0xff)); + // for padding AICA sample + dataZ=new unsigned char[(lengthZ+3)&(~0x03)]; + memset(dataZ,0,(lengthZ+3)&(~0x03)); break; case 4: // QSound ADPCM if (dataQSoundA!=NULL) delete[] dataQSoundA; @@ -669,10 +664,7 @@ void DivSample::render() { } break; } - case 2: // AICA ADPCM - aica_decode(dataAICA,data16,samples); - break; - case 3: // YMZ ADPCM + case 2: // YMZ ADPCM ymz_decode(dataZ,data16,samples); break; case 4: // QSound ADPCM @@ -727,13 +719,9 @@ void DivSample::render() { if (accum>127) accum=127; } } - if (depth!=2) { // AICA ADPCM + if (depth!=2) { // YMZ ADPCM if (!initInternal(2,samples)) return; - aica_encode(data16,dataAICA,(samples+511)&(~0x1ff)); - } - if (depth!=3) { // YMZ ADPCM - if (!initInternal(3,samples)) return; - ymz_encode(data16,dataZ,(samples+511)&(~0x1ff)); + ymz_encode(data16,dataZ,(samples+7)&(~0x7)); } if (depth!=4) { // QSound ADPCM if (!initInternal(4,samples)) return; @@ -772,8 +760,6 @@ void* DivSample::getCurBuf() { case 1: return dataDPCM; case 2: - return dataAICA; - case 3: return dataZ; case 4: return dataQSoundA; @@ -802,8 +788,6 @@ unsigned int DivSample::getCurBufLen() { case 1: return lengthDPCM; case 2: - return lengthAICA; - case 3: return lengthZ; case 4: return lengthQSoundA; @@ -915,7 +899,6 @@ DivSample::~DivSample() { if (data16) delete[] data16; if (data1) delete[] data1; if (dataDPCM) delete[] dataDPCM; - if (dataAICA) delete[] dataAICA; if (dataZ) delete[] dataZ; if (dataQSoundA) delete[] dataQSoundA; if (dataA) delete[] dataA; diff --git a/src/engine/sample.h b/src/engine/sample.h index 624139797..67a687c42 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -62,8 +62,7 @@ struct DivSample { // valid values are: // - 0: ZX Spectrum overlay drum (1-bit) // - 1: 1-bit NES DPCM (1-bit) - // - 2: AICA ADPCM - // - 3: YMZ ADPCM + // - 2: YMZ ADPCM // - 4: QSound ADPCM // - 5: ADPCM-A // - 6: ADPCM-B @@ -79,8 +78,7 @@ struct DivSample { short* data16; // 16 unsigned char* data1; // 0 unsigned char* dataDPCM; // 1 - unsigned char* dataAICA; // 2 - unsigned char* dataZ; // 3 + unsigned char* dataZ; // 2 unsigned char* dataQSoundA; // 4 unsigned char* dataA; // 5 unsigned char* dataB; // 6 @@ -88,9 +86,9 @@ struct DivSample { unsigned char* dataBRR; // 9 unsigned char* dataVOX; // 10 - unsigned int length8, length16, length1, lengthDPCM, lengthAICA, lengthZ, lengthQSoundA, lengthA, lengthB, lengthX68, lengthBRR, lengthVOX; - unsigned int off8, off16, off1, offDPCM, offAICA, offZ, offQSoundA, offA, offB, offX68, offBRR, offVOX; - unsigned int offSegaPCM, offQSound, offX1_010, offSU; + unsigned int length8, length16, length1, lengthDPCM, lengthZ, lengthQSoundA, lengthA, lengthB, lengthX68, lengthBRR, lengthVOX; + unsigned int off8, off16, off1, offDPCM, offZ, offQSoundA, offA, offB, offX68, offBRR, offVOX; + unsigned int offSegaPCM, offQSound, offX1_010, offSU, offYMZ280B; unsigned int samples; @@ -222,7 +220,6 @@ struct DivSample { data16(NULL), data1(NULL), dataDPCM(NULL), - dataAICA(NULL), dataZ(NULL), dataQSoundA(NULL), dataA(NULL), @@ -234,7 +231,6 @@ struct DivSample { length16(0), length1(0), lengthDPCM(0), - lengthAICA(0), lengthZ(0), lengthQSoundA(0), lengthA(0), @@ -246,7 +242,6 @@ struct DivSample { off16(0), off1(0), offDPCM(0), - offAICA(0), offZ(0), offQSoundA(0), offA(0), diff --git a/src/engine/song.h b/src/engine/song.h index 187df1995..3964442af 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -107,6 +107,7 @@ enum DivSystem { DIV_SYSTEM_SOUND_UNIT, DIV_SYSTEM_MSM6295, DIV_SYSTEM_MSM6258, + DIV_SYSTEM_YMZ280B, DIV_SYSTEM_DUMMY }; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 10f8907f4..210f941c5 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1883,6 +1883,15 @@ void DivEngine::registerSystems() { {DIV_INS_AMIGA} ); + sysDefs[DIV_SYSTEM_YMZ280B]=new DivSysDef( + "Yamaha YMZ280B", NULL, 0xb8, 0, 8, false, true, 0x151, false, + "used in some arcade boards. Has 8 channels of either 4-bit ADPCM, 8-bit PCM or 16-bit PCM.", + {"PCM 1", "PCM 2", "PCM 3", "PCM 4", "PCM 5", "PCM 6", "PCM 7", "PCM 8"}, + {"1", "2", "3", "4", "5", "6", "7", "8"}, + {DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM}, + {DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA} + ); + sysDefs[DIV_SYSTEM_DUMMY]=new DivSysDef( "Dummy System", NULL, 0xfd, 0, 8, false, true, 0, false, "this is a system designed for testing purposes..", diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index 35e59e94f..2b47a5d88 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -675,6 +675,11 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write logW("SCC+: writing to unmapped address %.2x!",write.addr); } break; + case DIV_SYSTEM_YMZ280B: + w->writeC(0x0d|baseAddr1); + w->writeC(write.addr&0xff); + w->writeC(write.val&0xff); + break; default: logW("write not handled!"); break; @@ -794,6 +799,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { int writeSegaPCM=0; DivDispatch* writeX1010[2]={NULL,NULL}; DivDispatch* writeQSound[2]={NULL,NULL}; + DivDispatch* writeZ280[2]={NULL,NULL}; for (int i=0; ichipClock; + willExport[i]=true; + writeZ280[0]=disCont[i].dispatch; + } else if (!(hasZ280&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + writeZ280[1]=disCont[i].dispatch; + hasZ280|=0x40000000; + howManyChips++; + } + break; default: break; } @@ -1398,9 +1417,6 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { w->writeI(0); w->write(writeADPCM[i]->getSampleMem(0),writeADPCM[i]->getSampleMemUsage(0)); } - } - - for (int i=0; i<2; i++) { if (writeADPCM[i]!=NULL && writeADPCM[i]->getSampleMemUsage(1)>0) { w->writeC(0x67); w->writeC(0x66); @@ -1410,9 +1426,6 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { w->writeI(0); w->write(writeADPCM[i]->getSampleMem(1),writeADPCM[i]->getSampleMemUsage(1)); } - } - - for (int i=0; i<2; i++) { if (writeQSound[i]!=NULL && writeQSound[i]->getSampleMemUsage()>0) { unsigned int blockSize=(writeQSound[i]->getSampleMemUsage()+0xffff)&(~0xffff); if (blockSize > 0x1000000) { @@ -1426,9 +1439,6 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { w->writeI(0); w->write(writeQSound[i]->getSampleMem(),blockSize); } - } - - for (int i=0; i<2; i++) { if (writeX1010[i]!=NULL && writeX1010[i]->getSampleMemUsage()>0) { w->writeC(0x67); w->writeC(0x66); @@ -1438,6 +1448,15 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version) { w->writeI(0); w->write(writeX1010[i]->getSampleMem(),writeX1010[i]->getSampleMemUsage()); } + if (writeZ280[i]!=NULL && writeZ280[i]->getSampleMemUsage()>0) { + w->writeC(0x67); + w->writeC(0x66); + w->writeC(0x86); + w->writeI((writeZ280[i]->getSampleMemUsage()+8)|(i*0x80000000)); + w->writeI(writeZ280[i]->getSampleMemCapacity()); + w->writeI(0); + w->write(writeZ280[i]->getSampleMem(),writeZ280[i]->getSampleMemUsage()); + } } // initialize streams diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 62f065f92..13cb4986e 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -116,8 +116,8 @@ const char* insTypes[DIV_INS_MAX]={ const char* sampleDepths[17]={ "1-bit PCM", "1-bit DPCM", - "Yamaha AICA", - "YMZ/YMU ADPCM", + "YMZ/YMU/AICA ADPCM", + NULL, "QSound ADPCM", "ADPCM-A", "ADPCM-B", @@ -877,6 +877,7 @@ const int availableSystems[]={ DIV_SYSTEM_MMC5, DIV_SYSTEM_SCC, DIV_SYSTEM_SCC_PLUS, + DIV_SYSTEM_YMZ280B, 0 // don't remove this last one! }; diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index d04d329d7..b966432b2 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -260,6 +260,12 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "Yamaha YMZ280B", { + DIV_SYSTEM_YMZ280B, 64, 0, 0, + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("Wavetable","chips which use user-specified waveforms to generate sound."); diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index e70348454..7b9ce1612 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -399,6 +399,7 @@ void FurnaceGUI::drawSysConf(int chan, DivSystem type, unsigned int& flags, bool case DIV_SYSTEM_PET: case DIV_SYSTEM_SCC: case DIV_SYSTEM_SCC_PLUS: + case DIV_SYSTEM_YMZ280B: ImGui::Text("nothing to configure"); break; default: From 4551c558182576559507d0c8b3f588144775ca6f Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Wed, 18 May 2022 21:46:14 +0700 Subject: [PATCH 2/4] Fix GCC errors --- src/engine/platform/sound/ymz280b.cpp | 10 +++++----- src/engine/platform/ymz280b.cpp | 5 ++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/engine/platform/sound/ymz280b.cpp b/src/engine/platform/sound/ymz280b.cpp index 683cb67dd..6db651955 100644 --- a/src/engine/platform/sound/ymz280b.cpp +++ b/src/engine/platform/sound/ymz280b.cpp @@ -106,7 +106,7 @@ static void compute_tables() int ymz280b_device::generate_adpcm(struct YMZ280BVoice *voice, s16 *buffer, int samples) { - int position = voice->position; + u32 position = voice->position; int signal = voice->signal; int step = voice->step; int val; @@ -218,7 +218,7 @@ int ymz280b_device::generate_adpcm(struct YMZ280BVoice *voice, s16 *buffer, int int ymz280b_device::generate_pcm8(struct YMZ280BVoice *voice, s16 *buffer, int samples) { - int position = voice->position; + u32 position = voice->position; int val; /* two cases: first cases is non-looping */ @@ -288,7 +288,7 @@ int ymz280b_device::generate_pcm8(struct YMZ280BVoice *voice, s16 *buffer, int s int ymz280b_device::generate_pcm16(struct YMZ280BVoice *voice, s16 *buffer, int samples) { - int position = voice->position; + u32 position = voice->position; int val; /* two cases: first cases is non-looping */ @@ -425,8 +425,8 @@ void ymz280b_device::sound_stream_update(s16 **outputs, int samples) /* if there are leftovers, ramp back to 0 */ int base = new_samples - samples_left; - int i, t = (base == 0) ? curr : m_scratch[base - 1]; - for (i = 0; i < samples_left; i++) + int t = (base == 0) ? curr : m_scratch[base - 1]; + for (u32 i = 0; i < samples_left; i++) { if (t < 0) t = -((-t * 15) >> 4); else if (t > 0) t = (t * 15) >> 4; diff --git a/src/engine/platform/ymz280b.cpp b/src/engine/platform/ymz280b.cpp index 33e8189ee..7d1936524 100644 --- a/src/engine/platform/ymz280b.cpp +++ b/src/engine/platform/ymz280b.cpp @@ -70,12 +70,11 @@ void DivPlatformYMZ280B::acquire(short* bufL, short* bufR, size_t start, size_t buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7], buf[8],buf[9],buf[10],buf[11],buf[12],buf[13],buf[14],buf[15] }; - int dataL,dataR; size_t pos=start; while (len > 0) { size_t blockLen = MIN(len, 256); ymz280b.sound_stream_update(bufPtrs, blockLen); - for (int i=0; isong.sample[i]; int length=s->getCurBufLen(); unsigned char* src=(unsigned char*)s->getCurBuf(); - int actualLength=MIN(getSampleMemCapacity()-memPos-length,length); + int actualLength=MIN((int)(getSampleMemCapacity()-memPos)-length,length); if (actualLength>0) { memcpy(&sampleMem[memPos],src,actualLength); s->offYMZ280B=memPos; From b70ea9af57f8f13df23142c344d76fd157dbc5f0 Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Thu, 19 May 2022 12:39:38 +0700 Subject: [PATCH 3/4] Change YMZ ADPCM type back to 3 Also fix some other mistakes --- src/engine/platform/ymz280b.cpp | 8 ++++---- src/engine/platform/ymz280b.h | 2 +- src/engine/sample.cpp | 12 ++++++------ src/engine/sample.h | 4 ++-- src/engine/sysDef.cpp | 2 +- src/gui/guiConst.cpp | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/engine/platform/ymz280b.cpp b/src/engine/platform/ymz280b.cpp index 7d1936524..4f9f6a93a 100644 --- a/src/engine/platform/ymz280b.cpp +++ b/src/engine/platform/ymz280b.cpp @@ -136,7 +136,7 @@ void DivPlatformYMZ280B::tick(bool sysTick) { DivSample* s=parent->getSample(chan[i].sample); unsigned char ctrl; switch (s->depth) { - case 2: ctrl=0x20; break; + case 3: ctrl=0x20; break; case 8: ctrl=0x40; break; case 16: ctrl=0x60; break; default: ctrl=0; @@ -146,7 +146,7 @@ void DivPlatformYMZ280B::tick(bool sysTick) { if (chan[i].freq<0) chan[i].freq=0; if (chan[i].freq>511) chan[i].freq=511; // ADPCM has half the range - if (s->depth==2 && chan[i].freq>255) chan[i].freq=255; + if (s->depth==3 && chan[i].freq>255) chan[i].freq=255; ctrl|=(chan[i].active?0x80:0)|((s->loopStart>=0)?0x10:0)|(chan[i].freq>>8); if (chan[i].keyOn) { unsigned int start=s->offYMZ280B; @@ -154,14 +154,14 @@ void DivPlatformYMZ280B::tick(bool sysTick) { unsigned int end=start+s->getCurBufLen(); if (chan[i].audPos>0) { switch (s->depth) { - case 2: start+=chan[i].audPos/2; break; + case 3: start+=chan[i].audPos/2; break; case 8: start+=chan[i].audPos; break; case 16: start+=chan[i].audPos*2; break; } } if (s->loopStart>=0) { switch (s->depth) { - case 2: loop=start+s->loopStart/2; break; + case 3: loop=start+s->loopStart/2; break; case 8: loop=start+s->loopStart; break; case 16: loop=start+s->loopStart*2; break; } diff --git a/src/engine/platform/ymz280b.h b/src/engine/platform/ymz280b.h index 65d2aa48e..4957d8998 100644 --- a/src/engine/platform/ymz280b.h +++ b/src/engine/platform/ymz280b.h @@ -18,7 +18,7 @@ */ #ifndef _YMZ280B_H -#define _QSOUND_H +#define _YMZ280B_H #include "../dispatch.h" #include diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index 7847a6621..108b1eb76 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -93,7 +93,7 @@ bool DivSample::initInternal(unsigned char d, int count) { dataDPCM=new unsigned char[lengthDPCM]; memset(dataDPCM,0,lengthDPCM); break; - case 2: // YMZ ADPCM + case 3: // YMZ ADPCM if (dataZ!=NULL) delete[] dataZ; lengthZ=(count+1)/2; // for padding AICA sample @@ -664,7 +664,7 @@ void DivSample::render() { } break; } - case 2: // YMZ ADPCM + case 3: // YMZ ADPCM ymz_decode(dataZ,data16,samples); break; case 4: // QSound ADPCM @@ -719,8 +719,8 @@ void DivSample::render() { if (accum>127) accum=127; } } - if (depth!=2) { // YMZ ADPCM - if (!initInternal(2,samples)) return; + if (depth!=3) { // YMZ ADPCM + if (!initInternal(3,samples)) return; ymz_encode(data16,dataZ,(samples+7)&(~0x7)); } if (depth!=4) { // QSound ADPCM @@ -759,7 +759,7 @@ void* DivSample::getCurBuf() { return data1; case 1: return dataDPCM; - case 2: + case 3: return dataZ; case 4: return dataQSoundA; @@ -787,7 +787,7 @@ unsigned int DivSample::getCurBufLen() { return length1; case 1: return lengthDPCM; - case 2: + case 3: return lengthZ; case 4: return lengthQSoundA; diff --git a/src/engine/sample.h b/src/engine/sample.h index 67a687c42..2a8385e9c 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -62,7 +62,7 @@ struct DivSample { // valid values are: // - 0: ZX Spectrum overlay drum (1-bit) // - 1: 1-bit NES DPCM (1-bit) - // - 2: YMZ ADPCM + // - 3: YMZ ADPCM // - 4: QSound ADPCM // - 5: ADPCM-A // - 6: ADPCM-B @@ -78,7 +78,7 @@ struct DivSample { short* data16; // 16 unsigned char* data1; // 0 unsigned char* dataDPCM; // 1 - unsigned char* dataZ; // 2 + unsigned char* dataZ; // 3 unsigned char* dataQSoundA; // 4 unsigned char* dataA; // 5 unsigned char* dataB; // 6 diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 210f941c5..abafed4e1 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1885,7 +1885,7 @@ void DivEngine::registerSystems() { sysDefs[DIV_SYSTEM_YMZ280B]=new DivSysDef( "Yamaha YMZ280B", NULL, 0xb8, 0, 8, false, true, 0x151, false, - "used in some arcade boards. Has 8 channels of either 4-bit ADPCM, 8-bit PCM or 16-bit PCM.", + "used in some arcade boards. Can play back either 4-bit ADPCM, 8-bit PCM or 16-bit PCM.", {"PCM 1", "PCM 2", "PCM 3", "PCM 4", "PCM 5", "PCM 6", "PCM 7", "PCM 8"}, {"1", "2", "3", "4", "5", "6", "7", "8"}, {DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM}, diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 13cb4986e..44022eded 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -116,8 +116,8 @@ const char* insTypes[DIV_INS_MAX]={ const char* sampleDepths[17]={ "1-bit PCM", "1-bit DPCM", - "YMZ/YMU/AICA ADPCM", NULL, + "YMZ/YMU/AICA ADPCM", "QSound ADPCM", "ADPCM-A", "ADPCM-B", From efd36d29825b9bd85b0b7e0eeecff6d0a1bc5250 Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Sat, 21 May 2022 02:42:23 +0700 Subject: [PATCH 4/4] YMZ280B: Fix full sample memory behavior --- src/engine/platform/sound/ymz280b.h | 1 - src/engine/platform/ymz280b.cpp | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/engine/platform/sound/ymz280b.h b/src/engine/platform/sound/ymz280b.h index 422b816c3..0d6325385 100644 --- a/src/engine/platform/sound/ymz280b.h +++ b/src/engine/platform/sound/ymz280b.h @@ -36,7 +36,6 @@ public: void device_start(u8 *ext_mem); void device_reset(); - void device_update(); void sound_stream_update(s16 **outputs, int samples); diff --git a/src/engine/platform/ymz280b.cpp b/src/engine/platform/ymz280b.cpp index 4f9f6a93a..2d714243b 100644 --- a/src/engine/platform/ymz280b.cpp +++ b/src/engine/platform/ymz280b.cpp @@ -151,13 +151,14 @@ void DivPlatformYMZ280B::tick(bool sysTick) { if (chan[i].keyOn) { unsigned int start=s->offYMZ280B; unsigned int loop=0; - unsigned int end=start+s->getCurBufLen(); + unsigned int end=MIN(start+s->getCurBufLen(),getSampleMemCapacity()-1); if (chan[i].audPos>0) { switch (s->depth) { case 3: start+=chan[i].audPos/2; break; case 8: start+=chan[i].audPos; break; case 16: start+=chan[i].audPos*2; break; } + start=MIN(start,end); } if (s->loopStart>=0) { switch (s->depth) { @@ -165,6 +166,7 @@ void DivPlatformYMZ280B::tick(bool sysTick) { case 8: loop=start+s->loopStart; break; case 16: loop=start+s->loopStart*2; break; } + loop=MIN(loop,end); } rWrite(0x01+i*4,ctrl&~0x80); // force keyoff first rWrite(0x20+i*4,(start>>16)&0xff); @@ -408,7 +410,7 @@ void DivPlatformYMZ280B::renderSamples() { DivSample* s=parent->song.sample[i]; int length=s->getCurBufLen(); unsigned char* src=(unsigned char*)s->getCurBuf(); - int actualLength=MIN((int)(getSampleMemCapacity()-memPos)-length,length); + int actualLength=MIN((int)(getSampleMemCapacity()-memPos),length); if (actualLength>0) { memcpy(&sampleMem[memPos],src,actualLength); s->offYMZ280B=memPos;