From e0ab279bfa7ec7265c04860e73d6cb18b4746c8a Mon Sep 17 00:00:00 2001 From: m1macrophage <168948267+m1macrophage@users.noreply.github.com> Date: Sun, 9 Nov 2025 00:11:43 -0800 Subject: [PATCH] sound/cem3394.cpp: improved CEM3394 and adapted sente6vb to the changes. Added and improved CEM3394 functionality relevant to the `sixtrak` while working on audio for that. Sending as a separate PR to keep the scope manageable, given there are also changes to `sente6vb`. Incidental improvements to `sente6vb`: * "unlocked" some sounds. An obvious one is the post-tuning chirp for each voice during boot. * Centered audio around 0. sound/cem3394 changes: * Made VCO output range [-1, 1], instead of [0, 1]. * added pulse-width dependent DC level of pulse waveform, as described in the datasheet. * Waveform selection logic now works for voltages outside the bounds provided in the datasheet. * Both the sixtrak and sente6vb will some times use voltages outside those ranges. * Added AC coupling between VCF output and VCA input. * Forcing parameter computation in device_start(). * Added ability to stream voltage inputs. The sixtrak needs that for the VCA gain and VCF frequency. * New configuration interface: * New configure() method computes VCO and VCF zero frequencies based on the attached passive components. * Exposed method for enabling pulse-width limiting for drivers that need it. Now disabled by default. * Fixed compilation for FILTER_TYPE_ESQ1. midway/sente6vb changes: * Using new CEM3394 configuration interface. * Verified that it produces almost the same VCO and VCF zero frequencies. * Reduced CEM3394->speaker gain to compensate for increased loudness of CEM3394 * Old range: [0, 1]. New range: [-1, 1] * Added AC coupling to noise output. * Also changed the noise->CEM3394 gain from 0.5 to 1.0, so that the relative loudness remains the same. * Old range: [0, 1] x 0.5 = [0, 0.5]. New range: [-0.5, 0.5] x 1 --- src/devices/sound/cem3394.cpp | 184 +++++++++++++++++++++++++++++----- src/devices/sound/cem3394.h | 38 +++++-- src/mame/midway/sente6vb.cpp | 14 ++- 3 files changed, 198 insertions(+), 38 deletions(-) diff --git a/src/devices/sound/cem3394.cpp b/src/devices/sound/cem3394.cpp index d2920a333c6f6..342cd6353fd8e 100644 --- a/src/devices/sound/cem3394.cpp +++ b/src/devices/sound/cem3394.cpp @@ -21,11 +21,14 @@ #define FILTER_TYPE FILTER_TYPE_SVTRAP +#define ENABLE_AC_COUPLING 1 + // logging #define LOG_CONTROL_CHANGES (1U << 1) #define LOG_NANS (1U << 2) #define LOG_VALUES (1U << 3) +#define LOG_CONFIG (1U << 4) #define VERBOSE (LOG_NANS) #include "logmacro.h" @@ -51,6 +54,7 @@ static constexpr double EXTERNAL_VOLUME = PULSE_VOLUME; // pulse shaping parameters +// can be enabled with set_limit_pw(true) // examples: // hat trick - skidding ice sounds too loud if minimum width is too big // snake pit - melody during first level too soft if minimum width is too small @@ -130,16 +134,19 @@ DEFINE_DEVICE_TYPE(CEM3394, cem3394_device, "cem3394", "CEM3394 Synthesizer Voic //************************************************************************** //------------------------------------------------- -// cem3394_device - constructor +// cem3394_device - constructor and configuration //------------------------------------------------- cem3394_device::cem3394_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock) : device_t(mconfig, CEM3394, tag, owner, clock), device_sound_interface(mconfig, *this), m_stream(nullptr), + m_inv_sample_rate(1.0 / 48000.0), m_vco_zero_freq(500.0), m_filter_zero_freq(1300.0), - m_values{0}, + m_hpf_k(0), + m_limit_pw(false), + m_values{-1}, // will be initialized in device_start() m_wave_select(0), m_volume(0), m_mixer_internal(0), @@ -152,11 +159,40 @@ cem3394_device::cem3394_device(const machine_config &mconfig, const char *tag, d m_filter_in{0}, m_filter_out{0}, m_pulse_width(0), - m_inv_sample_rate(1.0/48000.0) + m_hpf_mem(0) { - (void)m_filter_in; + // configuring with the example values in the datasheet + configure(270E3, 2E-9, 33E-9, 4.7E-6); } +cem3394_device &cem3394_device::configure(double r_vco, double c_vco, double c_vcf, double c_ac) +{ + // datasheet equation for Fout at CV = 0 + m_vco_zero_freq = 1.3 / (5.0 * r_vco * c_vco); + + // VCO can range up to pow(2, 4.0/.75) = ~40.3 * zero-voltage-freq + const double sample_rate = m_vco_zero_freq * pow(2, 4.0 / 0.75) * 5; + m_inv_sample_rate = 1.0 / sample_rate; + + // datasheet equation for Pzcv + // Note that "4.3 x 10E-5" in the equation should be 4.3E-5. The surrounding + // text and the example walkthrough use the correct value. + m_filter_zero_freq = 4.3E-5 / c_vcf; + + // See hpf() for more info + constexpr double R_AC = 11E3; // internal AC coupling resistor + m_hpf_k = 1.0 - exp((-1 / (R_AC * c_ac)) * m_inv_sample_rate); + + LOGMASKED(LOG_CONFIG, "CEM3394 config - vco zero freq: %f, filter zero freq: %f, sample rate: %d\n", + m_vco_zero_freq, m_filter_zero_freq, int(sample_rate)); + return *this; +} + +cem3394_device &cem3394_device::configure_limit_pw(bool limit_pw) +{ + m_limit_pw = limit_pw; + return *this; +} //------------------------------------------------- // filter - apply the lowpass filter at the given @@ -229,7 +265,7 @@ double cem3394_device::filter(double input, double cutoff) cutoff = std::clamp(cutoff, 50.0, 20000.0); // clamp resonance to 0.95 to prevent infinite gain - double r = 4.0 * std::min(res, 0.95); + double r = 4.0 * std::min(m_filter_resonance, 0.95); // core filter implementation double g = 2 * M_PI * cutoff; @@ -291,6 +327,29 @@ double cem3394_device::filter(double input, double cutoff) #endif +//------------------------------------------------- +// hpf - apply AC coupling to the output of the +// filter, before the signal gets routed to the VCA. +//------------------------------------------------- + +double cem3394_device::hpf(double input) +{ + // The filter's output is AC-coupled to the VCA input. + + // Based on the block diagram in the datasheet, the AC coupling (high-pass + // filtering) is implemented by subtracting a low-pass-filtered signal + // from the original signal. + + // The capacitor of the LPF RC is attached to pin 17, whereas the resistor + // (11 KOhm) is internal to the chip. + + // The LPF code was obtained from sound/flt_rc.cpp. + + m_hpf_mem += (input - m_hpf_mem) * m_hpf_k; // low-pass filtered signal + return input - m_hpf_mem; // HPFed signal = signal - LPFed signal +} + + //------------------------------------------------- // sound_stream_update - generate sound to the mix // buffer in mono @@ -301,9 +360,18 @@ void cem3394_device::sound_stream_update(sound_stream &stream) if (m_wave_select == 0 && m_mixer_external == 0) LOGMASKED(LOG_VALUES, "%f V didn't cut it\n", m_values[WAVE_SELECT]); + const u64 input_mask = get_sound_requested_inputs_mask(); + const bool streaming_cv = input_mask & 0x1fe; + // loop over samples for (int sampindex = 0; sampindex < stream.samples(); sampindex++) { + // take into account any streaming voltage inputs + if (streaming_cv) + for (int i = 1; i < INPUT_COUNT; i++) + if (BIT(input_mask, i)) + set_voltage_internal(i, stream.get(i, sampindex)); + // get the current VCO position and step it forward double vco_position = m_vco_position; m_vco_position += m_vco_step; @@ -312,34 +380,51 @@ void cem3394_device::sound_stream_update(sound_stream &stream) if (m_vco_position >= 1.0) m_vco_position -= floor(m_vco_position); - // handle the pulse component; might need some more thought here + // handle the pulse component double result = 0; if (ENABLE_PULSE && (m_wave_select & WAVE_PULSE)) + { + // The datasheet mentions a "unique circuit [...] that keeps the + // average DC level [...] constant regardless of duty cycle". The + // block diagram shows the pulse signal being subtracted from the + // pulse width. Here, the pulse width is subtracted from the signal + // instead, to ensure a phase consistent with the rest of the signal. if (vco_position < m_pulse_width) - result += PULSE_VOLUME * m_mixer_internal; + result += (1 - m_pulse_width) * PULSE_VOLUME * m_mixer_internal; + else + result += (0 - m_pulse_width) * PULSE_VOLUME * m_mixer_internal; + } // handle the sawtooth component if (ENABLE_SAWTOOTH && (m_wave_select & WAVE_SAWTOOTH)) - result += SAWTOOTH_VOLUME * m_mixer_internal * vco_position; + result += SAWTOOTH_VOLUME * m_mixer_internal * (vco_position - 0.5); // always compute the triangle waveform which is also used for filter modulation double triangle = 2.0 * vco_position; if (triangle > 1.0) triangle = 2.0 - triangle; + triangle -= 0.5; // handle the triangle component if (ENABLE_TRIANGLE && (m_wave_select & WAVE_TRIANGLE)) result += TRIANGLE_VOLUME * m_mixer_internal * triangle; + // convert from [-0.5, 0.5] to [-1, 1] + result *= 2; + // compute extension input (for Bally/Sente this is the noise) - if (ENABLE_EXTERNAL) - result += EXTERNAL_VOLUME * m_mixer_external * stream.get(0, sampindex); + if (ENABLE_EXTERNAL && BIT(input_mask, AUDIO_INPUT)) + result += EXTERNAL_VOLUME * m_mixer_external * stream.get(AUDIO_INPUT, sampindex); // compute the modulated filter frequency and apply the filter // modulation tracks the VCO triangle - double filter_freq = m_filter_frequency * (1 + m_filter_modulation * (triangle - 0.5)); + double filter_freq = m_filter_frequency * (1 + m_filter_modulation * triangle); result = filter(result, filter_freq); + // apply AC coupling + if (ENABLE_AC_COUPLING) + result = hpf(result); + // write the sample stream.put(0, sampindex, result * m_volume); } @@ -353,12 +438,10 @@ void cem3394_device::sound_stream_update(sound_stream &stream) void cem3394_device::device_start() { // compute a sample rate - // VCO can range up to pow(2, 4.0/.75) = ~40.3 * zero-voltage-freq (ZVF) - int sample_rate = m_vco_zero_freq * pow(2, 4.0 / 0.75) * 5; - m_inv_sample_rate = 1.0 / double(sample_rate); + const int sample_rate = int(round(1.0 / m_inv_sample_rate)); - // allocate stream channels, 1 per chip, with one external input - m_stream = stream_alloc(1, 1, sample_rate); + // allocate stream channels + m_stream = stream_alloc(get_sound_requested_inputs(), 1, sample_rate); save_item(NAME(m_values)); save_item(NAME(m_wave_select)); @@ -373,8 +456,17 @@ void cem3394_device::device_start() save_item(NAME(m_filter_frequency)); save_item(NAME(m_filter_modulation)); save_item(NAME(m_filter_resonance)); + save_item(NAME(m_filter_in)); + save_item(NAME(m_filter_out)); save_item(NAME(m_pulse_width)); + + save_item(NAME(m_hpf_mem)); + + // Ensures that m_values, and member variables derived from m_values, are + // properly initialized. Index 0 is unused. + for (int i = 1; i < INPUT_COUNT; i++) + set_voltage_internal(i, 0); } @@ -434,7 +526,7 @@ sound_stream::sample_t cem3394_device::compute_db_volume(double voltage) } -void cem3394_device::set_voltage(int input, double voltage) +void cem3394_device::set_voltage_internal(int input, double voltage) { double temp; @@ -443,9 +535,6 @@ void cem3394_device::set_voltage(int input, double voltage) return; m_values[input] = voltage; - // update the stream first - m_stream->update(); - // switch off the input switch (input) { @@ -456,14 +545,22 @@ void cem3394_device::set_voltage(int input, double voltage) LOGMASKED(LOG_CONTROL_CHANGES, "VCO_FREQ=%6.3fV -> freq=%f\n", voltage, temp); break; - // wave select determines triangle/sawtooth enable + // Wave select chooses between triangle, sawtooth, both, or neither. + // The waveform selection voltages, as specified in the datasheet, are: + // - none: less than -0.5 + // - triangle: -0.5 - -0.2 + // - triangle + sawtooth: 0.9 - 1.5 + // - sawtooth: 2.3 - 3.9 + // However, some systems (such as the Six-Trak) use voltages outside + // those ranges. The logic below uses the midpoint of two boundaries as + // the transition point. case WAVE_SELECT: m_wave_select &= ~(WAVE_TRIANGLE | WAVE_SAWTOOTH); - if (voltage >= -0.5 && voltage <= -0.2) + if (voltage >= -0.5 && voltage < 0.35) m_wave_select |= WAVE_TRIANGLE; - else if (voltage >= 0.9 && voltage <= 1.5) + else if (voltage >= 0.35 && voltage < 1.9) m_wave_select |= WAVE_TRIANGLE | WAVE_SAWTOOTH; - else if (voltage >= 2.3 && voltage <= 3.9) + else if (voltage >= 1.9) m_wave_select |= WAVE_SAWTOOTH; LOGMASKED(LOG_CONTROL_CHANGES, "WAVE_SEL=%6.3fV -> tri=%d saw=%d\n", voltage, (m_wave_select & WAVE_TRIANGLE) ? 1 : 0, (m_wave_select & WAVE_SAWTOOTH) ? 1 : 0); break; @@ -475,10 +572,15 @@ void cem3394_device::set_voltage(int input, double voltage) m_pulse_width = 0; m_wave_select &= ~WAVE_PULSE; } + else if (voltage > 2.0) + { + m_pulse_width = 100; + m_wave_select &= ~WAVE_PULSE; + } else { m_pulse_width = voltage * 0.5; - if (LIMIT_WIDTH) + if (LIMIT_WIDTH && m_limit_pw) m_pulse_width = MINIMUM_WIDTH + (MAXIMUM_WIDTH - MINIMUM_WIDTH) * m_pulse_width; m_wave_select |= WAVE_PULSE; } @@ -534,13 +636,43 @@ void cem3394_device::set_voltage(int input, double voltage) m_filter_resonance = voltage * (1.0 / 2.5); LOGMASKED(LOG_CONTROL_CHANGES, "FLT_RESO=%6.3fV -> mod=%f\n", voltage, m_filter_resonance); break; + + default: + fatalerror("%s - unrecognized input: %d\n", tag(), input); + break; } } +void cem3394_device::set_voltage(int input, double voltage) +{ + if (input < 1 || input >= INPUT_COUNT) + fatalerror("%s - Invalid input to set_voltage(): %d\n", tag(), input); + + if (BIT(get_sound_requested_inputs_mask(), input)) + fatalerror("%s - Cannot call set_voltage(%d, ...). %d is a streaming input.\n", tag(), input, input); + + if (voltage == m_values[input]) + return; + + m_stream->update(); + set_voltage_internal(input, voltage); +} + +double cem3394_device::get_voltage(int input) +{ + if (input < 1 || input >= INPUT_COUNT) + fatalerror("%s - Invalid input to get_voltage(): %d\n", tag(), input); + + if (BIT(get_sound_requested_inputs_mask(), input)) + m_stream->update(); + + return m_values[input]; +} + double cem3394_device::get_parameter(int input) { - double voltage = m_values[input]; + const double voltage = get_voltage(input); switch (input) { diff --git a/src/devices/sound/cem3394.h b/src/devices/sound/cem3394.h index bb109dbc2a2f9..d7e5920c53de0 100644 --- a/src/devices/sound/cem3394.h +++ b/src/devices/sound/cem3394.h @@ -12,20 +12,29 @@ class cem3394_device : public device_t, public device_sound_interface // inputs enum { - VCO_FREQUENCY = 0, + AUDIO_INPUT = 0, // not valid for set_voltage() + VCO_FREQUENCY, MODULATION_AMOUNT, WAVE_SELECT, PULSE_WIDTH, MIXER_BALANCE, FILTER_RESONANCE, FILTER_FREQENCY, - FINAL_GAIN + FINAL_GAIN, + INPUT_COUNT }; - cem3394_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock); + cem3394_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock = 0) ATTR_COLD; - void set_vco_zero_freq(double freq) { m_vco_zero_freq = freq; } - void set_filter_zero_freq(double freq) { m_filter_zero_freq = freq; } + // The constructor will initialize components values to those recommended + // in the datasheet. configure() can be used to change those. + // r_vco: Pin 1 - Resistor to VEE. Sets reference current for the VCO internals. + // c_vco: Pin 4 - VCO timing capacitor. + // c_vcf: Pin 12 (or 13, or 14, they should be equal) - VCF capacitor. + // c_ac: Pin 17 - AC-coupling capacitor on the VCF output. + cem3394_device &configure(double r_vco, double c_vco, double c_vcf, double c_ac) ATTR_COLD; + + cem3394_device &configure_limit_pw(bool limit_pw) ATTR_COLD; protected: // device-level overrides @@ -38,6 +47,9 @@ class cem3394_device : public device_t, public device_sound_interface // Set the voltage going to a particular parameter void set_voltage(int input, double voltage); + // Requesting a streaming voltage will force a stream update. + double get_voltage(int input); + // Get the translated parameter associated with the given input as follows: // VCO_FREQUENCY: frequency in Hz // MODULATION_AMOUNT: scale factor, 0.0 to 2.0 @@ -47,20 +59,30 @@ class cem3394_device : public device_t, public device_sound_interface // FILTER_RESONANCE: resonance, from 0.0 to 1.0 // FILTER_FREQENCY: frequency, in Hz // FINAL_GAIN: gain, in dB + // Requesting a parameter associated with a streaming input will force a + // stream update. double get_parameter(int input); private: double compute_db(double voltage); sound_stream::sample_t compute_db_volume(double voltage); -private: + void set_voltage_internal(int input, double voltage); + double filter(double input, double cutoff); + double hpf(double input); + // device configuration, not needed in save state sound_stream *m_stream; // our stream + double m_inv_sample_rate; // 1/current sample rate double m_vco_zero_freq; // frequency of VCO at 0.0V double m_filter_zero_freq; // frequency of filter at 0.0V + double m_hpf_k; // RC filter coefficient for AC coupling + bool m_limit_pw; // whether to clamp the pulse width. + + // device state - double m_values[8]; // raw values of registers + double m_values[INPUT_COUNT]; // raw values of registers u8 m_wave_select; // flags which waveforms are enabled double m_volume; // linear overall volume (0-1) @@ -78,7 +100,7 @@ class cem3394_device : public device_t, public device_sound_interface double m_pulse_width; // fractional pulse width - double m_inv_sample_rate; // 1/current sample rate + double m_hpf_mem; // AC coupling filter memory }; DECLARE_DEVICE_TYPE(CEM3394, cem3394_device) diff --git a/src/mame/midway/sente6vb.cpp b/src/mame/midway/sente6vb.cpp index 6f875ba10cd1d..f2e9267093334 100644 --- a/src/mame/midway/sente6vb.cpp +++ b/src/mame/midway/sente6vb.cpp @@ -55,10 +55,12 @@ ***************************************************************************/ #include "emu.h" +#include "sound/flt_rc.h" #include "sound/mm5837.h" #include "sente6vb.h" #include "cpu/z80/z80.h" #include "machine/clock.h" +#include "machine/rescap.h" #include "speaker.h" #define LOG_CEM_WRITES (1U << 1) @@ -131,14 +133,18 @@ void sente6vb_device::device_add_mconfig(machine_config &config) mm5837_stream_device &noise(MM5837_STREAM(config, "noise", 0)); // noise.set_vdd(-6.5); // seems too low -- possible the mapping in mm5837 is wrong noise.set_vdd(-8.0); + noise.add_route(ALL_OUTPUTS, "ac_noise", 1.0); + + filter_rc_device &ac_noise(FILTER_RC(config, "ac_noise")); + ac_noise.set_rc(filter_rc_device::HIGHPASS, RES_K(68) + RES_K(1), 0, 0, CAP_U(2.2)); // R19, R20, C115 for (auto &cem_device : m_cem_device) { CEM3394(config, cem_device, 0); - cem_device->set_vco_zero_freq(431.894); - cem_device->set_filter_zero_freq(1300.0); - cem_device->add_route(ALL_OUTPUTS, "mono", 0.90); - noise.add_route(0, *cem_device, 0.5); + cem_device->configure(RES_K(301), CAP_U(0.002), CAP_U(0.033), CAP_U(10)); // R1, C1, C11, C3 on voice 0 (U1) + cem_device->configure_limit_pw(true); + cem_device->add_route(ALL_OUTPUTS, "mono", 0.50); + ac_noise.add_route(0, *cem_device, 1.0); } }