mirror of
https://github.com/holub/mame
synced 2025-04-16 21:44:32 +03:00
Implemented VA EG (Envelope Generator) and VCA (Voltage Controlled Amplifier) sound devices. (#13545)
* sound/va_eg.cpp, sound/va_vca.cpp: Envelope generator and voltage-controlled amplifier. Implemented RC-based envelope generator and voltage-controlled amplifier devices. Replaced custom implementations in paia/fatman, linn/linndrum and oberheim/dmx. * Fixing comments. * More comment fixes. * Adding comments based on feedback. * Fixed typo.
This commit is contained in:
parent
a71c0333b7
commit
3299e78bae
@ -1166,6 +1166,29 @@ if (SOUNDS["UPD7752"]~=null) then
|
||||
}
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- Virtual analog envelope generators (EGs)
|
||||
--@src/devices/sound/va_eg.h,SOUNDS["VA_EG"] = true
|
||||
--------------------------------------------------
|
||||
|
||||
if (SOUNDS["VA_EG"]~=null) then
|
||||
files {
|
||||
MAME_DIR .. "src/devices/sound/va_eg.cpp",
|
||||
MAME_DIR .. "src/devices/sound/va_eg.h",
|
||||
}
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- Virtual analog voltage-controlled amplifiers (VCAs)
|
||||
--@src/devices/sound/va_vca.h,SOUNDS["VA_VCA"] = true
|
||||
--------------------------------------------------
|
||||
|
||||
if (SOUNDS["VA_VCA"]~=null) then
|
||||
files {
|
||||
MAME_DIR .. "src/devices/sound/va_vca.cpp",
|
||||
MAME_DIR .. "src/devices/sound/va_vca.h",
|
||||
}
|
||||
end
|
||||
|
||||
---------------------------------------------------
|
||||
-- VLM5030 speech synthesizer
|
||||
|
129
src/devices/sound/va_eg.cpp
Normal file
129
src/devices/sound/va_eg.cpp
Normal file
@ -0,0 +1,129 @@
|
||||
// license:BSD-3-Clause
|
||||
// copyright-holders:m1macrophage
|
||||
|
||||
#include "emu.h"
|
||||
#include "va_eg.h"
|
||||
#include "machine/rescap.h"
|
||||
|
||||
DEFINE_DEVICE_TYPE(VA_RC_EG, va_rc_eg_device, "va_rc_eg", "RC-based Envelope Generator")
|
||||
|
||||
va_rc_eg_device::va_rc_eg_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
|
||||
: device_t(mconfig, VA_RC_EG, tag, owner, clock)
|
||||
, device_sound_interface(mconfig, *this)
|
||||
, m_stream(nullptr)
|
||||
// Initialize to a valid state.
|
||||
, m_r(RES_M(1))
|
||||
, m_c(CAP_U(1))
|
||||
, m_rc_inv(1.0F / (m_r * m_c))
|
||||
, m_v_start(0)
|
||||
, m_v_end(0)
|
||||
{
|
||||
}
|
||||
|
||||
va_rc_eg_device &va_rc_eg_device::set_r(float r)
|
||||
{
|
||||
assert(r > 0);
|
||||
if (r == m_r)
|
||||
return *this;
|
||||
if (m_stream != nullptr)
|
||||
m_stream->update();
|
||||
|
||||
set_target_v(m_v_end); // Snapshots voltage, using the old `r` value.
|
||||
m_r = r;
|
||||
m_rc_inv = 1.0F / (m_r * m_c);
|
||||
return *this;
|
||||
}
|
||||
|
||||
va_rc_eg_device &va_rc_eg_device::set_c(float c)
|
||||
{
|
||||
assert(c > 0);
|
||||
if (c == m_c)
|
||||
return *this;
|
||||
if (m_stream != nullptr)
|
||||
m_stream->update();
|
||||
|
||||
set_target_v(m_v_end); // Snapshots voltage, using the old `c` value.
|
||||
m_c = c;
|
||||
m_rc_inv = 1.0F / (m_r * m_c);
|
||||
return *this;
|
||||
}
|
||||
|
||||
va_rc_eg_device &va_rc_eg_device::set_target_v(float v)
|
||||
{
|
||||
if (v == m_v_end)
|
||||
return *this;
|
||||
if (m_stream != nullptr)
|
||||
m_stream->update();
|
||||
|
||||
if (has_running_machine())
|
||||
{
|
||||
const attotime now = machine().time();
|
||||
m_v_start = get_v(now);
|
||||
m_v_end = v;
|
||||
m_t_start = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_v_start = 0;
|
||||
m_v_end = v;
|
||||
m_t_start = attotime::zero;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
va_rc_eg_device &va_rc_eg_device::set_instant_v(float v)
|
||||
{
|
||||
if (m_stream != nullptr)
|
||||
m_stream->update();
|
||||
|
||||
m_v_start = v;
|
||||
m_v_end = v;
|
||||
m_t_start = has_running_machine() ? machine().time() : attotime::zero;
|
||||
return *this;
|
||||
}
|
||||
|
||||
float va_rc_eg_device::get_v(const attotime &t) const
|
||||
{
|
||||
assert(t >= m_t_start);
|
||||
const float delta_t = float((t - m_t_start).as_double());
|
||||
return m_v_start + (m_v_end - m_v_start) * (1.0F - expf(-delta_t * m_rc_inv));
|
||||
}
|
||||
|
||||
float va_rc_eg_device::get_v() const
|
||||
{
|
||||
return get_v(machine().time());
|
||||
}
|
||||
|
||||
void va_rc_eg_device::device_start()
|
||||
{
|
||||
m_stream = stream_alloc(0, 1, SAMPLE_RATE_OUTPUT_ADAPTIVE);
|
||||
save_item(NAME(m_r));
|
||||
save_item(NAME(m_c));
|
||||
save_item(NAME(m_rc_inv));
|
||||
save_item(NAME(m_v_start));
|
||||
save_item(NAME(m_v_end));
|
||||
save_item(NAME(m_t_start));
|
||||
}
|
||||
|
||||
void va_rc_eg_device::sound_stream_update(sound_stream &stream, const std::vector<read_stream_view> &inputs, std::vector<write_stream_view> &outputs)
|
||||
{
|
||||
// The envelope stage will be considered done once the voltage reaches
|
||||
// within MIN_DELTA of the target.
|
||||
static constexpr const float MIN_DELTA = 0.0001F;
|
||||
|
||||
assert(inputs.size() == 0 && outputs.size() == 1);
|
||||
write_stream_view &out = outputs[0];
|
||||
attotime t = out.start_time();
|
||||
|
||||
if (fabsf(get_v(t) - m_v_end) < MIN_DELTA)
|
||||
{
|
||||
// Avoid expensive get_v() calls if the envelope stage has completed.
|
||||
out.fill(m_v_end);
|
||||
return;
|
||||
}
|
||||
|
||||
const int n = out.samples();
|
||||
const attotime dt = out.sample_period();
|
||||
for (int i = 0; i < n; ++i, t += dt)
|
||||
out.put(i, get_v(t));
|
||||
}
|
57
src/devices/sound/va_eg.h
Normal file
57
src/devices/sound/va_eg.h
Normal file
@ -0,0 +1,57 @@
|
||||
// license:BSD-3-Clause
|
||||
// copyright-holders:m1macrophage
|
||||
|
||||
#ifndef MAME_SOUND_VA_EG_H
|
||||
#define MAME_SOUND_VA_EG_H
|
||||
|
||||
#pragma once
|
||||
|
||||
// Building block for emulating envelope generators (EGs) based on a single RC
|
||||
// circuit. The controlling source sets a target voltage and the device
|
||||
// interpolates up or down to it through a RC circuit. The voltage is published
|
||||
// as a sound stream and also available from `get_v()`.
|
||||
//
|
||||
// For example, emulating a CPU-controlled ADSR EG will look something like:
|
||||
// - Machine configuration: rc_eg.set_c(C);
|
||||
// - Start attack: rc_eg.set_r(attack_r).set_target_v(max_v);
|
||||
// - Start decay: rc_eg.set_r(decay_r).set_target_v(sustain_v);
|
||||
// - Start release: rc_eg.set_r(release_r).set_target_v(0);
|
||||
//
|
||||
class va_rc_eg_device : public device_t, public device_sound_interface
|
||||
{
|
||||
public:
|
||||
va_rc_eg_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD;
|
||||
|
||||
// These setters can be used during both: configuration and normal operation.
|
||||
// `r` and `c` must be > 0. To instantly set the capacitor's voltage to
|
||||
// a specific value, use set_instant_v().
|
||||
va_rc_eg_device &set_r(float r);
|
||||
va_rc_eg_device &set_c(float v);
|
||||
|
||||
// Sets target voltage to (dis)charge towards.
|
||||
va_rc_eg_device &set_target_v(float v);
|
||||
|
||||
// Sets the voltage to the given value, instantly.
|
||||
va_rc_eg_device &set_instant_v(float v);
|
||||
|
||||
float get_v(const attotime &t) const;
|
||||
float get_v() const; // Get voltage at the machine's current time.
|
||||
|
||||
protected:
|
||||
void device_start() override ATTR_COLD;
|
||||
void sound_stream_update(sound_stream &stream, const std::vector<read_stream_view> &inputs, std::vector<write_stream_view> &outputs) override;
|
||||
|
||||
private:
|
||||
sound_stream *m_stream;
|
||||
|
||||
float m_r;
|
||||
float m_c;
|
||||
float m_rc_inv;
|
||||
float m_v_start;
|
||||
float m_v_end;
|
||||
attotime m_t_start;
|
||||
};
|
||||
|
||||
DECLARE_DEVICE_TYPE(VA_RC_EG, va_rc_eg_device)
|
||||
|
||||
#endif // MAME_SOUND_VA_EG_H
|
76
src/devices/sound/va_vca.cpp
Normal file
76
src/devices/sound/va_vca.cpp
Normal file
@ -0,0 +1,76 @@
|
||||
// license:BSD-3-Clause
|
||||
// copyright-holders:m1macrophage
|
||||
|
||||
#include "emu.h"
|
||||
#include "va_vca.h"
|
||||
|
||||
#include <cfloat>
|
||||
|
||||
// Default max peak-to-peak voltage for the CV input.
|
||||
static constexpr const float DEFAULT_MAX_VPP = 100;
|
||||
|
||||
DEFINE_DEVICE_TYPE(VA_VCA, va_vca_device, "va_vca", "Voltage Controlled Amplifier")
|
||||
|
||||
va_vca_device::va_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
|
||||
: device_t(mconfig, VA_VCA, tag, owner, clock)
|
||||
, device_sound_interface(mconfig, *this)
|
||||
, m_stream(nullptr)
|
||||
, m_has_cv_stream(false)
|
||||
, m_min_cv(-DEFAULT_MAX_VPP)
|
||||
, m_max_cv(DEFAULT_MAX_VPP)
|
||||
, m_cv_scale(1.0F)
|
||||
, m_fixed_gain(1.0F)
|
||||
{
|
||||
}
|
||||
|
||||
va_vca_device &va_vca_device::configure_streaming_cv(bool use_streaming_cv)
|
||||
{
|
||||
m_has_cv_stream = use_streaming_cv;
|
||||
return *this;
|
||||
}
|
||||
|
||||
va_vca_device &va_vca_device::configure_cem3360_linear_cv()
|
||||
{
|
||||
// TODO: For now, the CEM3360 is treated as a linear device. But since it
|
||||
// is OTA-based, it likely has a tanh response. This requires more research.
|
||||
|
||||
// Typical linear CV for max gain, as reported on the CEM3360 datasheet.
|
||||
static constexpr const float CEM3360_MAX_GAIN_CV = 1.93F;
|
||||
m_min_cv = 0;
|
||||
m_max_cv = CEM3360_MAX_GAIN_CV;
|
||||
m_cv_scale = 1.0F / CEM3360_MAX_GAIN_CV;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void va_vca_device::set_fixed_cv(float cv)
|
||||
{
|
||||
m_stream->update();
|
||||
m_fixed_gain = cv_to_gain(cv);
|
||||
}
|
||||
|
||||
void va_vca_device::device_start()
|
||||
{
|
||||
const int input_count = m_has_cv_stream ? 2 : 1;
|
||||
m_stream = stream_alloc(input_count, 1, SAMPLE_RATE_OUTPUT_ADAPTIVE);
|
||||
save_item(NAME(m_fixed_gain));
|
||||
}
|
||||
|
||||
void va_vca_device::sound_stream_update(sound_stream &stream, const std::vector<read_stream_view> &inputs, std::vector<write_stream_view> &outputs)
|
||||
{
|
||||
if (m_has_cv_stream)
|
||||
{
|
||||
for (int i = 0; i < outputs[0].samples(); i++)
|
||||
outputs[0].put(i, inputs[0].get(i) * cv_to_gain(inputs[1].get(i)));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < outputs[0].samples(); i++)
|
||||
outputs[0].put(i, inputs[0].get(i) * m_fixed_gain);
|
||||
}
|
||||
}
|
||||
|
||||
float va_vca_device::cv_to_gain(float cv) const
|
||||
{
|
||||
return std::clamp(cv, m_min_cv, m_max_cv) * m_cv_scale;
|
||||
}
|
||||
|
58
src/devices/sound/va_vca.h
Normal file
58
src/devices/sound/va_vca.h
Normal file
@ -0,0 +1,58 @@
|
||||
// license:BSD-3-Clause
|
||||
// copyright-holders:m1macrophage
|
||||
|
||||
#ifndef MAME_SOUND_VA_VCA_H
|
||||
#define MAME_SOUND_VA_VCA_H
|
||||
|
||||
#pragma once
|
||||
|
||||
// Emulates a voltage-controled amplifier (VCA). The control value (CV) can
|
||||
// be set directly (set_fixed_cv()), or it can be provided in a sound stream
|
||||
// (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.
|
||||
//
|
||||
// The behavior of specific VCAs can be emulated by using the respective
|
||||
// configure_* functions.
|
||||
// Note that "CV" ("control value") could either refer to a control voltage, or
|
||||
// a control current, depending on the device.
|
||||
class va_vca_device : public device_t, public device_sound_interface
|
||||
{
|
||||
public:
|
||||
va_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD;
|
||||
|
||||
// When streaming is enabled, the CV will be obtained from input 1 of the
|
||||
// sound stream. Otherwise, the value set with `set_fixed_cv` will be used.
|
||||
va_vca_device &configure_streaming_cv(bool use_streaming_cv);
|
||||
|
||||
// By default, the CV will be treated as a typical gain (output = cv * input)
|
||||
// The configure_*() functions below might change this.
|
||||
|
||||
// CEM3360 (or AS3360) VCA in linear CV configuration: CV connected to pin
|
||||
// Vc, and pins Vo and Ve connected to each other. The CV input (fixed or
|
||||
// streaming) should be the voltage at the Vc pin.
|
||||
va_vca_device &configure_cem3360_linear_cv();
|
||||
|
||||
// Ignored when streaming CVs are enabled.
|
||||
void set_fixed_cv(float cv);
|
||||
|
||||
protected:
|
||||
void device_start() override ATTR_COLD;
|
||||
void sound_stream_update(sound_stream &stream, const std::vector<read_stream_view> &inputs, std::vector<write_stream_view> &outputs) override;
|
||||
|
||||
private:
|
||||
float cv_to_gain(float cv) const;
|
||||
|
||||
// Configuration. No need to include in save state.
|
||||
sound_stream *m_stream;
|
||||
bool m_has_cv_stream;
|
||||
float m_min_cv;
|
||||
float m_max_cv;
|
||||
float m_cv_scale;
|
||||
|
||||
// State.
|
||||
float m_fixed_gain;
|
||||
};
|
||||
|
||||
DECLARE_DEVICE_TYPE(VA_VCA, va_vca_device)
|
||||
|
||||
#endif // MAME_SOUND_VA_VCA_H
|
@ -101,6 +101,8 @@ Example:
|
||||
#include "sound/flt_vol.h"
|
||||
#include "sound/mixer.h"
|
||||
#include "sound/spkrdev.h"
|
||||
#include "sound/va_eg.h"
|
||||
#include "sound/va_vca.h"
|
||||
#include "speaker.h"
|
||||
|
||||
#include "linn_linndrum.lh"
|
||||
@ -113,7 +115,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 LOG_HAT_EG (1U << 9)
|
||||
|
||||
#define VERBOSE (LOG_GENERAL)
|
||||
//#define LOG_OUTPUT_FUNC osd_printf_info
|
||||
@ -182,156 +184,6 @@ 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
|
||||
{
|
||||
@ -361,6 +213,7 @@ private:
|
||||
static s32 get_ls267_freq(const std::array<s32, 2>& freq_range_hz, float cv);
|
||||
static float get_snare_tom_pitch_cv(float v);
|
||||
|
||||
TIMER_DEVICE_CALLBACK_MEMBER(hat_trigger_timer_tick);
|
||||
TIMER_DEVICE_CALLBACK_MEMBER(mux_timer_tick);
|
||||
TIMER_DEVICE_CALLBACK_MEMBER(snare_timer_tick);
|
||||
TIMER_DEVICE_CALLBACK_MEMBER(click_timer_tick);
|
||||
@ -374,11 +227,14 @@ private:
|
||||
|
||||
// Mux drums.
|
||||
required_ioport m_mux_tuning_trimmer;
|
||||
required_ioport m_hat_decay_pot;
|
||||
required_memory_region_array<NUM_MUX_VOICES> m_mux_samples;
|
||||
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;
|
||||
required_device<timer_device> m_hat_trigger_timer; // U37B (LM556).
|
||||
required_device<va_rc_eg_device> m_hat_eg;
|
||||
required_device<va_vca_device> m_hat_vca; // CEM3360 (U91B).
|
||||
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 };
|
||||
|
||||
@ -450,6 +306,13 @@ private:
|
||||
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.
|
||||
|
||||
// Constants for hi hat envelope generator circuit.
|
||||
static constexpr const float HAT_C22 = CAP_U(1);
|
||||
static constexpr const float HAT_R33 = RES_M(1);
|
||||
static constexpr const float HAT_R34 = RES_K(10);
|
||||
static constexpr const float HAT_DECAY_POT_R_MAX = RES_K(100);
|
||||
static constexpr const float HAT_MAX_CV = VCC * RES_VOLTAGE_DIVIDER(RES_K(8.2), RES_K(10)); // R67, R66.
|
||||
|
||||
// The audio pipeline operates on voltage magnitudes. This scaler normalizes
|
||||
// the final output's range to approximately: -1 - 1.
|
||||
static constexpr const float VOLTAGE_TO_SOUND_SCALER = 0.2F;
|
||||
@ -486,10 +349,13 @@ DEFINE_DEVICE_TYPE(LINNDRUM_AUDIO, linndrum_audio_device, "linndrum_audio_device
|
||||
linndrum_audio_device::linndrum_audio_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
|
||||
: device_t(mconfig, LINNDRUM_AUDIO, tag, owner, clock)
|
||||
, m_mux_tuning_trimmer(*this, ":pot_mux_tuning")
|
||||
, m_hat_decay_pot(*this, ":pot_tuning_7")
|
||||
, m_mux_samples(*this, ":sample_mux_drum_%u", 0)
|
||||
, 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_trigger_timer(*this, "hat_trigger_timer")
|
||||
, m_hat_eg(*this, "hat_eg")
|
||||
, m_hat_vca(*this, "hat_vca")
|
||||
, m_snare_samples(*this, ":sample_snare")
|
||||
, m_sidestick_samples(*this, ":sample_sidestick")
|
||||
@ -538,9 +404,22 @@ void linndrum_audio_device::mux_drum_w(int voice, u8 data, bool is_strobe)
|
||||
|
||||
if (voice == MV_HAT)
|
||||
{
|
||||
m_hat_vca->set_open(BIT(data, 2));
|
||||
float r = HAT_R33;
|
||||
const bool is_open_hat = BIT(data, 2);
|
||||
if (!is_open_hat)
|
||||
{
|
||||
const float r_decay = HAT_DECAY_POT_R_MAX * m_hat_decay_pot->read() / 100.0F;
|
||||
r = RES_2_PARALLEL(HAT_R33, HAT_R34 + r_decay);
|
||||
}
|
||||
m_hat_eg->set_r(r);
|
||||
LOGMASKED(LOG_HAT_EG, "Hat decay. Open: %d, r: %g\n", is_open_hat, r);
|
||||
|
||||
if (is_strobe)
|
||||
m_hat_vca->trigger();
|
||||
{
|
||||
m_hat_eg->set_instant_v(HAT_MAX_CV);
|
||||
m_hat_trigger_timer->adjust(PERIOD_OF_555_MONOSTABLE(RES_K(510), CAP_U(0.01))); // R8, C4.
|
||||
LOGMASKED(LOG_HAT_EG, "Hat EG triggered.\n");
|
||||
}
|
||||
}
|
||||
|
||||
LOGMASKED(LOG_STROBES, "Strobed mux drum %s: %02x (gain: %f)\n",
|
||||
@ -697,8 +576,11 @@ void linndrum_audio_device::device_add_mconfig(machine_config &config)
|
||||
m_mux_dac[voice]->add_route(0, m_mux_volume[voice], get_dac_scaler(MUX_DAC_IREF));
|
||||
}
|
||||
|
||||
LINNDRUM_HAT_VCA(config, m_hat_vca);
|
||||
TIMER(config, m_hat_trigger_timer).configure_generic(FUNC(linndrum_audio_device::hat_trigger_timer_tick)); // LM556 (U37B).
|
||||
VA_RC_EG(config, m_hat_eg).set_c(HAT_C22);
|
||||
VA_VCA(config, m_hat_vca).configure_streaming_cv(true).configure_cem3360_linear_cv();
|
||||
m_mux_volume[MV_HAT]->add_route(0, m_hat_vca, 1.0);
|
||||
m_hat_eg->add_route(0, m_hat_vca, 1.0);
|
||||
|
||||
// *** Snare / sidestick section.
|
||||
|
||||
@ -879,6 +761,12 @@ float linndrum_audio_device::get_snare_tom_pitch_cv(float v_tune)
|
||||
return std::clamp<float>(cv, 0, VCC);
|
||||
}
|
||||
|
||||
TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::hat_trigger_timer_tick)
|
||||
{
|
||||
m_hat_eg->set_target_v(0);
|
||||
LOGMASKED(LOG_HAT_EG, "Hat EG started decay.\n");
|
||||
}
|
||||
|
||||
TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::mux_timer_tick)
|
||||
{
|
||||
// The timer on the actual hardware ticks 4 times per voice. A combination
|
||||
|
@ -82,6 +82,8 @@ Usage notes:
|
||||
#include "sound/flt_rc.h"
|
||||
#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"
|
||||
|
||||
@ -93,9 +95,7 @@ Usage notes:
|
||||
#define LOG_SOUND (1U << 4)
|
||||
#define LOG_PITCH (1U << 5)
|
||||
#define LOG_VOLUME (1U << 6)
|
||||
#define LOG_SAMPLES (1U << 7)
|
||||
#define LOG_SAMPLES_DECAY (1U << 8)
|
||||
#define LOG_METRONOME (1U << 9)
|
||||
#define LOG_METRONOME (1U << 7)
|
||||
|
||||
#define VERBOSE (LOG_GENERAL)
|
||||
//#define LOG_OUTPUT_FUNC osd_printf_info
|
||||
@ -170,216 +170,6 @@ struct dmx_voice_card_config
|
||||
const filter_components filter;
|
||||
};
|
||||
|
||||
// The combination of the gain control circuit (which includes decay for some
|
||||
// voices), and the multiplying DAC form a VCA. The gain control circuit sets
|
||||
// the reference current into the DAC, and the DAC multiplies that with the
|
||||
// digital value to produce and output current.
|
||||
class dmx_voice_card_vca_device : public device_t, public device_sound_interface
|
||||
{
|
||||
public:
|
||||
dmx_voice_card_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, const dmx_voice_card_config &config) ATTR_COLD;
|
||||
dmx_voice_card_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD;
|
||||
|
||||
void start(int trigger_mode);
|
||||
void decay();
|
||||
|
||||
bool in_decay() const { return m_decaying; }
|
||||
|
||||
protected:
|
||||
void device_start() override ATTR_COLD;
|
||||
void device_reset() override ATTR_COLD;
|
||||
void sound_stream_update(sound_stream &stream, const std::vector<read_stream_view> &inputs, std::vector<write_stream_view> &outputs) override;
|
||||
|
||||
private:
|
||||
void init_gain_and_decay_variations(const dmx_voice_card_config &config) ATTR_COLD;
|
||||
|
||||
bool has_decay() const { return !m_decay_rc_inv.empty(); }
|
||||
bool has_decay_variations() const { return m_decay_rc_inv.size() > 1; }
|
||||
|
||||
// Configuration. Do not include in save state.
|
||||
sound_stream *m_stream = nullptr;
|
||||
const bool m_gain_control; // Is VCA configured for gain variations?
|
||||
std::vector<float> m_gain; // Gain variations.
|
||||
std::vector<float> m_decay_rc_inv; // Decay 1/RC variations.
|
||||
|
||||
// Device state.
|
||||
float m_selected_gain = 1;
|
||||
bool m_decaying = false;
|
||||
bool m_decay_done = false;
|
||||
float m_selected_rc_inv = 1;
|
||||
attotime m_decay_start_time;
|
||||
};
|
||||
|
||||
DEFINE_DEVICE_TYPE(DMX_VOICE_CARD_VCA, dmx_voice_card_vca_device, "dmx_voice_card_vca", "DMX Voice Card VCA");
|
||||
|
||||
dmx_voice_card_vca_device::dmx_voice_card_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, const dmx_voice_card_config &config)
|
||||
: device_t(mconfig, DMX_VOICE_CARD_VCA, tag, owner, 0)
|
||||
, device_sound_interface(mconfig, *this)
|
||||
, m_gain_control(!config.pitch_control)
|
||||
{
|
||||
init_gain_and_decay_variations(config);
|
||||
}
|
||||
|
||||
dmx_voice_card_vca_device::dmx_voice_card_vca_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
|
||||
: device_t(mconfig, DMX_VOICE_CARD_VCA, tag, owner, clock)
|
||||
, device_sound_interface(mconfig, *this)
|
||||
, m_gain_control(false)
|
||||
{
|
||||
}
|
||||
|
||||
void dmx_voice_card_vca_device::start(int trigger_mode)
|
||||
{
|
||||
assert(trigger_mode >= 1 && trigger_mode <= 3);
|
||||
|
||||
m_stream->update();
|
||||
m_decaying = false;
|
||||
m_decay_done = false;
|
||||
|
||||
if (m_gain_control)
|
||||
m_selected_gain = m_gain[trigger_mode];
|
||||
else
|
||||
m_selected_gain = m_gain[0];
|
||||
|
||||
if (has_decay_variations())
|
||||
m_selected_rc_inv = m_decay_rc_inv[trigger_mode];
|
||||
else if (has_decay())
|
||||
m_selected_rc_inv = m_decay_rc_inv[0];
|
||||
else
|
||||
m_selected_rc_inv = 1;
|
||||
|
||||
LOGMASKED(LOG_VOLUME, "Selected gain: %f, 1/RC: %f\n",
|
||||
m_selected_gain, m_selected_rc_inv);
|
||||
}
|
||||
|
||||
void dmx_voice_card_vca_device::decay()
|
||||
{
|
||||
assert(has_decay());
|
||||
if (!has_decay())
|
||||
return;
|
||||
|
||||
m_stream->update();
|
||||
m_decaying = true;
|
||||
m_decay_start_time = machine().time();
|
||||
}
|
||||
|
||||
void dmx_voice_card_vca_device::device_start()
|
||||
{
|
||||
m_stream = stream_alloc(1, 1, machine().sample_rate());
|
||||
|
||||
save_item(NAME(m_selected_gain));
|
||||
save_item(NAME(m_decaying));
|
||||
save_item(NAME(m_decay_done));
|
||||
save_item(NAME(m_selected_rc_inv));
|
||||
save_item(NAME(m_decay_start_time));
|
||||
}
|
||||
|
||||
void dmx_voice_card_vca_device::device_reset()
|
||||
{
|
||||
m_selected_gain = 1;
|
||||
m_decaying = false;
|
||||
m_decay_done = false;
|
||||
m_selected_rc_inv = 1;
|
||||
}
|
||||
|
||||
void dmx_voice_card_vca_device::sound_stream_update(sound_stream &stream, const std::vector<read_stream_view> &inputs, std::vector<write_stream_view> &outputs)
|
||||
{
|
||||
// Gain lower than MIN_GAIN will be treated as 0.
|
||||
static constexpr const float MIN_GAIN = 0.0001F;
|
||||
|
||||
const read_stream_view &in = inputs[0];
|
||||
write_stream_view &out = outputs[0];
|
||||
const int n = in.samples();
|
||||
|
||||
if (!m_decaying) // Just gain variation without decay.
|
||||
{
|
||||
for (int i = 0; i < n; ++i)
|
||||
out.put(i, m_selected_gain * in.get(i));
|
||||
|
||||
LOGMASKED(LOG_SAMPLES, "%s VCA - just gain: %f. Samples: %f, %f.\n",
|
||||
tag(), m_selected_gain, in.get(0), in.get(n - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_decay_done) // Avoid expensive expf() call if volume has decayed.
|
||||
{
|
||||
out.fill(0);
|
||||
LOGMASKED(LOG_SAMPLES, "%s VCA - decay done.\n", tag());
|
||||
return;
|
||||
}
|
||||
|
||||
attotime t = in.start_time() - m_decay_start_time;
|
||||
assert(!m_decaying || t >= attotime::from_double(0));
|
||||
|
||||
float gain = 1;
|
||||
for (int i = 0; i < n; ++i, t += in.sample_period())
|
||||
{
|
||||
const float decay = expf(-t.as_double() * m_selected_rc_inv);
|
||||
gain = decay * m_selected_gain;
|
||||
out.put(i, gain * in.get(i));
|
||||
}
|
||||
|
||||
if (gain < MIN_GAIN)
|
||||
m_decay_done = true;
|
||||
|
||||
LOGMASKED(LOG_SAMPLES_DECAY, "%s VCA - in decay: %f. Samples: %f, %f.\n",
|
||||
tag(), gain, in.get(0), in.get(n - 1));
|
||||
}
|
||||
|
||||
void dmx_voice_card_vca_device::init_gain_and_decay_variations(const dmx_voice_card_config &config)
|
||||
{
|
||||
static constexpr const float VD = 0.6; // Diode drop.
|
||||
static constexpr const float R8 = RES_K(2.7);
|
||||
static constexpr const float R9 = RES_K(5.6);
|
||||
static constexpr const float MAX_IREF = VCC / (R8 + R9);
|
||||
|
||||
const float r12 = config.r12;
|
||||
const float r17 = config.r17;
|
||||
const float c3 = config.c3;
|
||||
|
||||
// Precompute gain variations.
|
||||
m_gain.clear();
|
||||
m_gain.push_back(MAX_IREF);
|
||||
if (m_gain_control) // Configured for gain variations.
|
||||
{
|
||||
// The equations below were derived from Kirchhoff analysis and verified
|
||||
// with simulations: https://tinyurl.com/22wxwh8h
|
||||
|
||||
// For trigger mode 1.
|
||||
m_gain.push_back((r12 * r17 * VCC + R8 * r12 * VD + R8 * r17 * VD) /
|
||||
((R8 * r12 * r17) + (r12 * r17 * R9) + (R8 * r17 * R9) + (R8 * r12 * R9)));
|
||||
// For trigger mode 2.
|
||||
m_gain.push_back((r12 * VCC + R8 * VD) / (r12 * R8 + R8 * R9 + r12 * R9));
|
||||
// For trigger mode 3.
|
||||
m_gain.push_back(m_gain[0]);
|
||||
}
|
||||
for (int i = 0; i < m_gain.size(); ++i)
|
||||
{
|
||||
LOGMASKED(LOG_VOLUME, "%s: Gain variation %d: %f uA, %f\n",
|
||||
tag(), i, m_gain[i] * 1e6F, m_gain[i] / MAX_IREF);
|
||||
m_gain[i] /= MAX_IREF; // Normalize.
|
||||
}
|
||||
|
||||
// Precompute decay variations.
|
||||
m_decay_rc_inv.clear();
|
||||
if (config.decay != dmx_voice_card_config::decay_mode::DISABLED && c3 > 0)
|
||||
{
|
||||
std::vector<float> r_lower;
|
||||
r_lower.push_back(R9); // For when there are no variations.
|
||||
if (m_gain_control)
|
||||
{
|
||||
r_lower.push_back(RES_3_PARALLEL(R9, r12, r17)); // For trigger mode 1.
|
||||
r_lower.push_back(RES_2_PARALLEL(R9, r12)); // For trigger mode 2.
|
||||
r_lower.push_back(R9); // For trigger mode 3.
|
||||
}
|
||||
for (float r : r_lower)
|
||||
{
|
||||
m_decay_rc_inv.push_back(1.0F / ((R8 + r) * c3));
|
||||
LOGMASKED(LOG_VOLUME, "%s: Decay 1/RC variation %d: %f\n",
|
||||
tag(), m_decay_rc_inv.size() - 1, m_decay_rc_inv.back());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emulates the original DMX voice cards, including the cymbal card. Later
|
||||
// DMX models shipped with the "Mark II" voice cards for the Tom voices.
|
||||
// The Mark II cards are not yet emulated.
|
||||
@ -407,6 +197,11 @@ private:
|
||||
void compute_pitch_variations();
|
||||
void select_pitch();
|
||||
|
||||
void init_gain_and_decay_variations() ATTR_COLD;
|
||||
bool has_decay() const { return !m_decay_r.empty(); }
|
||||
bool has_decay_variations() const { return m_decay_r.size() > 1; }
|
||||
bool has_gain_variations() const { return !m_config.pitch_control; }
|
||||
|
||||
bool is_decay_enabled() const;
|
||||
bool is_early_decay_enabled() const;
|
||||
TIMER_DEVICE_CALLBACK_MEMBER(clock_callback);
|
||||
@ -415,7 +210,8 @@ private:
|
||||
|
||||
required_device<timer_device> m_timer; // 555, U5.
|
||||
required_device<dac76_device> m_dac; // AM6070, U8. Compatible with DAC76.
|
||||
required_device<dmx_voice_card_vca_device> m_vca;
|
||||
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;
|
||||
|
||||
// Configuration. Do not include in save state.
|
||||
@ -424,11 +220,14 @@ private:
|
||||
std::vector<float> m_cv; // 555 CV (pin 5) voltage variations.
|
||||
std::vector<attotime> m_sample_t; // Sample period variations.
|
||||
s32 m_t1_percent = T1_DEFAULT_PERCENT;
|
||||
std::vector<float> m_gain; // Gain variations.
|
||||
std::vector<float> m_decay_r; // Decay resistance variations.
|
||||
|
||||
// Device state.
|
||||
bool m_counting = false;
|
||||
u16 m_counter = 0; // 4040 counter.
|
||||
u8 m_trigger_mode = 0; // Valid modes: 1-3. 0 OK after reset.
|
||||
bool m_decaying = false;
|
||||
};
|
||||
|
||||
DEFINE_DEVICE_TYPE(DMX_VOICE_CARD, dmx_voice_card_device, "dmx_voice_card", "DMX Voice Card");
|
||||
@ -438,12 +237,14 @@ 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_vca(*this, "dmx_vca")
|
||||
, m_dac_mult(*this, "dac_mult_u8")
|
||||
, m_eg(*this, "envelope_generator")
|
||||
, m_filters(*this, "aa_sk_filter_%d", 0)
|
||||
, m_config(config)
|
||||
, m_sample_rom(sample_rom)
|
||||
{
|
||||
init_pitch();
|
||||
init_gain_and_decay_variations();
|
||||
}
|
||||
|
||||
dmx_voice_card_device::dmx_voice_card_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
|
||||
@ -451,7 +252,8 @@ 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_vca(*this, "dmx_vca")
|
||||
, 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.
|
||||
, m_config(dmx_voice_card_config{.filter={1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}})
|
||||
@ -473,11 +275,18 @@ void dmx_voice_card_device::trigger(bool tr0, bool tr1)
|
||||
m_stream->update();
|
||||
m_counter = 0;
|
||||
m_counting = true;
|
||||
m_decaying = false;
|
||||
|
||||
if (m_config.pitch_control)
|
||||
select_pitch();
|
||||
m_vca->start(m_trigger_mode);
|
||||
|
||||
LOGMASKED(LOG_SOUND, "Trigger: (%d, %d) %d %f\n", tr0, tr1, m_trigger_mode);
|
||||
const float gain = has_gain_variations() ? m_gain[m_trigger_mode] : m_gain[0];
|
||||
if (has_decay()) // Gain is controled by an envelope generator.
|
||||
m_eg->set_instant_v(gain);
|
||||
else // Constant gain.
|
||||
m_dac_mult->set_fixed_cv(gain);
|
||||
|
||||
LOGMASKED(LOG_SOUND, "Trigger: (%d, %d) %d, %f\n", tr0, tr1, m_trigger_mode, gain);
|
||||
}
|
||||
|
||||
void dmx_voice_card_device::set_pitch_adj(s32 t1_percent)
|
||||
@ -494,7 +303,15 @@ void dmx_voice_card_device::device_add_mconfig(machine_config &config)
|
||||
|
||||
TIMER(config, m_timer).configure_generic(FUNC(dmx_voice_card_device::clock_callback));
|
||||
DAC76(config, m_dac, 0U);
|
||||
DMX_VOICE_CARD_VCA(config, m_vca, m_config);
|
||||
VA_VCA(config, m_dac_mult);
|
||||
m_dac->add_route(0, m_dac_mult, 1.0);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
FILTER_BIQUAD(config, m_filters[0]).opamp_sk_lowpass_setup(
|
||||
m_config.filter.r15, m_config.filter.r14, SK_R3, SK_R4,
|
||||
@ -506,12 +323,10 @@ 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->add_route(ALL_OUTPUTS, m_vca, 1.0);
|
||||
m_vca->add_route(ALL_OUTPUTS, m_filters[0], 1.0);
|
||||
|
||||
m_filters[0]->add_route(ALL_OUTPUTS, m_filters[1], 1.0);
|
||||
m_filters[1]->add_route(ALL_OUTPUTS, m_filters[2], 1.0);
|
||||
m_filters[2]->add_route(ALL_OUTPUTS, *this, 1.0);
|
||||
m_dac_mult->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);
|
||||
}
|
||||
|
||||
void dmx_voice_card_device::device_start()
|
||||
@ -521,18 +336,22 @@ void dmx_voice_card_device::device_start()
|
||||
save_item(NAME(m_counting));
|
||||
save_item(NAME(m_counter));
|
||||
save_item(NAME(m_trigger_mode));
|
||||
save_item(NAME(m_decaying));
|
||||
}
|
||||
|
||||
void dmx_voice_card_device::device_reset()
|
||||
{
|
||||
m_trigger_mode = 0;
|
||||
m_decaying = false;
|
||||
reset_counter();
|
||||
compute_pitch_variations();
|
||||
if (m_eg)
|
||||
m_eg->set_instant_v(0);
|
||||
}
|
||||
|
||||
void dmx_voice_card_device::sound_stream_update(sound_stream &stream, const std::vector<read_stream_view> &inputs, std::vector<write_stream_view> &outputs)
|
||||
{
|
||||
outputs[0] = inputs[0];
|
||||
outputs[0].copy(inputs[0]);
|
||||
}
|
||||
|
||||
void dmx_voice_card_device::reset_counter()
|
||||
@ -662,6 +481,56 @@ void dmx_voice_card_device::select_pitch()
|
||||
1.0 / sampling_t.as_double());
|
||||
}
|
||||
|
||||
void dmx_voice_card_device::init_gain_and_decay_variations()
|
||||
{
|
||||
static constexpr const float VD = 0.6; // Diode drop.
|
||||
static constexpr const float R8 = RES_K(2.7);
|
||||
static constexpr const float R9 = RES_K(5.6);
|
||||
static constexpr const float MAX_IREF = VCC / (R8 + R9);
|
||||
|
||||
const float r12 = m_config.r12;
|
||||
const float r17 = m_config.r17;
|
||||
const float c3 = m_config.c3;
|
||||
|
||||
// Precompute gain variations.
|
||||
m_gain.clear();
|
||||
m_gain.push_back(MAX_IREF);
|
||||
if (has_gain_variations()) // Configured gain variations.
|
||||
{
|
||||
// The equations below were derived from Kirchhoff analysis and verified
|
||||
// with simulations: https://tinyurl.com/22wxwh8h
|
||||
|
||||
// For trigger mode 1.
|
||||
m_gain.push_back((r12 * r17 * VCC + R8 * r12 * VD + R8 * r17 * VD) /
|
||||
((R8 * r12 * r17) + (r12 * r17 * R9) + (R8 * r17 * R9) + (R8 * r12 * R9)));
|
||||
// For trigger mode 2.
|
||||
m_gain.push_back((r12 * VCC + R8 * VD) / (r12 * R8 + R8 * R9 + r12 * R9));
|
||||
// For trigger mode 3.
|
||||
m_gain.push_back(m_gain[0]);
|
||||
}
|
||||
for (int i = 0; i < m_gain.size(); ++i)
|
||||
{
|
||||
LOGMASKED(LOG_VOLUME, "%s: Gain variation %d: %f uA, %f\n",
|
||||
tag(), i, m_gain[i] * 1e6F, m_gain[i] / MAX_IREF);
|
||||
m_gain[i] /= MAX_IREF; // Normalize.
|
||||
}
|
||||
|
||||
// Precompute decay resistance variations.
|
||||
m_decay_r.clear();
|
||||
if (m_config.decay != dmx_voice_card_config::decay_mode::DISABLED && c3 > 0)
|
||||
{
|
||||
m_decay_r.push_back(R8 + R9); // For when there are no variations.
|
||||
if (has_gain_variations()) // Gain variations imply decay variations.
|
||||
{
|
||||
m_decay_r.push_back(R8 + RES_3_PARALLEL(R9, r12, r17)); // For trigger mode 1.
|
||||
m_decay_r.push_back(R8 + RES_2_PARALLEL(R9, r12)); // For trigger mode 2.
|
||||
m_decay_r.push_back(R8 + R9); // For trigger mode 3.
|
||||
}
|
||||
for (int i = 0; i < m_decay_r.size(); ++i)
|
||||
LOGMASKED(LOG_VOLUME, "%s: Decay R variation %d: %f\n", tag(), i, m_decay_r[i]);
|
||||
}
|
||||
}
|
||||
|
||||
bool dmx_voice_card_device::is_decay_enabled() const
|
||||
{
|
||||
switch (m_config.decay)
|
||||
@ -721,12 +590,19 @@ TIMER_DEVICE_CALLBACK_MEMBER(dmx_voice_card_device::clock_callback)
|
||||
// the counter's bit 10 transitions to 1.
|
||||
static constexpr const u16 LATE_DECAY_START = 1 << 10;
|
||||
|
||||
if (!m_vca->in_decay() && is_decay_enabled())
|
||||
if (!m_decaying && is_decay_enabled())
|
||||
{
|
||||
if ((is_early_decay_enabled() && m_counter >= EARLY_DECAY_START) ||
|
||||
m_counter >= LATE_DECAY_START)
|
||||
{
|
||||
m_vca->decay();
|
||||
assert(has_decay());
|
||||
m_decaying = true;
|
||||
if (has_decay_variations())
|
||||
m_eg->set_r(m_decay_r[m_trigger_mode]);
|
||||
else
|
||||
m_eg->set_r(m_decay_r[0]);
|
||||
m_eg->set_target_v(0);
|
||||
LOGMASKED(LOG_SOUND, "%s: Start decay\n", tag());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ until a restart or reset (F3).
|
||||
#include "bus/midi/midiinport.h"
|
||||
#include "bus/midi/midioutport.h"
|
||||
#include "machine/rescap.h"
|
||||
#include "sound/va_eg.h"
|
||||
#include "video/pwm.h"
|
||||
|
||||
#include "attotime.h"
|
||||
@ -71,67 +72,6 @@ until a restart or reset (F3).
|
||||
|
||||
#include "logmacro.h"
|
||||
|
||||
// Emulates a (DC) RC circuit with a variable resistance. Useful for emulating
|
||||
// envelope generators. This is not a simulation, just uses the standard RC
|
||||
// equations to return voltage at a specific time.
|
||||
class fatman_rc_network_state_device : public device_t
|
||||
{
|
||||
public:
|
||||
fatman_rc_network_state_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0) ATTR_COLD;
|
||||
|
||||
void set_c(float c);
|
||||
void reset(float v_target, float r, const attotime &t);
|
||||
float get_v(const attotime &t) const;
|
||||
|
||||
protected:
|
||||
void device_start() override ATTR_COLD;
|
||||
|
||||
private:
|
||||
// Initialize to a slow (large RC) but valid charging state.
|
||||
float m_c = CAP_U(1000);
|
||||
float m_r = RES_M(100);
|
||||
float m_v_start = 0;
|
||||
float m_v_end = 1;
|
||||
attotime m_start_t;
|
||||
};
|
||||
|
||||
DEFINE_DEVICE_TYPE(FATMAN_RC_NETWORK_STATE, fatman_rc_network_state_device, "fatman_rc_network_state", "Fatman EG RC network");
|
||||
|
||||
fatman_rc_network_state_device::fatman_rc_network_state_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
|
||||
: device_t(mconfig, FATMAN_RC_NETWORK_STATE, tag, owner, clock)
|
||||
{
|
||||
}
|
||||
|
||||
void fatman_rc_network_state_device::device_start()
|
||||
{
|
||||
save_item(NAME(m_c));
|
||||
save_item(NAME(m_r));
|
||||
save_item(NAME(m_v_start));
|
||||
save_item(NAME(m_v_end));
|
||||
save_item(NAME(m_start_t));
|
||||
}
|
||||
|
||||
void fatman_rc_network_state_device::set_c(float c)
|
||||
{
|
||||
m_c = c;
|
||||
}
|
||||
|
||||
void fatman_rc_network_state_device::reset(float v_target, float r, const attotime &t)
|
||||
{
|
||||
m_v_start = get_v(t);
|
||||
m_v_end = v_target;
|
||||
m_r = r;
|
||||
m_start_t = t;
|
||||
}
|
||||
|
||||
float fatman_rc_network_state_device::get_v(const attotime &t) const
|
||||
{
|
||||
assert(t >= m_start_t);
|
||||
const attotime delta_t = t - m_start_t;
|
||||
return m_v_start + (m_v_end - m_v_start) * (1.0F - expf(-delta_t.as_double() / (m_r * m_c)));
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char MAINCPU_TAG[] = "8031";
|
||||
@ -142,8 +82,8 @@ public:
|
||||
fatman_state(const machine_config &mconfig, device_type type, const char *tag) ATTR_COLD
|
||||
: driver_device(mconfig, type, tag)
|
||||
, m_maincpu(*this, MAINCPU_TAG)
|
||||
, m_vca_adsr_state(*this, "vca_dsr_eg")
|
||||
, m_vcf_ar_state(*this, "vcf_ar_eg")
|
||||
, m_vca_adsr(*this, "vca_adsr_eg")
|
||||
, m_vcf_ar(*this, "vcf_ar_eg")
|
||||
, m_midi_led_pwm(*this, "midi_led_pwm_device")
|
||||
, m_gate_led(*this, "gate_led")
|
||||
, m_dsw_io(*this, "dsw")
|
||||
@ -170,7 +110,7 @@ private:
|
||||
int vca_adsr_attack_r() const;
|
||||
void vca_adsr_decay_w(int state);
|
||||
void vca_adsr_release_w(int state);
|
||||
void update_vca_adsr_state();
|
||||
void update_vca_adsr();
|
||||
|
||||
int vcf_ar_attack_r() const;
|
||||
void vcf_ar_release_w(int state);
|
||||
@ -189,8 +129,8 @@ private:
|
||||
void external_memory_map(address_map &map) ATTR_COLD;
|
||||
|
||||
required_device<mcs51_cpu_device> m_maincpu;
|
||||
required_device<fatman_rc_network_state_device> m_vca_adsr_state;
|
||||
required_device<fatman_rc_network_state_device> m_vcf_ar_state;
|
||||
required_device<va_rc_eg_device> m_vca_adsr;
|
||||
required_device<va_rc_eg_device> m_vcf_ar;
|
||||
required_device<pwm_display_device> m_midi_led_pwm; // D2
|
||||
output_finder<> m_gate_led; // D13
|
||||
required_ioport m_dsw_io;
|
||||
@ -265,8 +205,7 @@ void fatman_state::midi_rxd_w(int state)
|
||||
|
||||
int fatman_state::vca_adsr_attack_r() const
|
||||
{
|
||||
const float v = m_vca_adsr_state->get_v(machine().time());
|
||||
return (v >= V_EG_COMP) ? 1 : 0;
|
||||
return (m_vca_adsr->get_v() >= V_EG_COMP) ? 1 : 0;
|
||||
}
|
||||
|
||||
void fatman_state::vca_adsr_decay_w(int state)
|
||||
@ -277,7 +216,7 @@ void fatman_state::vca_adsr_decay_w(int state)
|
||||
|
||||
m_vca_adsr_decay = decay;
|
||||
LOGMASKED(LOG_EG, "vca decay: %d\n", m_vca_adsr_decay);
|
||||
update_vca_adsr_state();
|
||||
update_vca_adsr();
|
||||
}
|
||||
|
||||
void fatman_state::vca_adsr_release_w(int state)
|
||||
@ -292,10 +231,10 @@ void fatman_state::vca_adsr_release_w(int state)
|
||||
// LED cathode connected to ground, anode connected to IC7:F inverter,
|
||||
// which inverts the 'release' signal.
|
||||
m_gate_led = m_vca_adsr_release ? 0 : 1;
|
||||
update_vca_adsr_state();
|
||||
update_vca_adsr();
|
||||
}
|
||||
|
||||
void fatman_state::update_vca_adsr_state()
|
||||
void fatman_state::update_vca_adsr()
|
||||
{
|
||||
// This function only needs to be called on a state change. For instance, if
|
||||
// either m_vca_adsr_decay or m_vca_adsr_release has changed.
|
||||
@ -303,15 +242,14 @@ void fatman_state::update_vca_adsr_state()
|
||||
if (m_vca_adsr_decay && m_vca_adsr_release) // Release.
|
||||
{
|
||||
const float r96 = m_vca_release_pot->read() * POT_R96 / 100.0F;
|
||||
m_vca_adsr_state->reset(V_GND, r96 + R95, machine().time());
|
||||
m_vca_adsr->set_r(r96 + R95).set_target_v(V_GND);
|
||||
}
|
||||
else if (m_vca_adsr_decay) // Decay.
|
||||
{
|
||||
const attotime now = machine().time();
|
||||
const float sustain_v = V_PLUS * m_vca_sustain_pot->read() / 100.0F;
|
||||
const float current_v = m_vca_adsr_state->get_v(now);
|
||||
const float current_v = m_vca_adsr->get_v();
|
||||
const float r92 = m_vca_decay_pot->read() * POT_R92 / 100.0F;
|
||||
m_vca_adsr_state->reset(std::min(sustain_v, current_v), r92 + R91, now);
|
||||
m_vca_adsr->set_r(r92 + R91).set_target_v(std::min(sustain_v, current_v));
|
||||
}
|
||||
else if (m_vca_adsr_release) // "Invalid" state.
|
||||
{
|
||||
@ -324,12 +262,12 @@ void fatman_state::update_vca_adsr_state()
|
||||
const float r_release = r96 + R95;
|
||||
const float target_v = V_PLUS * RES_VOLTAGE_DIVIDER(r_attack, r_release);
|
||||
const float effective_r = RES_2_PARALLEL(r_attack, r_release);
|
||||
m_vca_adsr_state->reset(target_v, effective_r, machine().time());
|
||||
m_vca_adsr->set_r(effective_r).set_target_v(target_v);
|
||||
}
|
||||
else // Attack.
|
||||
{
|
||||
const float r94 = m_vca_attack_pot->read() * POT_R94 / 100.0F;
|
||||
m_vca_adsr_state->reset(V_PLUS, POT_R90 + R93 + r94, machine().time());
|
||||
m_vca_adsr->set_r(POT_R90 + R93 + r94).set_target_v(V_PLUS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,8 +281,7 @@ int fatman_state::vcf_ar_attack_r() const
|
||||
// completed, and it doesn't start the EG release.
|
||||
return 0;
|
||||
}
|
||||
const float v = m_vcf_ar_state->get_v(machine().time());
|
||||
return (v >= V_EG_COMP) ? 1 : 0;
|
||||
return (m_vcf_ar->get_v() >= V_EG_COMP) ? 1 : 0;
|
||||
}
|
||||
|
||||
void fatman_state::vcf_ar_release_w(int state)
|
||||
@ -359,12 +296,12 @@ void fatman_state::vcf_ar_release_w(int state)
|
||||
if (m_vcf_ar_release) // Start release.
|
||||
{
|
||||
const float r82 = m_vcf_release_pot->read() * POT_R82 / 100.0;
|
||||
m_vcf_ar_state->reset(V_GND, r82 + R81, machine().time());
|
||||
m_vcf_ar->set_r(r82 + R81).set_target_v(V_GND);
|
||||
}
|
||||
else // Start attack.
|
||||
{
|
||||
const float r84 = m_vcf_attack_pot->read() * POT_R84 / 100.0;
|
||||
m_vcf_ar_state->reset(V_PLUS, R80 + R83 + r84, machine().time());
|
||||
m_vcf_ar->set_r(R80 + R83 + r84).set_target_v(V_PLUS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -505,8 +442,8 @@ void fatman_state::fatman(machine_config &config)
|
||||
m_maincpu->port_out_cb<3>().set(FUNC(fatman_state::vcf_ar_release_w)).bit(1);
|
||||
m_maincpu->port_out_cb<3>().append(FUNC(fatman_state::cv_mux_w)).mask(0x30); // Bits 4, 5.
|
||||
|
||||
FATMAN_RC_NETWORK_STATE(config, m_vca_adsr_state).set_c(C19);
|
||||
FATMAN_RC_NETWORK_STATE(config, m_vcf_ar_state).set_c(C22);
|
||||
VA_RC_EG(config, m_vca_adsr).set_c(C19);
|
||||
VA_RC_EG(config, m_vcf_ar).set_c(C22);
|
||||
|
||||
midi_port_device &midi_in(MIDI_PORT(config, "mdin", midiin_slot, "midiin"));
|
||||
MIDI_PORT(config, "mdthru", midiout_slot, "midiout");
|
||||
|
Loading…
Reference in New Issue
Block a user