linn/linndrum.cpp: Emulating hi-hat decay. (#13509)

* Hi-hat decay knob is now working.
* Open and closed hats have different decays.
* Better default tuning for the snare.
* Removed `strobe_` prefix from functions where it was redundant. Made it an argument in a function where it matters.
This commit is contained in:
m1macrophage 2025-03-22 08:09:55 -07:00 committed by GitHub
parent 891a89643f
commit d11197c548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -63,11 +63,10 @@ Reasons for MACHINE_IMPERFECT_SOUND:
* Missing a few sample checksums.
* Missing bass drum LPF and filter envelope.
* Missing snare / sidestick volume envelope.
* Missing hi-hat volume envelope (open and closed hats will sound the same.
Decay knob is inoperative).
* Missing tom / conga LPF and filter envelope.
* Inaccurate filter for "click".
* Linear, instead of audio-taper volume sliders and master volume knob.
* Linear, instead of tanh response for hi-hat VCA.
PCBoards:
* CPU board. 2 sections in schematics:
@ -114,6 +113,7 @@ Example:
#define LOG_TAPE_SYNC_ENABLE (1U << 6)
#define LOG_MIX (1U << 7)
#define LOG_PITCH (1U << 8)
#define LOG_HAT_VCA (1U << 9)
#define VERBOSE (LOG_GENERAL)
//#define LOG_OUTPUT_FUNC osd_printf_info
@ -182,15 +182,165 @@ enum mixer_channels
} // anonymous namespace
// This device combines the CEM3360 and its envelope generator (EG) that process
// the hi-hat voice.
// TODO: Look into implementing the CEM3360 and a generic EG as devices under
// src/devices/sound.
class linndrum_hat_vca_device : public device_t, public device_sound_interface
{
public:
linndrum_hat_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD;
void trigger();
void set_open(bool open_hat);
protected:
void device_add_mconfig(machine_config &config) override ATTR_COLD;
void device_start() override ATTR_COLD;
void device_reset() override ATTR_COLD;
void sound_stream_update(sound_stream &stream, std::vector<read_stream_view> const &inputs, std::vector<write_stream_view> &outputs) override;
private:
static float get_cem3360_gain(float cv);
TIMER_DEVICE_CALLBACK_MEMBER(trigger_timer_tick);
static constexpr const float C22 = CAP_U(1);
static constexpr const float R33 = RES_M(1);
static constexpr const float R34 = RES_K(10);
static constexpr const float DECAY_POT_R_MAX = RES_K(100);
sound_stream *m_stream = nullptr;
required_ioport m_decay_pot;
required_device<timer_device> m_trigger_timer; // U37B (LM556).
float m_rc_inv = 1.0F / (R33 * C22);
bool m_decaying = true;
bool m_decay_done = true;
attotime m_decay_start_time;
};
DEFINE_DEVICE_TYPE(LINNDRUM_HAT_VCA, linndrum_hat_vca_device, "linndrum_hat_vca", "LinnDrum CEM3360 VCA and EG");
linndrum_hat_vca_device::linndrum_hat_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: device_t(mconfig, LINNDRUM_HAT_VCA, tag, owner, clock)
, device_sound_interface(mconfig, *this)
, m_decay_pot(*this, ":pot_tuning_7")
, m_trigger_timer(*this, "hat_trigger_timer")
{
}
void linndrum_hat_vca_device::trigger()
{
m_stream->update();
m_decaying = false;
m_decay_done = false;
m_trigger_timer->adjust(PERIOD_OF_555_MONOSTABLE(RES_K(510), CAP_U(0.01))); // R8, C4.
LOGMASKED(LOG_HAT_VCA, "Hat VCA trigerred.\n");
}
void linndrum_hat_vca_device::set_open(bool open_hat)
{
// The envelope generator can run in two different modes.
// - Open hat: the capacitor is discharged through a 1M resistor.
// - Closed hat: U90 (CD4053 MUX) adds a parallel discharge path through the
// "hihat decay" knob.
m_stream->update();
float r = R33;
if (!open_hat)
{
const float r_decay = DECAY_POT_R_MAX * m_decay_pot->read() / 100.0F;
r = RES_2_PARALLEL(R33, R34 + r_decay);
}
m_rc_inv = 1.0F / (r * C22);
LOGMASKED(LOG_HAT_VCA, "Hat decay. Open: %d, r: %g\n", open_hat, r);
}
void linndrum_hat_vca_device::device_add_mconfig(machine_config &config)
{
TIMER(config, m_trigger_timer).configure_generic(FUNC(linndrum_hat_vca_device::trigger_timer_tick));
}
void linndrum_hat_vca_device::device_start()
{
m_stream = stream_alloc(1, 1, machine().sample_rate());
save_item(NAME(m_rc_inv));
save_item(NAME(m_decaying));
save_item(NAME(m_decay_done));
save_item(NAME(m_decay_start_time));
}
void linndrum_hat_vca_device::device_reset()
{
set_open(false);
}
void linndrum_hat_vca_device::sound_stream_update(sound_stream &stream, std::vector<read_stream_view> const &inputs, std::vector<write_stream_view> &outputs)
{
static constexpr const float MIN_GAIN = 0.0001F; // A gain lower than this will be treated as 0.
static constexpr const float MAX_EG_CV = 5;
static constexpr const float CV_SCALE = RES_VOLTAGE_DIVIDER(RES_K(8.2), RES_K(10)); // R67, R66.
assert(inputs.size() == 1 && outputs.size() == 1);
const read_stream_view &in = inputs[0];
write_stream_view &out = outputs[0];
if (m_decay_done)
{
out.fill(0);
return;
}
const int n = in.samples();
if (!m_decaying)
{
const float gain = get_cem3360_gain(MAX_EG_CV * CV_SCALE);
for (int i = 0; i < n; ++i)
out.put(i, gain * in.get(i));
return;
}
attotime t = in.start_time() - m_decay_start_time;
assert(t >= attotime::from_double(0));
float gain = 0;
for (int i = 0; i < n; ++i, t += in.sample_period())
{
// TODO: The CEM3360 is based on an OTA, which means it likely has a
// tanh, rather than a linear response. But this needs more research.
const float decay = expf(-t.as_double() * m_rc_inv);
gain = get_cem3360_gain(decay * MAX_EG_CV * CV_SCALE);
out.put(i, gain * in.get(i));
}
if (gain < MIN_GAIN)
m_decay_done = true;
}
float linndrum_hat_vca_device::get_cem3360_gain(float cv)
{
// Typical linear CV for max gain, as reported on the CEM3360 datasheet.
static constexpr const float MAX_GAIN_CV = 1.93F;
return std::clamp<float>(cv / MAX_GAIN_CV, 0, 1);
}
TIMER_DEVICE_CALLBACK_MEMBER(linndrum_hat_vca_device::trigger_timer_tick)
{
m_stream->update();
m_decaying = true;
m_decay_done = false;
m_decay_start_time = machine().time();
LOGMASKED(LOG_HAT_VCA, "Hat VCA started decay.\n");
}
class linndrum_audio_device : public device_t
{
public:
linndrum_audio_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock = 0) ATTR_COLD;
void strobe_mux_drum_w(int voice, u8 data);
void strobe_snare_w(u8 data); // Snare and sidestick.
void strobe_tom_w(u8 data); // Tom and conga.
void mux_drum_w(int voice, u8 data, bool is_strobe = true);
void snare_w(u8 data); // Snare and sidestick.
void tom_w(u8 data); // Tom and conga.
void strobe_click_w(u8 data);
void beep_w(int state);
@ -228,6 +378,7 @@ private:
required_device<timer_device> m_mux_timer; // 74LS627 (U77A).
required_device_array<dac76_device, NUM_MUX_VOICES> m_mux_dac; // AM6070 (U88).
required_device_array<filter_volume_device, NUM_MUX_VOICES> m_mux_volume; // CD4053 (U90), R60, R62.
required_device<linndrum_hat_vca_device> m_hat_vca;
std::array<bool, NUM_MUX_VOICES> m_mux_counting = { false, false, false, false, false, false, false, false };
std::array<u16, NUM_MUX_VOICES> m_mux_counters = { 0, 0, 0, 0, 0, 0, 0, 0 };
@ -339,6 +490,7 @@ linndrum_audio_device::linndrum_audio_device(const machine_config &mconfig, cons
, m_mux_timer(*this, "mux_drum_timer")
, m_mux_dac(*this, "mux_drums_virtual_dac_%u", 1)
, m_mux_volume(*this, "mux_drums_volume_control_%u", 1)
, m_hat_vca(*this, "hat_vca")
, m_snare_samples(*this, ":sample_snare")
, m_sidestick_samples(*this, ":sample_sidestick")
, m_snare_timer(*this, "snare_sidestick_timer")
@ -366,7 +518,7 @@ linndrum_audio_device::linndrum_audio_device(const machine_config &mconfig, cons
{
}
void linndrum_audio_device::strobe_mux_drum_w(int voice, u8 data)
void linndrum_audio_device::mux_drum_w(int voice, u8 data, bool is_strobe)
{
assert(voice >= 0 && voice < NUM_MUX_VOICES);
@ -384,11 +536,18 @@ void linndrum_audio_device::strobe_mux_drum_w(int voice, u8 data)
const bool attenuate = !BIT(data, 1) && voice != MV_CLAP && voice != MV_COWBELL;
m_mux_volume[voice]->set_gain(attenuate ? ATTENUATION : 1);
if (voice == MV_HAT)
{
m_hat_vca->set_open(BIT(data, 2));
if (is_strobe)
m_hat_vca->trigger();
}
LOGMASKED(LOG_STROBES, "Strobed mux drum %s: %02x (gain: %f)\n",
MUX_VOICE_NAMES[voice], data, m_mux_volume[voice]->gain());
}
void linndrum_audio_device::strobe_snare_w(u8 data)
void linndrum_audio_device::snare_w(u8 data)
{
m_snare_counting = BIT(data, 0);
if (!m_snare_counting)
@ -435,7 +594,7 @@ void linndrum_audio_device::strobe_snare_w(u8 data)
data, iref, gain);
}
void linndrum_audio_device::strobe_tom_w(u8 data)
void linndrum_audio_device::tom_w(u8 data)
{
m_tom_counting = BIT(data, 0);
if (!m_tom_counting)
@ -534,15 +693,18 @@ 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).
FILTER_VOLUME(config, m_mux_volume[voice]); // CD4053 (U90), R60, R62 (see strobe_mux_drum_w()).
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));
}
LINNDRUM_HAT_VCA(config, m_hat_vca);
m_mux_volume[MV_HAT]->add_route(0, m_hat_vca, 1.0);
// *** Snare / sidestick section.
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 strobe_snare_w().
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);
@ -581,7 +743,7 @@ void linndrum_audio_device::device_add_mconfig(machine_config &config)
m_mux_volume[MV_BASS],
m_snare_out,
m_sidestick_out,
m_mux_volume[MV_HAT],
m_hat_vca,
m_tom_out[TV_HI_TOMS],
m_tom_out[TV_MID_TOMS],
m_tom_out[TV_LOW_TOMS],
@ -745,7 +907,7 @@ TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::mux_timer_tick)
if (m_mux_counters[voice] >= m_mux_samples[voice]->bytes())
{
// All outputs in the voice's data latch (74LS74) are cleared.
strobe_mux_drum_w(voice, 0);
mux_drum_w(voice, 0, false);
}
}
@ -763,7 +925,7 @@ TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::snare_timer_tick)
if (BIT(m_snare_counter, 12)) // Counter reached 0x1000 (4096).
{
// All outputs of U41 and U42 (74LS74 flip-flops) are cleared.
strobe_snare_w(0);
snare_w(0);
return;
}
@ -789,7 +951,7 @@ TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::tom_timer_tick)
if (BIT(m_tom_counter, 13)) // Counter reached 0x2000 (8192).
{
// All outputs of U42B and U73B (74LS74 flip-flops) are cleared.
strobe_tom_w(0);
tom_w(0);
return;
}
@ -1214,16 +1376,16 @@ void linndrum_state::memory_map(address_map &map)
map(0x1f84, 0x1f84).mirror(0x0030).w("latch_u16", FUNC(output_latch_device::write)); // LEDs & outputs.
// Voice strobes.
map(0x1f85, 0x1f85).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_BASS, get_voice_data(data)); }));
map(0x1f86, 0x1f86).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_snare_w(get_voice_data(data)); }));
map(0x1f87, 0x1f87).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_HAT, get_voice_data(data)); }));
map(0x1f88, 0x1f88).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_tom_w(get_voice_data(data)); }));
map(0x1f89, 0x1f89).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_RIDE, get_voice_data(data)); }));
map(0x1f8a, 0x1f8a).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_CRASH, get_voice_data(data)); }));
map(0x1f8b, 0x1f8b).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_CABASA, get_voice_data(data)); }));
map(0x1f8c, 0x1f8c).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_TAMBOURINE, get_voice_data(data)); }));
map(0x1f8d, 0x1f8d).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_COWBELL, get_voice_data(data)); }));
map(0x1f8e, 0x1f8e).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->strobe_mux_drum_w(MV_CLAP, get_voice_data(data)); }));
map(0x1f85, 0x1f85).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_BASS, get_voice_data(data)); }));
map(0x1f86, 0x1f86).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->snare_w(get_voice_data(data)); }));
map(0x1f87, 0x1f87).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_HAT, get_voice_data(data)); }));
map(0x1f88, 0x1f88).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->tom_w(get_voice_data(data)); }));
map(0x1f89, 0x1f89).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_RIDE, get_voice_data(data)); }));
map(0x1f8a, 0x1f8a).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_CRASH, get_voice_data(data)); }));
map(0x1f8b, 0x1f8b).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_CABASA, get_voice_data(data)); }));
map(0x1f8c, 0x1f8c).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_TAMBOURINE, get_voice_data(data)); }));
map(0x1f8d, 0x1f8d).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_COWBELL, get_voice_data(data)); }));
map(0x1f8e, 0x1f8e).mirror(0x0030).lw8(NAME([this] (u8 data) { m_audio->mux_drum_w(MV_CLAP, get_voice_data(data)); }));
map(0x1f8f, 0x1f8f).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_click_w)); // No voice data sent.
map(0x1fc0, 0x1fff).r(FUNC(linndrum_state::keyboard_r)); // /READ KEYBD.
@ -1396,7 +1558,7 @@ INPUT_PORTS_START(linndrum)
PORT_ADJUSTER(25, "TRIMMER: MUX DRUM TUNING") PORT_CHANGED_MEMBER(AUDIO_TAG, FUNC(linndrum_audio_device::mux_drum_tuning_changed), 0)
PORT_START("pot_tuning_1")
PORT_ADJUSTER(50, "SNARE TUNING") PORT_CHANGED_MEMBER(AUDIO_TAG, FUNC(linndrum_audio_device::snare_tuning_changed), 0)
PORT_ADJUSTER(25, "SNARE TUNING") PORT_CHANGED_MEMBER(AUDIO_TAG, FUNC(linndrum_audio_device::snare_tuning_changed), 0)
PORT_START("pot_tuning_2")
PORT_ADJUSTER(60, "HI TOM TUNING") PORT_CHANGED_MEMBER(AUDIO_TAG, FUNC(linndrum_audio_device::tom_tuning_changed), 0)
PORT_START("pot_tuning_3")