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:
parent
891a89643f
commit
d11197c548
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user