From 9bad37d8cb561bf9dc7b5c48273e9b26f813a119 Mon Sep 17 00:00:00 2001 From: superctr Date: Tue, 14 Aug 2018 15:56:44 +0200 Subject: Improve Taito Zoom ZSG-2 sound emulation (#3847) * Improve Taito Zoom ZSG-2 sound emulation zsg2.cpp: implement emphasis filter, this is a noise reduction scheme that amplifies higher frequncies to reduce quantization noise. zsg2.cpp: Add sample interpolation and another adjustable lowpass filter. This seems to be roughly what real hardware does... zsg2.cpp: Improve panning registers and identify DSP output gain registers. * zsg2: minor changes [nw] zsg2: Register 0b appears to be status flags [nw] zsg2: Linear ramping probably makes more sense [nw] * zsg2: slight adjustment of emphasis filter [nw] * zsg2: slight adjustment of emphasis filter #2 [nw] * zsg2: more sober ramping algorithm [nw] --- src/devices/sound/zsg2.cpp | 209 ++++++++++++++++++++++++++++++++++++-------- src/devices/sound/zsg2.h | 27 +++++- src/mame/audio/taito_zm.cpp | 6 +- 3 files changed, 201 insertions(+), 41 deletions(-) diff --git a/src/devices/sound/zsg2.cpp b/src/devices/sound/zsg2.cpp index 42107792265..9e0eb89bdb8 100644 --- a/src/devices/sound/zsg2.cpp +++ b/src/devices/sound/zsg2.cpp @@ -1,11 +1,12 @@ // license:BSD-3-Clause -// copyright-holders:Olivier Galibert, R. Belmont, hap +// copyright-holders:Olivier Galibert, R. Belmont, hap, superctr /* ZOOM ZSG-2 custom wavetable synthesizer Written by Olivier Galibert MAME conversion by R. Belmont Working emulation by The Talentuous Hands Of The Popularious hap + Improvements by superctr --------------------------------------------------------- Additional notes on the sample format, reverse-engineered @@ -44,9 +45,9 @@ --------------------------------------------------------- TODO: -- volume/panning is linear? volume slides are too steep -- most music sounds tinny, probably due to missing DSP? -- what is reg 0xa/0xc? seems related to volume +- Filter behavior might not be perfect. +- Volume ramping probably behaves differently on hardware. +- hook up DSP, it's used for reverb and chorus effects. - identify sample flags * bassdrum in shikigam level 1 music is a good hint: it should be one octave lower, indicating possible stereo sample, or base octave(like in ymf278) @@ -57,6 +58,13 @@ TODO: #include "emu.h" #include "zsg2.h" +#include +#include +#include + +#define EMPHASIS_CUTOFF_BASE 0x800 +#define EMPHASIS_CUTOFF_SHIFT 1 +#define EMPHASIS_OUTPUT_SHIFT 15 // device type definition DEFINE_DEVICE_TYPE(ZSG2, zsg2_device, "zsg2", "ZOOM ZSG-2") @@ -74,7 +82,6 @@ zsg2_device::zsg2_device(const machine_config &mconfig, const char *tag, device_ { } - //------------------------------------------------- // device_start - device-specific startup //------------------------------------------------- @@ -85,7 +92,7 @@ void zsg2_device::device_start() memset(&m_chan, 0, sizeof(m_chan)); - m_stream = stream_alloc(0, 2, clock() / 768); + m_stream = stream_alloc(0, 4, clock() / 768); m_mem_blocks = m_mem_base.length(); m_mem_copy = make_unique_clear(m_mem_blocks); @@ -96,6 +103,13 @@ void zsg2_device::device_start() save_pointer(NAME(m_full_samples), (m_mem_blocks * 4 + 4) / sizeof(int16_t)); save_item(NAME(m_read_address)); + // Generate the output gain table. Assuming -1dB per step for now. + for (int i = 0; i < 32; i++) + { + double val = pow(10, -(31 - i) / 20.) * 65535.; + gain_tab[i] = val; + } + for (int ch = 0; ch < 48; ch++) { save_item(NAME(m_chan[ch].v), ch); @@ -107,14 +121,28 @@ void zsg2_device::device_start() save_item(NAME(m_chan[ch].end_pos), ch); save_item(NAME(m_chan[ch].loop_pos), ch); save_item(NAME(m_chan[ch].page), ch); + save_item(NAME(m_chan[ch].vol), ch); - save_item(NAME(m_chan[ch].flags), ch); - save_item(NAME(m_chan[ch].panl), ch); - save_item(NAME(m_chan[ch].panr), ch); + save_item(NAME(m_chan[ch].vol_initial), ch); + save_item(NAME(m_chan[ch].vol_target), ch); + + save_item(NAME(m_chan[ch].emphasis_cutoff), ch); + save_item(NAME(m_chan[ch].emphasis_cutoff_initial), ch); + save_item(NAME(m_chan[ch].emphasis_cutoff_target), ch); + + save_item(NAME(m_chan[ch].output_cutoff), ch); + save_item(NAME(m_chan[ch].output_cutoff_initial), ch); + save_item(NAME(m_chan[ch].output_cutoff_target), ch); + + save_item(NAME(m_chan[ch].emphasis_filter_state), ch); + save_item(NAME(m_chan[ch].output_filter_state), ch); + + save_item(NAME(m_chan[ch].output_gain), ch); + + save_item(NAME(m_chan[ch].samples), ch); } } - //------------------------------------------------- // device_reset - device-specific reset //------------------------------------------------- @@ -148,7 +176,6 @@ void zsg2_device::device_reset() #endif } - /******************************************************************************/ uint32_t zsg2_device::read_memory(uint32_t offset) @@ -193,6 +220,22 @@ int16_t *zsg2_device::prepare_samples(uint32_t offset) return &m_full_samples[offset]; } +// Fill the buffer with filtered samples +void zsg2_device::filter_samples(zchan *ch) +{ + int16_t *raw_samples = prepare_samples(ch->page | ch->cur_pos); + ch->samples[0] = ch->samples[4]; // we want to remember the last sample + + for (int i = 0; i < 4; i++) + { + ch->samples[i+1] = raw_samples[i]; + + // not sure if the filter works exactly this way, however I am pleased + // with the output for now. + ch->emphasis_filter_state += (raw_samples[i]-(ch->emphasis_filter_state>>16)) * (EMPHASIS_CUTOFF_BASE - ch->emphasis_cutoff); + ch->samples[i+1] = (ch->emphasis_filter_state) >> EMPHASIS_OUTPUT_SHIFT; + } +} //------------------------------------------------- // sound_stream_update - handle a stream update @@ -202,12 +245,15 @@ void zsg2_device::sound_stream_update(sound_stream &stream, stream_sample_t **in { for (int i = 0; i < samples; i++) { - int32_t mix_l = 0; - int32_t mix_r = 0; + int32_t mix[4] = {}; + + int ch = 0; // loop over all channels for (auto & elem : m_chan) + //auto & elem = m_chan[0]; { + ch++; if (!elem.is_playing) continue; @@ -226,21 +272,47 @@ void zsg2_device::sound_stream_update(sound_stream &stream, stream_sample_t **in continue; } } - elem.samples = prepare_samples(elem.page | elem.cur_pos); + filter_samples(&elem); + //elem.samples = prepare_samples(elem.page | elem.cur_pos); } - int32_t sample = (elem.samples[elem.step_ptr >> 14 & 3] * elem.vol) >> 16; + uint8_t sample_pos = elem.step_ptr >> 14 & 3; + int32_t sample; // = elem.samples[sample_pos]; + + // linear interpolation (hardware certainly does something similar) + sample = elem.samples[sample_pos]; + sample += ((uint16_t)(elem.step_ptr<<2&0xffff) * (int16_t)(elem.samples[sample_pos+1] - sample))>>16; + sample = (sample * elem.vol) >> 16; + + // another filter... + elem.output_filter_state += (sample - (elem.output_filter_state>>16)) * elem.output_cutoff; + sample = elem.output_filter_state >> 16; + + for(int output=0; output<4; output++) + { + int output_gain = elem.output_gain[output] & 0x1f; // left / right + int32_t output_sample = sample; + + if (elem.output_gain[output] & 0x80) // perhaps ? + output_sample = -output_sample; - mix_l += (sample * elem.panl + sample * (0x1f - elem.panr)) >> 5; - mix_r += (sample * elem.panr + sample * (0x1f - elem.panl)) >> 5; + mix[output] += (output_sample * gain_tab[output_gain&0x1f]) >> 13; + } + + // Apply transitions (This is not accurate yet) + elem.vol = ramp(elem.vol, elem.vol_target); + elem.output_cutoff = ramp(elem.output_cutoff, elem.output_cutoff_target); + elem.emphasis_cutoff = ramp(elem.emphasis_cutoff, elem.emphasis_cutoff_target); } - outputs[0][i] = mix_l; - outputs[1][i] = mix_r; + ch = 0; + + for(int output=0; output<4; output++) + outputs[output][i] = mix[output]; + } } - /******************************************************************************/ void zsg2_device::chan_w(int ch, int reg, uint16_t data) @@ -275,9 +347,9 @@ void zsg2_device::chan_w(int ch, int reg, uint16_t data) case 0x5: // lo byte: loop address low - // hi byte: right panning (high bits always 0) + // hi byte: right output gain (bypass DSP) m_chan[ch].loop_pos = (m_chan[ch].loop_pos & 0xff00) | (data & 0xff); - m_chan[ch].panr = data >> 8 & 0x1f; + m_chan[ch].output_gain[1] = data >> 8; break; case 0x6: @@ -287,28 +359,52 @@ void zsg2_device::chan_w(int ch, int reg, uint16_t data) case 0x7: // lo byte: loop address high - // hi byte: left panning (high bits always 0) + // hi byte: left output gain (bypass DSP) m_chan[ch].loop_pos = (m_chan[ch].loop_pos & 0x00ff) | (data << 8 & 0xff00); - m_chan[ch].panl = data >> 8 & 0x1f; + m_chan[ch].output_gain[0] = data >> 8; + break; + + case 0x8: + // Filter cutoff (Direct) + m_chan[ch].output_cutoff_initial = data; break; case 0x9: // no function? always 0 break; + case 0xa: + // volume (Direct) + m_chan[ch].vol_initial = data; + break; + case 0xb: // always writes 0 // this register is read-only break; + case 0xc: + // filter gain ? + m_chan[ch].output_cutoff_target = data; + break; + + case 0xd: + // hi byte: DSP Chorus volume + // lo byte: Emphasis filter time constant (direct value) + m_chan[ch].output_gain[3] = data >> 8; + m_chan[ch].emphasis_cutoff_initial = expand_reg(data & 0xff); + break; + case 0xe: - // volume - m_chan[ch].vol = data; + // volume (Target) + m_chan[ch].vol_target = data; break; case 0xf: - // flags - m_chan[ch].flags = data; + // hi byte: DSP Reverb volume + // lo byte: Emphasis filter time constant + m_chan[ch].output_gain[2] = data >> 8; + m_chan[ch].emphasis_cutoff_target = expand_reg(data & 0xff); break; default: @@ -322,10 +418,8 @@ uint16_t zsg2_device::chan_r(int ch, int reg) { switch (reg) { - case 0xb: - // ? - return 0; - + case 0xb: // Only later games (taitogn) read this register... + return m_chan[ch].is_playing << 13; default: break; } @@ -333,6 +427,38 @@ uint16_t zsg2_device::chan_r(int ch, int reg) return m_chan[ch].v[reg]; } +// expand 8-bit reg to 16-bit value. This is used for the emphasis filter +// register. Not sure about how this works, the sound +// CPU uses a lookup table (stored in gdarius sound cpu ROM at 0x6332) to +// calculate this value, for now I'm generating an opproximate inverse. +int16_t zsg2_device::expand_reg(uint8_t val) +{ + static const signed char frac_tab[16] = {8,9,10,11,12,13,14,15,-15,-14,-13,-12,-11,-10,-9,-8}; + static const unsigned char shift_tab[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + + return (frac_tab[val&0x0f] << shift_tab[val>>4])>>EMPHASIS_CUTOFF_SHIFT; +} + +// ramp registers +// The CPU does not write often enough to make the transitions always sound +// smooth, so the sound chip probably helps by smoothing the changes. +// There are two sets of the volume and filter cutoff registers. +// At key on, the CPU writes to the "direct" registers, after that it will +// write to the "target" register instead. +inline int32_t zsg2_device::ramp(int32_t current, int32_t target) +{ + int32_t difference = abs(target-current); + difference -= 0x40; + + if(difference < 0) + return target; + else if(target < current) + return target + difference; + else if(target > current) + return target - difference; + + return target; +} /******************************************************************************/ @@ -352,7 +478,11 @@ void zsg2_device::control_w(int reg, uint16_t data) m_chan[ch].is_playing = true; m_chan[ch].cur_pos = m_chan[ch].start_pos; m_chan[ch].step_ptr = 0; - m_chan[ch].samples = prepare_samples(m_chan[ch].page | m_chan[ch].cur_pos); + m_chan[ch].emphasis_filter_state = 0; + m_chan[ch].vol = m_chan[ch].vol_initial; + m_chan[ch].output_cutoff = m_chan[ch].output_cutoff_initial; + m_chan[ch].emphasis_cutoff = m_chan[ch].emphasis_cutoff_initial; + filter_samples(&m_chan[ch]); } } break; @@ -373,8 +503,15 @@ void zsg2_device::control_w(int reg, uint16_t data) break; } - case 0x18: - break; +// case 0x0c: //These registers are sometimes written to by the CPU. Unknown purpose. +// break; +// case 0x0d: +// break; +// case 0x10: +// break; + +// case 0x18: +// break; case 0x1c: // rom readback address low (low 2 bits always 0) @@ -387,6 +524,7 @@ void zsg2_device::control_w(int reg, uint16_t data) break; default: + logerror("ZSG2 control %02X = %04X\n", reg, data & 0xffff); break; } } @@ -414,7 +552,6 @@ uint16_t zsg2_device::control_r(int reg) return 0; } - /******************************************************************************/ WRITE16_MEMBER(zsg2_device::write) diff --git a/src/devices/sound/zsg2.h b/src/devices/sound/zsg2.h index 0cd4479dca1..10ebb799b6f 100644 --- a/src/devices/sound/zsg2.h +++ b/src/devices/sound/zsg2.h @@ -51,7 +51,6 @@ private: { uint16_t v[16]; bool is_playing; - int16_t *samples; uint32_t cur_pos; uint32_t step_ptr; uint32_t step; @@ -59,12 +58,29 @@ private: uint32_t end_pos; uint32_t loop_pos; uint32_t page; + uint16_t vol; - uint16_t flags; - uint8_t panl; - uint8_t panr; + uint16_t vol_initial; + uint16_t vol_target; + + int16_t emphasis_cutoff; + int16_t emphasis_cutoff_initial; + int16_t emphasis_cutoff_target; + + uint16_t output_cutoff; + uint16_t output_cutoff_initial; + uint16_t output_cutoff_target; + + int32_t emphasis_filter_state; + int32_t output_filter_state; + + // Attenuation for output channels + uint8_t output_gain[4]; + + int16_t samples[5]; // +1 history }; + uint16_t gain_tab[256]; zchan m_chan[48]; required_region_ptr m_mem_base; @@ -83,6 +99,9 @@ private: void control_w(int reg, uint16_t data); uint16_t control_r(int reg); int16_t *prepare_samples(uint32_t offset); + void filter_samples(zchan *ch); + int16_t expand_reg(uint8_t val); + inline int32_t ramp(int32_t current, int32_t target); }; DECLARE_DEVICE_TYPE(ZSG2, zsg2_device) diff --git a/src/mame/audio/taito_zm.cpp b/src/mame/audio/taito_zm.cpp index 5813d5ec30b..818d69281ad 100644 --- a/src/mame/audio/taito_zm.cpp +++ b/src/mame/audio/taito_zm.cpp @@ -187,6 +187,10 @@ MACHINE_CONFIG_START(taito_zoom_device::device_add_mconfig) MCFG_ZSG2_ADD("zsg2", XTAL(25'000'000)) // we assume the parent machine has created lspeaker/rspeaker - MCFG_SOUND_ROUTE(0, "^lspeaker", 1.0) + MCFG_SOUND_ROUTE(0, "^lspeaker", 1.0) // bypass DSP MCFG_SOUND_ROUTE(1, "^rspeaker", 1.0) + + //MCFG_SOUND_ROUTE(2, "^lspeaker", 1.0) // DSP reverb + //MCFG_SOUND_ROUTE(3, "^rspeaker", 1.0) // DSP chorus + MACHINE_CONFIG_END -- cgit v1.2.3