summaryrefslogtreecommitdiffstatshomepage
path: root/src/devices
diff options
context:
space:
mode:
author m1macrophage <168948267+m1macrophage@users.noreply.github.com>2026-04-12 07:23:17 -0700
committer GitHub <noreply@github.com>2026-04-12 16:23:17 +0200
commit0f5872aa2eeb46ae87c6a715946cce1704749d84 (patch)
treea72bd7ac2a5280069db889a87f2d39ab3f68c0a6 /src/devices
parent13493035a704249a425828ae0316eb4dcab0b551 (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.cpp236
-rw-r--r--src/devices/sound/va_vca.h121
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