sound/dac76.cpp: Emulating multiplying capability. (#13577)

* Added support for reference current (multiplying capability).
* Added support for voltage output.
* Added support for streaming reference current.
* Used those capabilities in oberheim/dmx.cpp and linn/linndrum.cpp.
This commit is contained in:
m1macrophage 2025-04-10 02:04:35 -07:00 committed by GitHub
parent 6bc01c176c
commit e0ea6961f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 112 additions and 48 deletions

View File

@ -38,12 +38,38 @@ dac76_device::dac76_device(const machine_config &mconfig, const char *tag, devic
device_t(mconfig, DAC76, tag, owner, clock),
device_sound_interface(mconfig, *this),
m_stream(nullptr),
m_streaming_iref(false),
m_voltage_output(false),
m_r_pos(1.0F),
m_r_neg(1.0F),
m_chord(0),
m_step(0),
m_sb(false)
m_sb(false),
m_fixed_iref(1.0F)
{
}
void dac76_device::configure_streaming_iref(bool streaming_iref)
{
m_streaming_iref = streaming_iref;
}
void dac76_device::configure_voltage_output(float i2v_r_pos, float i2v_r_neg)
{
m_voltage_output = true;
m_r_pos = i2v_r_pos;
m_r_neg = i2v_r_neg;
}
void dac76_device::set_fixed_iref(float iref)
{
if (m_fixed_iref == iref)
return;
if (m_stream != nullptr)
m_stream->update();
m_fixed_iref = iref;
}
//-------------------------------------------------
// device_start - device-specific startup
//-------------------------------------------------
@ -51,12 +77,14 @@ dac76_device::dac76_device(const machine_config &mconfig, const char *tag, devic
void dac76_device::device_start()
{
// create sound stream
m_stream = stream_alloc(0, 1, machine().sample_rate() * 8);
const int input_count = m_streaming_iref ? 1 : 0;
m_stream = stream_alloc(input_count, 1, machine().sample_rate() * 8);
// register for save states
save_item(NAME(m_chord));
save_item(NAME(m_step));
save_item(NAME(m_sb));
save_item(NAME(m_fixed_iref));
}
//-------------------------------------------------
@ -85,5 +113,24 @@ void dac76_device::sound_stream_update(sound_stream &stream, std::vector<read_st
vout *= (m_sb ? +1 : -1);
// range is 0-8031, normalize to 0-1 range
outputs[0].fill(stream_buffer::sample_t(vout) * (1.0 / 8031.0));
stream_buffer::sample_t y = stream_buffer::sample_t(vout) * (1.0 / 8031.0);
if (m_voltage_output)
{
static constexpr const float FULL_SCALE_MULT = 3.8F; // From datasheet.
y *= ((y >= 0) ? m_r_pos : m_r_neg) * FULL_SCALE_MULT;
}
write_stream_view &out = outputs[0];
if (m_streaming_iref)
{
const read_stream_view &iref = inputs[0];
const int n = out.samples();
for (int i = 0; i < n; ++i)
out.put(i, iref.get(i) * y);
}
else
{
out.fill(m_fixed_iref * y);
}
}

View File

@ -4,7 +4,7 @@
PMI DAC-76 COMDAC
Companding D/A Converter
Companding Multiplying D/A Converter
Equivalent to the AM6070, which is an "improved pin-for-pin replacement for
DAC-76" (according to the AM6070 datasheet).
@ -20,6 +20,19 @@
B6 8 | | 11 VR+
B7 9 |_______| 10 VLC
Given:
- Iref = current flowing into VR+
- X = DAC value, normalized to [-1, 1]
- E/D (pin 1) is low
The output will be:
- IOD+ = (X > 0) ? (3.8 * Iref * abs(X)) : 0
- IOD- = (X < 0) ? (3.8 * Iref * abs(X)) : 0
The outputs are typically converted to voltages and summed into a single
signal by a current-to-voltage converter (I2V) consisting of an op-amp and
two resistors.
***************************************************************************/
#ifndef MAME_SOUND_DAC76_H
@ -39,6 +52,25 @@ public:
// construction/destruction
dac76_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock);
// When streaming is enabled, the Iref will be obtained from the input sound
// stream. Otherwise, the value set with `set_fixed_iref` will be used.
void configure_streaming_iref(bool streaming_iref);
// By default, the control current (Iref) is treated as normalized ([0, 1],
// defaults to 1), and the sound output is normalized to [-1, 1].
//
// When in "voltage output" mode, Iref (fixed or streaming) should be the
// current flowing into pin 11, and the output will be a voltage stream.
// `r_pos` is the feedback resistor of the I2V, which is also connected to
// IOD+ (pin 16).
// `r_neg` is the resistor to ground connected to IOD- (pin 17).
// Note that r_pos connects to the "-" input of the I2V op-amp, and r_neg to
// the "+" input.
void configure_voltage_output(float i2v_r_pos, float i2v_r_neg);
// Reference current. Ignored when streaming Iref mode is enabled.
void set_fixed_iref(float iref);
// chord
void b1_w(int state) { m_chord &= ~(1 << 2); m_chord |= (state << 2); }
void b2_w(int state) { m_chord &= ~(1 << 1); m_chord |= (state << 1); }
@ -67,9 +99,17 @@ private:
sound_stream *m_stream;
// configuration
bool m_streaming_iref;
bool m_voltage_output;
float m_r_pos;
float m_r_neg;
// state
uint8_t m_chord; // 3-bit
uint8_t m_step; // 4-bit
bool m_sb;
float m_fixed_iref;
};
// device type definition

View File

@ -209,7 +209,6 @@ protected:
private:
static void write_dac(dac76_device& dac, u8 sample);
static float get_dac_scaler(float iref);
static s32 get_ls267_freq(const std::array<s32, 2>& freq_range_hz, float cv);
static float get_snare_tom_pitch_cv(float v);
@ -243,7 +242,6 @@ private:
required_memory_region m_sidestick_samples; // 2732 ROMs (U78).
required_device<timer_device> m_snare_timer; // 74L627 (U80A).
required_device<dac76_device> m_snare_dac; // AM6070 (U92).
required_device<filter_volume_device> m_snare_volume; // R69, R72, R71, R70.
required_device<filter_volume_device> m_snare_out; // U90A (CD4053) pin 12 (ax).
required_device<filter_volume_device> m_sidestick_out; // U90A (CD4053) pin 13 (ay).
required_device<timer_device> m_click_timer; // 556 (U65A).
@ -305,6 +303,9 @@ private:
static constexpr const float VCC = 5; // Volts.
static constexpr const float MUX_DAC_IREF = VPLUS / (RES_K(15) + RES_K(15)); // R55 + R57.
static constexpr const float TOM_DAC_IREF = MUX_DAC_IREF; // Configured in the same way.
// All DAC current-to-voltage converter resistors, for both positive and
// negative values, are 2.49 KOhm.
static constexpr const float R_DAC_I2V = RES_K(2.49); // R58, R59, R127, R126, tom DAC I2V (missing designation).
// Constants for hi hat envelope generator circuit.
static constexpr const float HAT_C22 = CAP_U(1);
@ -361,7 +362,6 @@ linndrum_audio_device::linndrum_audio_device(const machine_config &mconfig, cons
, m_sidestick_samples(*this, ":sample_sidestick")
, m_snare_timer(*this, "snare_sidestick_timer")
, m_snare_dac(*this, "snare_sidestick_dac")
, m_snare_volume(*this, "snare_sidestick_volume")
, m_snare_out(*this, "snare_output")
, m_sidestick_out(*this, "sidestick_output")
, m_click_timer(*this, "click_timer")
@ -465,12 +465,9 @@ void linndrum_audio_device::snare_w(u8 data)
const float v2 = BIT(data, 2) ? VCC : 0;
const float v3 = BIT(data, 3) ? VCC : 0;
const float iref = (a * VPLUS + b * v2 + c * v3) / (R0 * (a + b + c + d));
m_snare_dac->set_fixed_iref(iref);
const float gain = get_dac_scaler(iref);
m_snare_volume->set_gain(gain);
LOGMASKED(LOG_STROBES, "Strobed snare / sidestick: %02x (iref: %f, gain: %f)\n",
data, iref, gain);
LOGMASKED(LOG_STROBES, "Strobed snare / sidestick: %02x (iref: %f)\n", data, iref);
}
void linndrum_audio_device::tom_w(u8 data)
@ -572,8 +569,10 @@ void linndrum_audio_device::device_add_mconfig(machine_config &config)
for (int voice = 0; voice < NUM_MUX_VOICES; ++voice)
{
DAC76(config, m_mux_dac[voice], 0); // AM6070 (U88).
m_mux_dac[voice]->configure_voltage_output(R_DAC_I2V, R_DAC_I2V); // R58, R59.
m_mux_dac[voice]->set_fixed_iref(MUX_DAC_IREF);
FILTER_VOLUME(config, m_mux_volume[voice]); // CD4053 (U90), R60, R62 (see mux_drum_w()).
m_mux_dac[voice]->add_route(0, m_mux_volume[voice], get_dac_scaler(MUX_DAC_IREF));
m_mux_dac[voice]->add_route(0, m_mux_volume[voice], 1.0);
}
TIMER(config, m_hat_trigger_timer).configure_generic(FUNC(linndrum_audio_device::hat_trigger_timer_tick)); // LM556 (U37B).
@ -586,17 +585,15 @@ void linndrum_audio_device::device_add_mconfig(machine_config &config)
TIMER(config, m_snare_timer).configure_generic(FUNC(linndrum_audio_device::snare_timer_tick)); // 74LS627 (U80A).
DAC76(config, m_snare_dac, 0); // AM6070 (U92)
FILTER_VOLUME(config, m_snare_volume); // See snare_w().
// DAC output scaling is incorporated in m_snare_volume's gain.
m_snare_dac->add_route(0, m_snare_volume, 1.0);
m_snare_dac->configure_voltage_output(R_DAC_I2V, R_DAC_I2V); // R127, R126.
// The DAC's current outputs are processed by a current-to-voltage converter
// that embeds an RC filter. This consists of an op-amp (U103), R127 and C65
// (for positive voltages), and R126 and C31 (for negative voltages). The
// two resistors and capacitors have the same value.
auto &snare_dac_filter = FILTER_RC(config, "snare_sidestick_dac_filter");
snare_dac_filter.set_lowpass(RES_K(2.49), CAP_P(2700)); // R127-C65, R126-C31. Cutoff: ~23.7KHz.
m_snare_volume->add_route(0, snare_dac_filter, 1.0);
snare_dac_filter.set_lowpass(R_DAC_I2V, CAP_P(2700)); // R127-C65, R126-C31. Cutoff: ~23.7KHz.
m_snare_dac->add_route(0, snare_dac_filter, 1.0);
FILTER_VOLUME(config, m_snare_out);
FILTER_VOLUME(config, m_sidestick_out);
@ -612,10 +609,14 @@ void linndrum_audio_device::device_add_mconfig(machine_config &config)
TIMER(config, m_tom_timer).configure_generic(FUNC(linndrum_audio_device::tom_timer_tick)); // 74LS627 (U77B).
DAC76(config, m_tom_dac, 0); // AM6070 (U82).
// Schematic is missing the second resistor, but that's almost certainly an error.
// It is also missing component designations.
m_tom_dac->configure_voltage_output(R_DAC_I2V, R_DAC_I2V);
m_tom_dac->set_fixed_iref(TOM_DAC_IREF);
for (int i = 0; i < NUM_TOM_VOICES; ++i)
{
FILTER_VOLUME(config, m_tom_out[i]); // One of U87'S (CD4051) outputs.
m_tom_dac->add_route(0, m_tom_out[i], get_dac_scaler(TOM_DAC_IREF));
m_tom_dac->add_route(0, m_tom_out[i], 1.0);
}
// *** Mixer.
@ -711,20 +712,6 @@ void linndrum_audio_device::write_dac(dac76_device &dac, u8 sample)
dac.b7_w(BIT(sample, 0));
}
float linndrum_audio_device::get_dac_scaler(float iref)
{
// Given the reference current into the DAC, computes the scaler that needs
// to be applied to the dac76_device output, to convert it to a voltage.
// The maximum output current on each of the "+" and "-" outputs of the
// AM6070 is `3.8 * Iref`, according to the datasheet.
// That current gets converted to a voltage by an op-amp configured as
// a current-to-voltage converter (I2V). All I2Vs on the LinnDrum use
// 2.49K resistors for both the "+" and "-" current outputs of the DAC.
return 3.8F * iref * float(RES_K(2.49));
}
s32 linndrum_audio_device::get_ls267_freq(const std::array<s32, 2>& freq_range, float cv)
{
// The relationship between CV and frequency is approximately linear. The

View File

@ -83,7 +83,6 @@ Usage notes:
#include "sound/mixer.h"
#include "sound/spkrdev.h"
#include "sound/va_eg.h"
#include "sound/va_vca.h"
#include "video/dl1416.h"
#include "speaker.h"
@ -207,7 +206,6 @@ private:
required_device<timer_device> m_timer; // 555, U5.
required_device<dac76_device> m_dac; // AM6070, U8. Compatible with DAC76.
required_device<va_vca_device> m_dac_mult; // AM6070 is a multiplying DAC.
optional_device<va_rc_eg_device> m_eg; // Volume envelope generator. Input to U8 Iref.
required_device_array<filter_biquad_device, 3> m_filters;
@ -234,7 +232,6 @@ dmx_voice_card_device::dmx_voice_card_device(const machine_config &mconfig, cons
, device_sound_interface(mconfig, *this)
, m_timer(*this, "555_u5")
, m_dac(*this, "dac_u8")
, m_dac_mult(*this, "dac_mult_u8")
, m_eg(*this, "envelope_generator")
, m_filters(*this, "aa_sk_filter_%d", 0)
, m_config(config)
@ -249,7 +246,6 @@ dmx_voice_card_device::dmx_voice_card_device(const machine_config &mconfig, cons
, device_sound_interface(mconfig, *this)
, m_timer(*this, "555_u5")
, m_dac(*this, "dac_u8")
, m_dac_mult(*this, "dac_mult_u8")
, m_eg(*this, "envelope_generator")
, m_filters(*this, "aa_sk_filter_%d", 0)
// Need non-zero entries for the filter for validation to pass.
@ -281,7 +277,7 @@ void dmx_voice_card_device::trigger(bool tr0, bool tr1)
if (has_decay()) // Gain is controled by an envelope generator.
m_eg->set_instant_v(iref);
else // Constant gain.
m_dac_mult->set_fixed_cv(iref);
m_dac->set_fixed_iref(iref);
LOGMASKED(LOG_SOUND, "Trigger: (%d, %d) %d, %f uA\n", tr0, tr1, m_trigger_mode, iref * 1e6F);
}
@ -295,23 +291,17 @@ void dmx_voice_card_device::set_pitch_adj(s32 t1_percent)
void dmx_voice_card_device::device_add_mconfig(machine_config &config)
{
// This multiplier relates the DAC reference current to the output voltage.
// Iout = dac value (normalized to -1 - 1) * 3.8 * Iref.
// Vout = Iout * R_current_to_voltage_converter.
static constexpr const double DAC_MULTIPLIER = 3.8 * RES_K(2.4); // R16, R11 on voice card.
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_device::clock_callback));
DAC76(config, m_dac, 0U);
VA_VCA(config, m_dac_mult);
m_dac->add_route(0, m_dac_mult, DAC_MULTIPLIER);
DAC76(config, m_dac, 0U).configure_voltage_output(RES_K(2.4), RES_K(2.4)); // R16, R11 on voice card.
if (has_decay())
{
VA_RC_EG(config, m_eg).set_c(m_config.c3);
m_dac_mult->configure_streaming_cv(true);
m_eg->add_route(0, m_dac_mult, 1.0);
m_dac->configure_streaming_iref(true);
m_eg->add_route(0, m_dac, 1.0);
}
FILTER_BIQUAD(config, m_filters[0]).opamp_sk_lowpass_setup(
@ -324,7 +314,7 @@ void dmx_voice_card_device::device_add_mconfig(machine_config &config)
m_config.filter.r19, m_config.filter.r20, SK_R3, SK_R4,
m_config.filter.c8, m_config.filter.c9);
m_dac_mult->add_route(0, m_filters[0], 1.0);
m_dac->add_route(0, m_filters[0], 1.0);
m_filters[0]->add_route(0, m_filters[1], 1.0);
m_filters[1]->add_route(0, m_filters[2], 1.0);
m_filters[2]->add_route(0, *this, 1.0);