From 52b77e0df7e2e754b5bdd611dbbaa6187d2f47a6 Mon Sep 17 00:00:00 2001 From: m1macrophage <168948267+m1macrophage@users.noreply.github.com> Date: Tue, 14 Jan 2025 00:00:37 -0800 Subject: oberheim/dmx.cpp: AA filters, VCA as a device, optimizations, bug fix. (#13228) * oberheim/dmx.cpp: AA filters, VCA as a device, optimizations, bug fix. - Refactored the gain and decay logic into its own VCA device, to make it easy to add the anti-aliasing filters. - Added the anti-aliasing / reconstruction filters. - Optimization: Gain and decay-RC-constant variations computed at initialization. - Bugfix: voices with pith control have 1 instead of 3 decay variations. Fixes decay speed in some TOM variations. * dmx: Initializing with non-zero filter params in "default" constructor. Fixes validation errors. --- src/mame/oberheim/dmx.cpp | 402 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 308 insertions(+), 94 deletions(-) diff --git a/src/mame/oberheim/dmx.cpp b/src/mame/oberheim/dmx.cpp index 2e87b31d570..35291f4b07d 100644 --- a/src/mame/oberheim/dmx.cpp +++ b/src/mame/oberheim/dmx.cpp @@ -44,9 +44,9 @@ PCBoards: - 2 x cymbal cards: A single voice that occupies two card slots. Known audio inaccuracies (reasons for MACHINE_IMPERFECT_SOUND): -- No anti-aliasing filters (coming soon). - Closed and Accent hi-hat volume variations might be wrong. See comments in HIHAT_CONFIG (to be researched soon). +- Uncertainty on component values for PERC2 (see comments in PERC_CONFIG). - No metronome yet. - Linear- instead of audio-taper faders. - The DMX stereo output uses fixed panning for each voice. Not yet emulated. @@ -79,6 +79,7 @@ Usage notes: #include "machine/rescap.h" #include "machine/timer.h" #include "sound/dac76.h" +#include "sound/flt_biquad.h" #include "sound/mixer.h" #include "video/dl1416.h" #include "speaker.h" @@ -98,6 +99,8 @@ Usage notes: #include "logmacro.h" +static constexpr const float VCC = 5; + // Voice card configuration. Captures jumper configuration and component values. struct dmx_voice_card_config { @@ -141,8 +144,219 @@ struct dmx_voice_card_config // Early decay enabled when trigger mode is 1. }; const early_decay_mode early_decay; + + // A cascade of 3 Sallen-Key filters. Components ordered from left to right + // on the schematic, which also matches the order in opamp_sk_lowpass_setup. + struct filter_components + { + const float r15; + const float r14; + const float c5; + const float c4; + + const float r13; + const float r18; + const float c6; + const float c7; + + const float r19; + const float r20; + const float c8; + const float c9; + }; + const filter_components filter; +}; + +class dmx_voice_card_vca : public device_t, public device_sound_interface +{ +public: + dmx_voice_card_vca(const machine_config &mconfig, const char *tag, device_t *owner, const dmx_voice_card_config &config) ATTR_COLD; + dmx_voice_card_vca(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD; + + void start(int trigger_mode); + void decay(); + void finish(); + + bool in_decay() const { return m_decaying; } + +protected: + void device_start() override ATTR_COLD; + void device_reset() override ATTR_COLD; + void sound_stream_update(sound_stream &stream, const std::vector &inputs, std::vector &outputs) override; + +private: + void init_gain_and_decay_variations(const dmx_voice_card_config &config) ATTR_COLD; + + bool has_decay() const { return !m_decay_rc_inv.empty(); } + bool has_decay_variations() const { return m_decay_rc_inv.size() > 1; } + + // Configuration. Do not include in save state. + sound_stream *m_stream = nullptr; + const bool m_gain_control; // Is VCA configured for gain variations? + std::vector m_gain; // Gain variations. + std::vector m_decay_rc_inv; // Decay 1/RC variations. + + // Device state. + bool m_running = false; + float m_selected_gain = 1; + bool m_decaying = false; + float m_selected_rc_inv = 1; + attotime m_decay_start_time; }; +DEFINE_DEVICE_TYPE(DMX_VOICE_CARD_VCA, dmx_voice_card_vca, "dmx_voice_card_vca", "DMX Voice Card VCA"); + +dmx_voice_card_vca::dmx_voice_card_vca(const machine_config &mconfig, const char *tag, device_t *owner, const dmx_voice_card_config &config) + : device_t(mconfig, DMX_VOICE_CARD_VCA, tag, owner, 0) + , device_sound_interface(mconfig, *this) + , m_gain_control(!config.pitch_control) +{ + init_gain_and_decay_variations(config); +} + +dmx_voice_card_vca::dmx_voice_card_vca(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) + : device_t(mconfig, DMX_VOICE_CARD_VCA, tag, owner, clock) + , device_sound_interface(mconfig, *this) + , m_gain_control(false) +{ +} + +void dmx_voice_card_vca::start(int trigger_mode) +{ + assert(trigger_mode >= 1 && trigger_mode <= 3); + + m_stream->update(); + m_running = true; + m_decaying = false; + + if (m_gain_control) + m_selected_gain = m_gain[trigger_mode]; + else + m_selected_gain = m_gain[0]; + + if (has_decay_variations()) + m_selected_rc_inv = m_decay_rc_inv[trigger_mode]; + else if (has_decay()) + m_selected_rc_inv = m_decay_rc_inv[0]; + else + m_selected_rc_inv = 1; + + LOGMASKED(LOG_VOLUME, "Selected gain: %f, 1/RC: %f\n", + m_selected_gain, m_selected_rc_inv); +} + +void dmx_voice_card_vca::decay() +{ + assert(has_decay()); + if (!has_decay()) + return; + + m_stream->update(); + m_decaying = true; + m_decay_start_time = machine().time(); +} + +void dmx_voice_card_vca::finish() +{ + m_running = false; +} + +void dmx_voice_card_vca::device_start() +{ + m_stream = stream_alloc(1, 1, machine().sample_rate()); + + save_item(NAME(m_running)); + save_item(NAME(m_selected_gain)); + save_item(NAME(m_decaying)); + save_item(NAME(m_selected_rc_inv)); + save_item(NAME(m_decay_start_time)); + +} + +void dmx_voice_card_vca::device_reset() +{ + m_running = false; + m_selected_gain = 1; + m_decaying = false; + m_selected_rc_inv = 1; +} + +void dmx_voice_card_vca::sound_stream_update(sound_stream &stream, const std::vector &inputs, std::vector &outputs) +{ + const read_stream_view &in = inputs[0]; + write_stream_view &out = outputs[0]; + const int n = in.samples(); + + attotime t = in.start_time() - m_decay_start_time; + assert(!m_decaying || t >= attotime::from_double(0)); + + for (int i = 0; i < n; ++i, t += in.sample_period()) + { + const float decay = m_decaying ? expf(-t.as_double() * m_selected_rc_inv) : 1; + out.put(i, decay * m_selected_gain * in.get(i)); + if (m_running && i == 0) + { + LOGMASKED(LOG_SAMPLES, "Sample: %d, %f, %d, %f, %f\n", + i, m_selected_gain, m_decaying, t.as_double(), decay); + } + } +} + +void dmx_voice_card_vca::init_gain_and_decay_variations(const dmx_voice_card_config &config) +{ + static constexpr const float VD = 0.6; // Diode drop. + static constexpr const float R8 = RES_K(2.7); + static constexpr const float R9 = RES_K(5.6); + static constexpr const float MAX_IREF = 5.0F / (R8 + R9); + + const float r12 = config.r12; + const float r17 = config.r17; + const float c3 = config.c3; + + // Precompute gain variations. + m_gain.clear(); + m_gain.push_back(MAX_IREF); + if (m_gain_control) // Configured for gain variations. + { + // The equations below were derived from Kirchhoff analysis and verified + // with simulations: https://tinyurl.com/22wxwh8h + + // For trigger mode 1. + m_gain.push_back((r12 * r17 * VCC + R8 * r12 * VD + R8 * r17 * VD) / + ((R8 * r12 * r17) + (r12 * r17 * R9) + (R8 * r17 * R9) + (R8 * r12 * R9))); + // For trigger mode 2. + m_gain.push_back((r12 * VCC + R8 * VD) / (r12 * R8 + R8 * R9 + r12 * R9)); + // For trigger mode 3. + m_gain.push_back(m_gain[0]); + } + for (int i = 0; i < m_gain.size(); ++i) + { + LOGMASKED(LOG_VOLUME, "%s: Gain variation %d: %f uA, %f\n", + tag(), i, m_gain[i] * 1e6F, m_gain[i] / MAX_IREF); + m_gain[i] /= MAX_IREF; // Normalize. + } + + // Precompute decay variations. + m_decay_rc_inv.clear(); + if (config.decay != dmx_voice_card_config::decay_mode::DISABLED && c3 > 0) + { + std::vector r_lower; + r_lower.push_back(R9); // For when there are no variations. + if (m_gain_control) + { + r_lower.push_back(RES_3_PARALLEL(R9, r12, r17)); // For trigger mode 1. + r_lower.push_back(RES_2_PARALLEL(R9, r12)); // For trigger mode 2. + r_lower.push_back(R9); // For trigger mode 3. + } + for (float r : r_lower) + { + m_decay_rc_inv.push_back(1.0F / ((R8 + r) * c3)); + LOGMASKED(LOG_VOLUME, "%s: Decay 1/RC variation %d: %f\n", + tag(), m_decay_rc_inv.size() - 1, m_decay_rc_inv.back()); + } + } +} + // Emulates the original DMX voice cards, including the cymbal card. Later // DMX models shipped with the "Mark II" voice cards for the Tom voices. // The Mark II cards are not yet emulated. @@ -169,7 +383,6 @@ private: void init_pitch() ATTR_COLD; void compute_pitch_variations(); void select_pitch(); - void configure_volume(); bool is_decay_enabled() const; bool is_early_decay_enabled() const; @@ -179,6 +392,8 @@ private: required_device m_timer; // 555, U5. required_device m_dac; // AM6070, U8. Compatible with DAC76. + required_device m_vca; + required_device_array m_filters; // Configuration. Do not include in save state. const dmx_voice_card_config m_config; @@ -188,45 +403,35 @@ private: s32 m_t1_percent = T1_DEFAULT_PERCENT; // Device state. - bool m_counting = false; u16 m_counter = 0; // 4040 counter. - u8 m_trigger_mode = 0; // Valid modes: 1-3. - float m_volume = 0; // Volume attenuation: 0-1. - - bool m_decaying = false; - attotime m_decay_start_time; - - static constexpr const float VCC = 5; - static constexpr const float R8 = RES_K(2.7); - static constexpr const float R9 = RES_K(5.6); + u8 m_trigger_mode = 0; // Valid modes: 1-3. 0 OK after reset. }; DEFINE_DEVICE_TYPE(DMX_VOICE_CARD, dmx_voice_card, "dmx_voice_card", "DMX Voice Card"); -dmx_voice_card::dmx_voice_card( - const machine_config &mconfig, - const char *tag, device_t *owner, - const dmx_voice_card_config &config, - required_memory_region *sample_rom) +dmx_voice_card::dmx_voice_card(const machine_config &mconfig, const char *tag, device_t *owner, const dmx_voice_card_config &config, required_memory_region *sample_rom) : device_t(mconfig, DMX_VOICE_CARD, tag, owner, 0) , device_sound_interface(mconfig, *this) , m_timer(*this, "555_u5") , m_dac(*this, "dac_u8") + , m_vca(*this, "dmx_vca") + , m_filters(*this, "aa_sk_filter_%d", 0) , m_config(config) , m_sample_rom(sample_rom) { init_pitch(); } -dmx_voice_card::dmx_voice_card( - const machine_config &mconfig, const char *tag, device_t *owner, - uint32_t clock) +dmx_voice_card::dmx_voice_card(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, DMX_VOICE_CARD, tag, owner, clock) , device_sound_interface(mconfig, *this) , m_timer(*this, "555_u5") , m_dac(*this, "dac_u8") - , m_config(dmx_voice_card_config{}) + , m_vca(*this, "dmx_vca") + , m_filters(*this, "aa_sk_filter_%d", 0) + // Need non-zero entries for the filter for validation to pass. + , m_config(dmx_voice_card_config{.filter={1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}) { } @@ -245,16 +450,11 @@ void dmx_voice_card::trigger(bool tr0, bool tr1) m_stream->update(); m_counter = 0; m_counting = true; - m_volume = 1; - m_decaying = false; - if (m_config.pitch_control) select_pitch(); - else - configure_volume(); + m_vca->start(m_trigger_mode); - LOGMASKED(LOG_SOUND, "Trigger: (%d, %d) %d %f\n", - tr0, tr1, m_trigger_mode, m_volume); + LOGMASKED(LOG_SOUND, "Trigger: (%d, %d) %d %f\n", tr0, tr1, m_trigger_mode); } void dmx_voice_card::set_pitch_adj(s32 t1_percent) @@ -266,8 +466,28 @@ void dmx_voice_card::set_pitch_adj(s32 t1_percent) void dmx_voice_card::device_add_mconfig(machine_config &config) { + static constexpr const double SK_R3 = RES_M(999.99); + static constexpr const double SK_R4 = RES_R(0.001); + TIMER(config, m_timer).configure_generic(FUNC(dmx_voice_card::clock_callback)); - DAC76(config, m_dac, 0U).add_route(ALL_OUTPUTS, *this, 1.0); + DAC76(config, m_dac, 0U); + DMX_VOICE_CARD_VCA(config, m_vca, m_config); + + FILTER_BIQUAD(config, m_filters[0]).opamp_sk_lowpass_setup( + m_config.filter.r15, m_config.filter.r14, SK_R3, SK_R4, + m_config.filter.c5, m_config.filter.c4); + FILTER_BIQUAD(config, m_filters[1]).opamp_sk_lowpass_setup( + m_config.filter.r13, m_config.filter.r18, SK_R3, SK_R4, + m_config.filter.c6, m_config.filter.c7); + FILTER_BIQUAD(config, m_filters[2]).opamp_sk_lowpass_setup( + m_config.filter.r19, m_config.filter.r20, SK_R3, SK_R4, + m_config.filter.c8, m_config.filter.c9); + + m_dac->add_route(ALL_OUTPUTS, m_vca, 1.0); + m_vca->add_route(ALL_OUTPUTS, m_filters[0], 1.0); + m_filters[0]->add_route(ALL_OUTPUTS, m_filters[1], 1.0); + m_filters[1]->add_route(ALL_OUTPUTS, m_filters[2], 1.0); + m_filters[2]->add_route(ALL_OUTPUTS, *this, 1.0); } void dmx_voice_card::device_start() @@ -277,9 +497,6 @@ void dmx_voice_card::device_start() save_item(NAME(m_counting)); save_item(NAME(m_counter)); save_item(NAME(m_trigger_mode)); - save_item(NAME(m_volume)); - save_item(NAME(m_decaying)); - save_item(NAME(m_decay_start_time)); } void dmx_voice_card::device_reset() @@ -287,45 +504,17 @@ void dmx_voice_card::device_reset() m_trigger_mode = 0; reset_counter(); compute_pitch_variations(); - configure_volume(); } -void dmx_voice_card::sound_stream_update( - sound_stream &stream, - const std::vector &inputs, - std::vector &outputs) +void dmx_voice_card::sound_stream_update(sound_stream &stream, const std::vector &inputs, std::vector &outputs) { - const read_stream_view &in = inputs[0]; - write_stream_view &out = outputs[0]; - - float rc_inv = 0; - if (m_decaying) - { - float r_lower = R9; - if (m_trigger_mode == 1) - r_lower = RES_3_PARALLEL(R9, m_config.r12, m_config.r17); - else if (m_trigger_mode == 2) - r_lower = RES_2_PARALLEL(R9, m_config.r12); - rc_inv = 1.0F / ((R8 + r_lower) * m_config.c3); - } - - const int n = in.samples(); - attotime t = in.start_time() - m_decay_start_time; - for (int i = 0; i < n; ++i, t += in.sample_period()) - { - const float decay = m_decaying ? expf(-t.as_double() * rc_inv) : 1.0F; - out.put(i, decay * m_volume * in.get(i)); - if (m_counting && i == 0) - { - LOGMASKED(LOG_SAMPLES, "Sample: %d, %d, %f, %d, %f, %f\n", - i, m_counter, m_volume, m_decaying, t.as_double(), decay); - } - } + const int n = inputs[0].samples(); + for (int i = 0; i < n; ++i) + outputs[0].put(i, inputs[0].get(i)); } void dmx_voice_card::reset_counter() { - m_stream->update(); m_counter = 0; m_counting = false; } @@ -451,32 +640,6 @@ void dmx_voice_card::select_pitch() 1.0 / sampling_t.as_double()); } -void dmx_voice_card::configure_volume() -{ - // The equations below were derived from Kirchhoff analysis and verified - // with simulations: https://tinyurl.com/22wxwh8h - - static constexpr const float VD = 0.6; // Diode drop. - static constexpr const float MAX_IREF = 5.0 / (R8 + R9); - - const float r12 = m_config.r12; - const float r17 = m_config.r17; - - float i_ref = MAX_IREF; - if (m_trigger_mode == 1) - { - i_ref = (r12 * r17 * VCC + R8 * r12 * VD + R8 * r17 * VD) / - ((R8 * r12 * r17) + (r12 * r17 * R9) + (R8 * r17 * R9) + (R8 * r12 * R9)); - } - else if (m_trigger_mode == 2) - { - i_ref = (r12 * VCC + R8 * VD) / (r12 * R8 + R8 * R9 + r12 * R9); - } - - m_volume = i_ref / MAX_IREF; - LOGMASKED(LOG_VOLUME, "Volume %d, Iref: %f uA\n", m_volume, i_ref * 1e6f); -} - bool dmx_voice_card::is_decay_enabled() const { switch (m_config.decay) @@ -515,6 +678,7 @@ TIMER_DEVICE_CALLBACK_MEMBER(dmx_voice_card::clock_callback) if (m_counter >= max_count) { reset_counter(); + m_vca->finish(); LOGMASKED(LOG_SOUND, "Done counting %d\n\n", m_config.split_rom); } @@ -536,14 +700,12 @@ TIMER_DEVICE_CALLBACK_MEMBER(dmx_voice_card::clock_callback) // the counter's bit 10 transitions to 1. static constexpr const u16 LATE_DECAY_START = 1 << 10; - if (!m_decaying && is_decay_enabled()) + if (!m_vca->in_decay() && is_decay_enabled()) { if ((is_early_decay_enabled() && m_counter >= EARLY_DECAY_START) || m_counter >= LATE_DECAY_START) { - m_stream->update(); - m_decaying = true; - m_decay_start_time = machine().time(); + m_vca->decay(); } } } @@ -553,6 +715,47 @@ namespace { constexpr const char MAINCPU_TAG[] = "z80"; constexpr const char NVRAM_TAG[] = "nvram"; +// There are two anti-aliasing / reconstruction filter setups used. One for +// lower- and one for higher-frequency voices. + +// ~10Khz cuttoff. +constexpr const dmx_voice_card_config::filter_components FILTER_CONFIG_LOW = +{ + .r15 = RES_K(6.8), + .r14 = RES_K(6.2), + .c5 = CAP_U(0.01), + .c4 = CAP_U(0.0047), + + .r13 = RES_K(9.1), + .r18 = RES_K(9.1), + .c6 = CAP_U(0.01), + .c7 = CAP_P(560), + + .r19 = RES_K(5.1), + .r20 = RES_K(5.1), + .c8 = CAP_U(0.047), + .c9 = CAP_P(200), +}; + +// ~16 KHz cuttoff. +constexpr const dmx_voice_card_config::filter_components FILTER_CONFIG_HIGH = +{ + .r15 = RES_K(4.7), + .r14 = RES_K(4.7), + .c5 = CAP_U(0.01), + .c4 = CAP_U(0.0047), + + .r13 = RES_K(5.6), + .r18 = RES_K(5.6), + .c6 = CAP_U(0.01), + .c7 = CAP_P(560), + + .r19 = RES_K(3.3), + .r20 = RES_K(3.3), + .c8 = CAP_U(0.047), + .c9 = CAP_P(200), +}; + // Voice card configurations below reverse R12 and R17 compared to the 1981 // schematics. See comments in dmx_voice_card_config for more on this. @@ -567,6 +770,7 @@ constexpr const dmx_voice_card_config BASS_CONFIG = .pitch_control = false, .decay = dmx_voice_card_config::decay_mode::ENABLED, .early_decay = dmx_voice_card_config::early_decay_mode::ENABLED, + .filter = FILTER_CONFIG_LOW, }; constexpr const dmx_voice_card_config SNARE_CONFIG = @@ -580,6 +784,7 @@ constexpr const dmx_voice_card_config SNARE_CONFIG = .pitch_control = false, .decay = dmx_voice_card_config::decay_mode::DISABLED, .early_decay = dmx_voice_card_config::early_decay_mode::ENABLED_ON_TR1, + .filter = FILTER_CONFIG_HIGH, }; // Some component values in HIHAT_CONFIG might be wrong. @@ -600,6 +805,7 @@ constexpr const dmx_voice_card_config HIHAT_CONFIG = .pitch_control = false, .decay = dmx_voice_card_config::decay_mode::ENABLED_ON_TR12, .early_decay = dmx_voice_card_config::early_decay_mode::ENABLED_ON_TR1, + .filter = FILTER_CONFIG_HIGH, }; constexpr const dmx_voice_card_config TOM_CONFIG = @@ -613,10 +819,16 @@ constexpr const dmx_voice_card_config TOM_CONFIG = .pitch_control = true, .decay = dmx_voice_card_config::decay_mode::ENABLED, .early_decay = dmx_voice_card_config::early_decay_mode::ENABLED, + .filter = FILTER_CONFIG_LOW, }; constexpr dmx_voice_card_config PERC_CONFIG(float c2) { + // The schematic and electrongate.com agree on all components for PERC1, + // but disagree for PERC2: + // * C2: 0.0033uF in schematic, but 0.0047uF on electrongate.com. + // * Filter: "high" configuration in the schematic, but "low" on + // electrongate.com. return { .c2 = c2, @@ -628,6 +840,7 @@ constexpr dmx_voice_card_config PERC_CONFIG(float c2) .pitch_control = false, .decay = dmx_voice_card_config::decay_mode::DISABLED, .early_decay = dmx_voice_card_config::early_decay_mode::ENABLED_ON_TR1, + .filter = FILTER_CONFIG_HIGH, }; } @@ -642,6 +855,7 @@ constexpr const dmx_voice_card_config CYMBAL_CONFIG = .pitch_control = false, .decay = dmx_voice_card_config::decay_mode::DISABLED, .early_decay = dmx_voice_card_config::early_decay_mode::ENABLED_ON_TR1, + .filter = FILTER_CONFIG_HIGH, }; -- cgit v1.2.3