diff options
| author | 2026-04-12 07:23:17 -0700 | |
|---|---|---|
| committer | 2026-04-12 16:23:17 +0200 | |
| commit | 0f5872aa2eeb46ae87c6a715946cce1704749d84 (patch) | |
| tree | a72bd7ac2a5280069db889a87f2d39ab3f68c0a6 /src/devices | |
| parent | 13493035a704249a425828ae0316eb4dcab0b551 (diff) | |
sound/va_vca.cpp: support for differential inputs. prophet5.cpp: VCA balance trimmers. (#15215)
Diffstat (limited to 'src/devices')
| -rw-r--r-- | src/devices/sound/va_vca.cpp | 236 | ||||
| -rw-r--r-- | src/devices/sound/va_vca.h | 121 |
2 files changed, 252 insertions, 105 deletions
diff --git a/src/devices/sound/va_vca.cpp b/src/devices/sound/va_vca.cpp index b74d6f57b2f..621f13b10ef 100644 --- a/src/devices/sound/va_vca.cpp +++ b/src/devices/sound/va_vca.cpp @@ -25,13 +25,18 @@ va_vca_device::va_vca_device(const machine_config &mconfig, device_type type, co , device_sound_interface(mconfig, *this) , m_stream(nullptr) , m_fixed_gain(1.0F) + , m_fixed_inv_input(0.0F) { } +void va_vca_device::update() +{ + if (m_stream) + m_stream->update(); +} + void va_vca_device::set_fixed_gain_cv(float gain_cv) { - if (!m_stream) - fatalerror("%s: set_fixed_gain_cv() cannot be called before device_start()\n", tag()); if (BIT(get_sound_requested_inputs_mask(), INPUT_GAIN)) fatalerror("%s: Cannot set a fixed gain CV when streaming it.\n", tag()); @@ -39,73 +44,83 @@ void va_vca_device::set_fixed_gain_cv(float gain_cv) if (gain == m_fixed_gain) return; - m_stream->update(); + update(); m_fixed_gain = gain; } +void va_vca_device::set_fixed_inv_input(float x) +{ + if (BIT(get_sound_requested_inputs_mask(), INPUT_AUDIO_INV)) + fatalerror("%s: Cannot set the inverting input when streaming it.\n", tag()); + + if (x == m_fixed_inv_input) + return; + + update(); + m_fixed_inv_input = x; +} + +float va_vca_device::diff(float p, float m) const +{ + return p - m; +} + float va_vca_device::cv_to_gain(float cv) const { return cv; } -float va_vca_device::distorted(float s) const +u32 va_vca_device::preferred_sample_rate() const { - fatalerror("A va_vca_device subclass enabled distortion without implementing the distorted() function.\n"); - return s; + return SAMPLE_RATE_OUTPUT_ADAPTIVE; } void va_vca_device::device_start() { - if (get_sound_requested_inputs_mask() != 0x01 && get_sound_requested_inputs_mask() != 0x03) - fatalerror("%s: Input 0 must be connected, input 1 can optionally be connected. No other inputs allowed.\n", tag()); + if (!BIT(get_sound_requested_inputs_mask() , INPUT_AUDIO)) + fatalerror("%s: Input 0 must be connected.", tag()); + if (get_sound_requested_inputs_mask() & ~u64(0x07)) + fatalerror("%s: Only inputs 0-2 can be connected.", tag()); + + m_stream = stream_alloc(get_sound_requested_inputs(), 1, preferred_sample_rate()); - // Upsample when using distortion, to reduce aliasing. - const u32 sample_rate = has_distortion() ? std::max(96000, machine().sample_rate()) : SAMPLE_RATE_OUTPUT_ADAPTIVE; - m_stream = stream_alloc(get_sound_requested_inputs(), 1, sample_rate); save_item(NAME(m_fixed_gain)); + save_item(NAME(m_fixed_inv_input)); } void va_vca_device::sound_stream_update(sound_stream &stream) { - if (has_distortion()) - { - if (BIT(get_sound_requested_inputs_mask(), INPUT_GAIN)) - { - for (int i = 0; i < stream.samples(); i++) - stream.put(0, i, distorted(stream.get(INPUT_AUDIO, i)) * cv_to_gain(stream.get(INPUT_GAIN, i))); - } - else - { - for (int i = 0; i < stream.samples(); i++) - stream.put(0, i, distorted(stream.get(INPUT_AUDIO, i)) * m_fixed_gain); - } - } - else + const int n = stream.samples(); + const bool streaming_gain = BIT(get_sound_requested_inputs_mask(), INPUT_GAIN); + const bool streaming_inv = BIT(get_sound_requested_inputs_mask(), INPUT_AUDIO_INV); + + for (int i = 0; i < n; ++i) { - if (BIT(get_sound_requested_inputs_mask(), INPUT_GAIN)) - { - for (int i = 0; i < stream.samples(); i++) - stream.put(0, i, stream.get(INPUT_AUDIO, i) * cv_to_gain(stream.get(INPUT_GAIN, i))); - } - else - { - for (int i = 0; i < stream.samples(); i++) - stream.put(0, i, stream.get(INPUT_AUDIO, i) * m_fixed_gain); - } + const float plus = stream.get(INPUT_AUDIO, i); + const float minus = streaming_inv ? stream.get(INPUT_AUDIO_INV, i) : m_fixed_inv_input; + const float gain = streaming_gain ? cv_to_gain(stream.get(INPUT_GAIN, i)) : m_fixed_gain; + stream.put(0, i, gain * diff(plus, minus)); } } ca3280_vca_device::ca3280_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : va_vca_device(mconfig, CA3280_VCA, tag, owner, 0) - , m_input_scale(1) + , m_plus_scale(1) + , m_minus_scale(1) , m_output_scale(1) { } -ca3280_vca_device &ca3280_vca_device::configure_input_divider(float r_in, float r_gnd) +ca3280_vca_device &ca3280_vca_device::configure_plus_divider(float r_in, float r_gnd) +{ + m_plus_scale = r_gnd / (r_in + r_gnd); + return *this; +} + +ca3280_vca_device &ca3280_vca_device::configure_minus_divider(float r_in, float r_gnd) { - m_input_scale = r_gnd / (r_in + r_gnd); + m_minus_scale = r_gnd / (r_in + r_gnd); return *this; } @@ -126,25 +141,32 @@ ca3280_vca_device &ca3280_vca_device::configure_voltage_output(float r_out) // DVIn ~ (Vin+ - Vin-). Vin- is typically 0V. // VT ~ Thermal voltage. A temperature-dependent constant. +float ca3280_vca_device::diff(float p, float m) const +{ + constexpr float VT = 25.7E-3; // Thermal voltage at 25 degrees C. + return tanhf((m_plus_scale * p - m_minus_scale * m) / (2 * VT)); +} + float ca3280_vca_device::cv_to_gain(float cv) const { // cv = Iabc. return CA3280_IOUT_MAX_SCALE * cv * m_output_scale; } -float ca3280_vca_device::distorted(float s) const +u32 ca3280_vca_device::preferred_sample_rate() const { - constexpr float VT = 25.7E-3; // Thermal voltage at 25 degrees C. - return tanhf((s * m_input_scale) / (2 * VT)); + // Upsample to reduce aliasing due to tanh() distortion. + return std::max(96000, machine().sample_rate()); } - ca3280_vca_lin_device::ca3280_vca_lin_device(const machine_config &mconfig, const char *tag, device_t *owner, float i_d) : va_vca_device(mconfig, CA3280_VCA_LIN, tag, owner, 0) - , m_i_d_inv(1.0F / i_d) - , m_input_scale(1) + , m_id(i_d) , m_output_scale(1) , m_cv_scale(1) + , m_rp(-1) + , m_rm(-1) + , m_r_inv(-1) { update_cv_scale(); } @@ -159,13 +181,6 @@ ca3280_vca_lin_device::ca3280_vca_lin_device(const machine_config &mconfig, cons { } -ca3280_vca_lin_device &ca3280_vca_lin_device::configure_voltage_input(float r_in) -{ - m_input_scale = 1.0F / r_in; - update_cv_scale(); - return *this; -} - ca3280_vca_lin_device &ca3280_vca_lin_device::configure_voltage_output(float r_out) { // output voltage = output_current * R, where R is the resistor to ground @@ -175,28 +190,127 @@ ca3280_vca_lin_device &ca3280_vca_lin_device::configure_voltage_output(float r_o return *this; } -// The output current for a linearized OTA can be determined by: -// Iout = Ioutmax * Iin / Id -// For the CA3280: -// Ioutmax = CA3280_IOUT_MAX_SCALE * Iabc +ca3280_vca_lin_device &ca3280_vca_lin_device::set_rplus(float r) +{ + assert(r > 0); + if (r == m_rp) + return *this; + update(); + m_rp = r; + m_r_inv = 1.0F / (m_rp + m_rm); + return *this; +} + +ca3280_vca_lin_device &ca3280_vca_lin_device::set_rminus(float r) +{ + assert(r > 0); + if (r == m_rm) + return *this; + update(); + m_rm = r; + m_r_inv = 1.0F / (m_rp + m_rm); + return *this; +} + +// The output current for a linearized OTA is: +// Iout = Idiff * s * Iabc / Id // Where: +// s ~ an OTA-specific scaling factor (e.g. CA3280_IOUT_MAX_SCALE) // Iabc ~ transconductance (gain) control current. -// Iin ~ current at in+, assuming in- is connected to ground (via a -// resistor). // Id ~ diode current. The current supplied to the Id pin. +// Idiff ~ differential input current. See diff() below. +// Returns (s * Iabc / Id), possibly scaled to account for the current-to-voltage +// conversion, if so configured. float ca3280_vca_lin_device::cv_to_gain(float cv) const { // cv = Iabc. - // m_cv_scale incorporates 1 / Id, along with the optional factors to - // convert the input voltage to a current, and the output current to a - // a voltage (see update_cv_scale() and the various configure_*() methods). return cv * m_cv_scale; } void ca3280_vca_lin_device::update_cv_scale() { - m_cv_scale = CA3280_IOUT_MAX_SCALE * m_input_scale * m_i_d_inv * m_output_scale; + m_cv_scale = CA3280_IOUT_MAX_SCALE * m_output_scale / m_id; +} + +// Idiff = Im - Ip +// Where: +// Im ~ current flowing out of the "-" input. +// Ip ~ current flowing out of the "+" input. +// +// See also page 3 (Background section) of +// http://www.openmusiclabs.com/files/otadist.pdf +// +// This diagram shows the part of the OTA that generates Im and Ip. While this +// circuit is typical of OTAs with linearizing diodes (e.g. LM13700), the +// CA3280's relevant circuit is more complex (see datasheet). But it is +// functionally similar. +// +// Id +// OTA | <- Vtop +// ----------------- +// | | | +// | +---+---+ | +// | | | | +// | D D | +// | v v | +// | | | | +// ----------------- +// +| |- +// | | +// Ip | Rp Rm | Im +// v | | v +// Vp Vm +// +// [1] Id = Ip + Im => Im = Id - Ip +// [2] VDp = Vt * log(Ip/Is) +// [3] VDm = Vt * log(Im/Is) +// [4] Vtop = Vp + Ip * Rp + VDp +// [5] Vtop = Vm + Im * Rm + VDm +// [6] Is = Diode saturation current. +// +// Use [4] to solve for Ip and [5] to solve for Im, using the other equations to +// substitue variables: +// Ip = (Id * Rm - (Vp - Vm) + Vt * log(Im/Ip)) / (Rp + Rm) +// Im = (Id * Rp + (vp - Vm) + Vt * log(Ip/Im)) / (Rp + Rm) +// +// For arbitrary Rp and Rm, the above do not have closed form solutions. +// However, in typical applications, Id * Rm >> Vt * log(Im/Ip) and +// Id * Rp >> Vt * log(Ip/Im), so we can pretend the diodes are not there to get +// the approximations: +// Ip ~= (Id * Rm - (Vp - Vm)) / (Rp + Rm) +// Im ~= (Id * Rp + (vp - Vm)) / (Rp + Rm) +// Idiff ~= Im - Ip +// +// If Rp = Rm = R, then the log terms cancel out and we get: +// Idiff = Im - Ip = (Vp - Vm) / R +// +// Importantly, when Rp = Rm (which is typical), we don't need the +// "Id * Rp >> ..." assumptions to hold, so emulation accuracy is higher. +// +float ca3280_vca_lin_device::diff(float p, float m) const +{ + // p = Vp, m = Vm + assert(m_rp > 0 && m_rm > 0 && m_r_inv > 0); + const float dv = p - m; + const float ip = (m_id * m_rm - dv) * m_r_inv; + const float im = (m_id * m_rp + dv) * m_r_inv; + return im - ip; +} + +void ca3280_vca_lin_device::device_start() +{ + va_vca_device::device_start(); + save_item(NAME(m_rp)); + save_item(NAME(m_rm)); + save_item(NAME(m_r_inv)); +} + +void ca3280_vca_lin_device::device_reset() +{ + va_vca_device::device_reset(); + if (m_rp <= 0 || m_rm <= 0 || m_r_inv <= 0) + fatalerror("%s requires both set_rplus() and set_rminus() to have been called.\n", tag()); } float ca3280_vca_lin_device::i_d(float r, float v_r, float v_minus) diff --git a/src/devices/sound/va_vca.h b/src/devices/sound/va_vca.h index 2ae9dbe9073..3938fd0ee19 100644 --- a/src/devices/sound/va_vca.h +++ b/src/devices/sound/va_vca.h @@ -44,6 +44,9 @@ DECLARE_DEVICE_TYPE(CA3280_VCA_LIN, ca3280_vca_lin_device) // (input 1), by a device in va_eg.h, for example. When the cv is provided via a // stream, this is also a ring modulator. // +// This can also emulate a differential amplifier, by supplying a fixed or +// streaming inverting input. +// // The CV for this device is simply the gain (output = gain * input). class va_vca_device : public device_t, public device_sound_interface { @@ -52,6 +55,7 @@ public: { INPUT_AUDIO = 0, INPUT_GAIN, + INPUT_AUDIO_INV, }; va_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD; @@ -60,12 +64,24 @@ public: // each (sub)class documentation. void set_fixed_gain_cv(float gain_cv); + // Sets the inverting (-) input to a fixed value. Defaults to 0. + void set_fixed_inv_input(float x); + + // Applies input differencing. This happens before gain is applied. + // Subclasses can override diff() to apply additional processing such as + // prescaling and distortion. + // Public because it is useful for calibrating and debugging. + // p ~ non-inverting input ("plus"). + // m ~ inverting input ("minus"). + virtual float diff(float p, float m) const; + protected: va_vca_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, uint32_t clock) ATTR_COLD; + void update(); + virtual float cv_to_gain(float cv) const; - virtual float distorted(float s) const; - virtual bool has_distortion() const { return false; } + virtual u32 preferred_sample_rate() const; void device_start() override ATTR_COLD; void sound_stream_update(sound_stream &stream) override; @@ -73,6 +89,7 @@ protected: private: sound_stream *m_stream; float m_fixed_gain; + float m_fixed_inv_input; }; @@ -84,27 +101,30 @@ private: // considered linear for very small input voltages (~[-0.02 - 0.02]). See // figure 3B in the datasheet. // -// ______ Iabc -// | \ | -// A --- Rin ---+--- B --- |+ \ | -// | | \ | -// Rgnd | \ | -// | | OO---- C ----+ -// GND | / | -// | / Rout -// +--------- |- Id / | -// | |___|__/ GND -// Rgnd | -// | | -// GND V- +// ______ Iabc +// | \ | +// A --- Rin+ ---+--- B --- |+ \ | +// | | \ | +// Rgnd+ | \ | +// | | OO---- F ----+ +// GND | / | +// | / Rout +// [D -- Rin- --]+--- E --- |- Id / | +// | |___|__/ GND +// Rgnd- | +// | | +// GND V- // // The streaming input should either be the voltage at point B, or at point A, // depending on how the object is configured. // +// An (optional) inverting input (fixed or streaming) is also supported. It +// should be the voltage at point E, or at point D, depending on configuration. +// // The gain CV (fixed or streaming) should be the control current (Iabc), in // Amperes. // -// The streaming output will be the output current at point C (the CA3280 is a +// The streaming output will be the output current at point F (the CA3280 is a // current source), but the object can be configured to output a voltage instead. // class ca3280_vca_device : public va_vca_device @@ -112,22 +132,27 @@ class ca3280_vca_device : public va_vca_device public: ca3280_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD; - // By default, the streaming input should be the voltage at point B. - // This method will configure the input to be the voltage at point A. - ca3280_vca_device &configure_input_divider(float r_in, float r_gnd) ATTR_COLD; + // By default, the inputs should be the voltages at points B and E. + // A common setup is to have voltage dividers at the input(s), which can be + // configured by these methods. If dividers are configured, the inputs + // should be the voltages at points A and D. + ca3280_vca_device &configure_plus_divider(float r_in, float r_gnd) ATTR_COLD; + ca3280_vca_device &configure_minus_divider(float r_in, float r_gnd) ATTR_COLD; - // By default, the streaming output will be the current at point C. This - // method will configure the output to be the voltage at point C. + // By default, the streaming output will be the current at point F. This + // method will configure the output to be the voltage at point F. ca3280_vca_device &configure_voltage_output(float r_out) ATTR_COLD; + float diff(float p, float m) const override; + protected: float cv_to_gain(float cv) const override; - float distorted(float s) const override; - bool has_distortion() const override { return true; } + u32 preferred_sample_rate() const override; private: // Configuration. Not needed in save state. - float m_input_scale; + float m_plus_scale; + float m_minus_scale; float m_output_scale; }; @@ -136,36 +161,31 @@ private: // to a current source, often a resistor to the positive supply. // // In this configuration, the CA3280 is linear for a wide voltage range, but it -// will hard-clip when Iout approaches Iabc (TODO: clipping is not currently +// will hard-clip when Iin approaches Id (TODO: clipping is not currently // emulated). See figure 3A in the datasheet. // -// Note that, in contrast to the non-linearized configuration above, it is more -// appropriate to treat the inputs as current inputs, rather than voltage inputs. -// // ______ Iabc // | \ | -// A --- Rin+ --- B --- |+ \ | +// A ---- Rin+ ---- |+ \ | // | \ | // | \ | -// | OO---- C ----+ +// | OO---- F ----+ // | / | // | / Rout -// GND --- Rin- --- |- Id / | +// B ---- Rin- ---- |- Id / | // |___|__/ GND // | // + ---- Rd --- V+ // -// The streaming input should be the *current* at point B (the + input is at near -// ground potential). But it can be configured to be the voltage at point A. +// Usually, Rin+ = Rin- and B = GND. When that's the case: +// In = V_A / Rin+ and Iout = In * 0.82 * Iabc / Id. // // The gain CV (fixed or streaming) should be the control current (Iabc), in // Amperes. // -// The streaming output will be the output current at point C (the CA3280 is a +// The streaming output will be the output current at point F (the CA3280 is a // current source), but the object can be configured to output a voltage instead. // -// This implementation assumes Rin- == Rin+, which is typical. -// class ca3280_vca_lin_device : public va_vca_device { public: @@ -182,27 +202,40 @@ public: // Don't use. ca3280_vca_lin_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) ATTR_COLD; - // By default, the streaming input should be the *current* at point B. - // This method will configure the input to be the voltage at point A. - ca3280_vca_lin_device &configure_voltage_input(float r_in) ATTR_COLD; - - // By default, the streaming output will be the current at point C. This - // method will configure the output to be the voltage at point C. + // By default, the streaming output will be the current at point F. This + // method will configure the output to be the voltage at point F. ca3280_vca_lin_device &configure_voltage_output(float r_out) ATTR_COLD; + // These set Rin+ and Rin-. They must be called at least once and they + // can change at runtime. + // If the input voltage is applied though a resistor network, such as a + // trimming circuit, use the Thevenin-equivalent resistance. Remember to + // also feed the Thevenin-equivalent voltage to the input(s) in that case. + ca3280_vca_lin_device &set_rplus(float r); + ca3280_vca_lin_device &set_rminus(float r); + + float diff(float p, float m) const override; + protected: float cv_to_gain(float cv) const override; + void device_start() override ATTR_COLD; + void device_reset() override ATTR_COLD; + private: void update_cv_scale(); static float i_d(float r, float v_r, float v_minus) ATTR_COLD; // Configuration. Not needed in save state. - const float m_i_d_inv; - float m_input_scale; + const float m_id; float m_output_scale; float m_cv_scale; + + // state + float m_rp; + float m_rm; + float m_r_inv; }; #endif // MAME_SOUND_VA_VCA_H |
