summaryrefslogtreecommitdiffstatshomepage
path: root/src/devices/sound/cem3394.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/devices/sound/cem3394.cpp')
-rw-r--r--src/devices/sound/cem3394.cpp184
1 files changed, 158 insertions, 26 deletions
diff --git a/src/devices/sound/cem3394.cpp b/src/devices/sound/cem3394.cpp
index d2920a333c6..342cd6353fd 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;
@@ -292,6 +328,29 @@ double cem3394_device::filter(double input, double cutoff)
//-------------------------------------------------
+// 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)
{