linn/linndrum.cpp: First pass at audio emulation. (#13458)

* linn/linndrum.cpp: First pass at audio emulation. Promoted to working.

* linndrum: Remove stray & from tom variation calculation.
This commit is contained in:
m1macrophage 2025-03-14 09:07:00 -07:00 committed by GitHub
parent 62fa67a5a5
commit 77ece1c0db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -10,14 +10,64 @@ The firmware runs on a Z80. It controls the UI (reads buttons, drives displays
and LEDs), synchronization with other devices, cassette I/O, and voice
triggering.
The LinnDrum has 12-voice polyphony, not including the "click" (metronome).
It has a total of 15 voices, along with a "click" sound. Some of the voices have
variations, which brings the total to 24 different sounds.
The LinnDrum has 10 voice cores, not including the "click" (metronome) and
"beep" sounds. Many of the voices have variations (loudness, pitch, sample
selection, decay time), bringing the number of possible sounds to 28. However,
end-users can only trigger 23 sounds, and can control mixing and panning
for 15 of them (some are grouped together). Each voice core can run
independently, for a max polyphony of 10. Only one variation per core can be
active at a time. The "click" and "beep" sounds can be played independently of
each other and the voice cores.
There are multiple voice architectures:
* The "mux drums" section consists of 8 voice cores. Bass, cabasa, tambourine,
clap and cowbell (4K samples each), hi-hat (16K) and ride and crash cymbals
(32K each). Voices other than the clap and cowbell can be attenuated, to
create 2 loudness variations. The clap and cowbell always play at full volume.
There is a single, time-multiplexed DAC used for all voices, and each voice
has its own output. All 8 voices can be played simultaneously, but only a
single loudness variation of each. The bass drum is post-processed by a
CEM3320 VCF with a hardcoded envelope, but all other voices lack a
reconstruction filter. The hat is post-processed by a CEM3360 VCA, which can
operate in 2 variations: slow decay (open hat) and faster, user-controlled
decay (closed hat). There's a single clock for all voices, which is tuned by
a trimmer on the circuit board.
* The "snare / sidestick" section consists of a single voice core (and single
DAC) which can either play the sidestick, or the snare voice (4K samples
each). The two voices have individual outputs, and their volume and pan can be
set independently. There are 4 possible loudness variations for each voice,
though the end-user only has access to 3 for the snare and 1 for the
sidestick. The voice core output is post-processed by a single-pole lowpass RC
filter. The end-user can control the sample rate via a tuning knob.
This section also includes the circuit for the "click" sound (metronome),
which triggers pulses of fixed length, and the circuit for the "beep" sound,
which allows the firmware to generate pulses of arbitrary length.
* The "tom / conga" section is similar to the "snare / sidestick" one. It
consists of a single voice core (and single DAC) which can either play the tom
or the conga voice (8K samples each). There are 3 pitch variations for the
tom, and 2 pitch variations for the conga. The end-user can set the 5 pitches
using the corresponding tuning knobs. Each of those 5 variations has its own
output, and can be mixed and panned independently, even though only a single
one can be active at a time. All variations are post-processed by a CEM3320
VCF with a hardcoded envelope.
The driver is based on the LinnDrum's service manual and schematics, and is
intended as an educational tool.
Most of the digital functionality is emulated. Audio is not yet emulated.
Most of the digital functionality is emulated. Audio is partially emulated.
Reasons for MACHINE_IMPERFECT_SOUND:
* Missing a few sample checksums.
* Volume and pan sliders don't work yet.
* Tuning knobs and trimmers don't work yet (no tom and conga variations).
* 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.
PCBoards:
* CPU board. 2 sections in schematics:
@ -33,14 +83,12 @@ PCBoards:
Usage:
The driver includes an interactive layout.
The driver includes an interactive layout. Make sure to enable plugins so that
sliders and knobs can be manipulated with the mouse. It is recommended to run
with a high sample rate.
Since there is no audio, the driver logs triggers and other info. To see those,
run the driver with `-log`:
./mame -window linndrum -log
Tail the log file:
(on linux): tail -f error.log
Example:
./mame -window -samplerate 96000 -plugins -plugin layout linndrum
*/
#include "emu.h"
@ -49,6 +97,11 @@ Tail the log file:
#include "machine/output_latch.h"
#include "machine/rescap.h"
#include "machine/timer.h"
#include "sound/dac76.h"
#include "sound/flt_rc.h"
#include "sound/flt_vol.h"
#include "sound/spkrdev.h"
#include "speaker.h"
#include "linn_linndrum.lh"
@ -56,25 +109,449 @@ Tail the log file:
#define LOG_DEBOUNCE (1U << 2)
#define LOG_TEMPO (1U << 3)
#define LOG_TEMPO_CHANGE (1U << 4)
#define LOG_TRIGGERS (1U << 5)
#define LOG_STROBES (1U << 5)
#define LOG_TAPE_SYNC_ENABLE (1U << 6)
#define LOG_VOLUME (1U << 7)
#define VERBOSE (LOG_GENERAL | LOG_TEMPO_CHANGE | LOG_TAPE_SYNC_ENABLE | LOG_TRIGGERS)
#define VERBOSE (LOG_GENERAL | LOG_STROBES)
//#define LOG_OUTPUT_FUNC osd_printf_info
#include "logmacro.h"
namespace {
enum mux_voices
{
TAMBOURINE = 0,
CABASA,
CLAP,
COWBELL,
BASS,
HAT,
RIDE,
CRASH,
NUM_MUX_VOICES
};
} // anonymous namespace
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 voice_data_enable_w(int state);
template <int Voice> void strobe_mux_drum_w(u8 data);
void strobe_snare_w(u8 data); // Snare and sidestick.
void strobe_tom_w(u8 data); // Tom and conga.
void strobe_click_w(u8 data);
void beep_w(int state);
protected:
void device_add_mconfig(machine_config &config) override ATTR_COLD;
void device_start() override ATTR_COLD;
private:
static void write_dac(dac76_device& dac, u8 sample);
void set_mux_drum_volume(int voice, bool d1);
void set_snare_volume(bool d3, bool d2);
u8 get_voice_data(u8 data) const;
TIMER_DEVICE_CALLBACK_MEMBER(mux_timer_tick);
TIMER_DEVICE_CALLBACK_MEMBER(snare_timer_tick);
TIMER_DEVICE_CALLBACK_MEMBER(click_timer_tick);
TIMER_DEVICE_CALLBACK_MEMBER(tom_timer_tick);
// Mux drums.
required_memory_region_array<NUM_MUX_VOICES> m_mux_samples;
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.
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 };
// Snare / sidestick.
required_memory_region m_snare_samples; // 2732 ROM (U79).
required_memory_region m_sidestick_samples; // 2732 ROMs (U78).
required_device<dac76_device> m_snare_dac; // AM6070 (U92).
required_device<filter_volume_device> m_snare_volume; // R69, R72, R71, R70.
required_device<timer_device> m_click_timer; // 556 (U65A).
required_device<speaker_sound_device> m_click;
required_device<speaker_sound_device> m_beep;
bool m_snare_counting = false; // /Q1 of U41 (74LS74).
u16 m_snare_counter = 0; // 13-bit counter (2 x 74LS393, U61, U62).
bool m_sidestick_selected = false; // Chooses between snare and sidestick.
// Tom / conga.
required_memory_region m_tom_samples; // 2 x 2732 ROMs (U68, U69).
required_memory_region m_conga_samples; // 2 x 2732 ROMs (U66, U67).
required_device<timer_device> m_tom_timer; // 74LS627 (U77B).
required_device<dac76_device> m_tom_dac; // AM6070 (U82).
bool m_tom_counting = false; // /Q1 of U73 (74LS74).
u16 m_tom_counter = 0; // 14-bit counter (2 x 74LS393, U70, U71).
bool m_tom_selected = false; // Selects between tom and conga.
u8 m_tom_variation = 0; // Tom / conga pitch variation.
bool m_voice_data_enabled = false; // Enables/disables U19 (74LS365).
static constexpr const float VPLUS = 15; // Volts.
static constexpr const float VCC = 5; // Volts.
static constexpr const char *MUX_VOICE_NAMES[NUM_MUX_VOICES] =
{
"TAMBOURINE", "CABASA", "CLAP", "COWBELL", "BASS", "HAT", "RIDE", "CRASH"
};
};
DEFINE_DEVICE_TYPE(LINNDRUM_AUDIO, linndrum_audio_device, "linndrum_audio_device", "LinnDrum audio circuits");
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_samples(*this, ":sample_mux_drum_%u", 0)
, m_mux_dac(*this, "mux_drums_virtual_dac_%u", 1)
, m_mux_volume(*this, "mux_drums_volume_control_%u", 1)
, m_snare_samples(*this, ":sample_snare")
, m_sidestick_samples(*this, ":sample_sidestick")
, m_snare_dac(*this, "snare_sidestick_dac")
, m_snare_volume(*this, "snare_sidestick_volume")
, m_click_timer(*this, "click_timer")
, m_click(*this, "click")
, m_beep(*this, "beep")
, m_tom_samples(*this, ":sample_tom")
, m_conga_samples(*this, ":sample_conga")
, m_tom_timer(*this, "tom_conga_timer")
, m_tom_dac(*this, "tom_conga_dac")
{
}
void linndrum_audio_device::voice_data_enable_w(int state)
{
// Controls whether data (D0-D3) is transmitted to the voice circuits. This
// is done by U19 (74LS365 hex buffer. Enable inputs are active-low).
// This is usually disabled to prevent interference from the "noisy" data
// bus to the voice circuits.
m_voice_data_enabled = !state;
}
template <int Voice> void linndrum_audio_device::strobe_mux_drum_w(u8 data)
{
static_assert(Voice >= 0 && Voice < NUM_MUX_VOICES);
data = get_voice_data(data);
m_mux_counting[Voice] = BIT(data, 0);
if (!m_mux_counting[Voice])
m_mux_counters[Voice] = 0;
set_mux_drum_volume(Voice, BIT(data, 1));
LOGMASKED(LOG_STROBES, "Strobed mux drum %s, %02x\n", MUX_VOICE_NAMES[Voice], data);
}
void linndrum_audio_device::strobe_snare_w(u8 data)
{
data = get_voice_data(data);
m_snare_counting = BIT(data, 0);
if (!m_snare_counting)
m_snare_counter = 0;
m_sidestick_selected = BIT(data, 1); // Play sidestick instead of snare.
set_snare_volume(BIT(data, 3), BIT(data, 2));
LOGMASKED(LOG_STROBES, "Strobed snare / sidestick: %02x\n", data);
}
void linndrum_audio_device::strobe_tom_w(u8 data)
{
data = get_voice_data(data);
m_tom_counting = BIT(data, 0);
if (!m_tom_counting)
m_tom_counter = 0;
m_tom_selected = BIT(data, 1); // Play tom instead of conga.
m_tom_variation = (data >> 2) & 0x03; // D3, D2.
LOGMASKED(LOG_STROBES, "Strobed tom / conga: %02x\n", data);
}
void linndrum_audio_device::strobe_click_w(u8 /*data*/)
{
static constexpr const float R10 = RES_K(100);
static constexpr const float C12 = CAP_U(0.01);
m_click_timer->adjust(PERIOD_OF_555_MONOSTABLE(R10, C12));
m_click->level_w(1);
LOGMASKED(LOG_STROBES, "Strobed click.\n");
}
void linndrum_audio_device::beep_w(int state)
{
// Beep signal is inverted by U76B (74LS00).
m_beep->level_w(state ? 0 : 1);
LOGMASKED(LOG_STROBES, "Beep: %d\n", state);
}
void linndrum_audio_device::device_add_mconfig(machine_config &config)
{
// Sample rate is controlled by 74LS627 timers. There isn't a formula that
// computes frequency for these chips. The datasheet just provides graphs.
// The values below were obtained by eyballing those graphs, and then
// adjusting by ear to youtube videos, while sticking to round numbers.
// TODO: Implement control by potentiomenters.
static constexpr const int MUX_DRUM_SAMPLE_RATE = 30000;
static constexpr const int SNARE_SAMPLE_RATE = 28000;
static constexpr const int TOM_SAMPLE_RATE = 32000;
// *** Mux drums section.
TIMER(config, "mux_drum_timer").configure_periodic( // 74LS627 (U77A).
FUNC(linndrum_audio_device::mux_timer_tick), attotime::from_hz(MUX_DRUM_SAMPLE_RATE));
// The actual "mux drums" hardware has a single AM6070, which is
// time-multiplexed across the 8 voices. Implementing it that way is
// possible, but requires a sample rate of at least 240KHz (8 x ~30K) for
// reasonable results. It also requires emulating audio sample & hold
// functionality. So 8 "virtual" DACs and volume-control MUXes are used
// instead.
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 set_mux_drum_volume()).
m_mux_dac[voice]->add_route(ALL_OUTPUTS, m_mux_volume[voice], 1.0);
}
// *** Snare / sidestick section.
TIMER(config, "snare_timer").configure_periodic( // 74LS627 (U80A).
FUNC(linndrum_audio_device::snare_timer_tick), attotime::from_hz(SNARE_SAMPLE_RATE));
DAC76(config, m_snare_dac, 0); // AM6070 (U92)
FILTER_VOLUME(config, m_snare_volume); // See set_snare_volume().
m_snare_dac->add_route(ALL_OUTPUTS, m_snare_volume, 1.0);
// 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.
filter_rc_device &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.
m_snare_volume->add_route(ALL_OUTPUTS, snare_dac_filter, 1.0);
TIMER(config, m_click_timer).configure_generic(FUNC(linndrum_audio_device::click_timer_tick)); // 556 (U65A).
SPEAKER_SOUND(config, m_click);
SPEAKER_SOUND(config, m_beep);
// *** Tom / conga section.
TIMER(config, m_tom_timer).configure_periodic( // 74LS627 (U77B).
FUNC(linndrum_audio_device::tom_timer_tick), attotime::from_hz(TOM_SAMPLE_RATE));
DAC76(config, m_tom_dac, 0); // AM6070 (U82).
// *** Output.
// TODO: Implement mixing and panning.
speaker_device &speaker = SPEAKER(config, "monospeaker").front_center();
for (int i = 0; i < NUM_MUX_VOICES; ++i)
m_mux_volume[i]->add_route(ALL_OUTPUTS, speaker, 1.0);
snare_dac_filter.add_route(ALL_OUTPUTS, speaker, 1.0);
m_click->add_route(ALL_OUTPUTS, speaker, 1.0);
m_beep->add_route(ALL_OUTPUTS, speaker, 1.0);
m_tom_dac->add_route(ALL_OUTPUTS, speaker, 1.0);
}
void linndrum_audio_device::device_start()
{
save_item(NAME(m_mux_counting));
save_item(NAME(m_mux_counters));
save_item(NAME(m_snare_counting));
save_item(NAME(m_snare_counter));
save_item(NAME(m_sidestick_selected));
save_item(NAME(m_tom_counting));
save_item(NAME(m_tom_counter));
save_item(NAME(m_tom_selected));
save_item(NAME(m_tom_variation));
save_item(NAME(m_voice_data_enabled));
}
void linndrum_audio_device::write_dac(dac76_device& dac, u8 sample)
{
dac.update();
dac.sb_w(BIT(sample, 7));
dac.b1_w(BIT(sample, 6));
dac.b2_w(BIT(sample, 5));
dac.b3_w(BIT(sample, 4));
dac.b4_w(BIT(sample, 3));
dac.b5_w(BIT(sample, 2));
dac.b6_w(BIT(sample, 1));
dac.b7_w(BIT(sample, 0));
}
void linndrum_audio_device::set_mux_drum_volume(int voice, bool d1)
{
// Volume variations are controlled by a CD4053 MUX (U90B), whose "select"
// input is connected to the active voice's D1 (via a 74LS151 encoder, U35).
// Depending on how the mux is configured, the audio signal will either
// remain unchanged, or it will get attenuated by a voltage divider.
// The MUX is always configured in the "no attenuation" mode for the clap
// and cowbell voices.
static constexpr const float ATTENUATION = RES_VOLTAGE_DIVIDER(RES_K(10), RES_K(3.3)); // R60, R62.
const bool attenuate = !d1 && voice != CLAP && voice != COWBELL;
assert(voice >= 0 && voice < NUM_MUX_VOICES);
m_mux_volume[voice]->set_gain(attenuate ? ATTENUATION : 1.0);
LOGMASKED(LOG_VOLUME, "Mux drum %s gain: %f\n", MUX_VOICE_NAMES[voice], m_mux_volume[voice]->gain());
}
void linndrum_audio_device::set_snare_volume(bool d3, bool d2)
{
// Snare and sidestick volume is set by controlling the reference current to
// the DAC. Bits D2 and D3 from the voice data bus (latched by U42, 74LS74)
// control the voltage at the end of R72 and R71.
static constexpr const float R0 = RES_K(3.3); // R70.
static constexpr const float R1 = RES_K(380); // R69.
static constexpr const float R2 = RES_K(22); // R72.
static constexpr const float R3 = RES_K(5.6); // R71.
static constexpr const float a = R0 * R2 * R3;
static constexpr const float b = R0 * R1 * R3;
static constexpr const float c = R0 * R1 * R2;
static constexpr const float d = R1 * R2 * R3;
static constexpr const float MAX_IREF = (a * VPLUS + b * VCC + c * VCC) / (R0 * (a + b + c + d));
const float v2 = d2 ? VCC : 0;
const float v3 = d3 ? VCC : 0;
const float iref = (a * VPLUS + b * v2 + c * v3) / (R0 * (a + b + c + d));
m_snare_volume->set_gain(iref / MAX_IREF);
LOGMASKED(LOG_VOLUME, "Snare volume - iref: %f, max_iref: %f, gain: %f\n",
iref, MAX_IREF, iref / MAX_IREF);
}
u8 linndrum_audio_device::get_voice_data(u8 data) const
{
if (m_voice_data_enabled)
{
return data & 0x0f; // Voice data bus is 4 bits wide.
}
else
{
LOG("Firmware bug: floating voice data bus when strobing a voice.\n");
return 0x0f; // Floating TTL inputs. Will likely resolve to 1s.
}
}
TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::mux_timer_tick)
{
// The timer on the actual hardware ticks 4 times per voice. A combination
// of counters, latches, decoders, encoders and analog multiplexers achieves
// the following:
// Tick 0: Selects next voice and enables DAC.
// - ROM address counter is incremented, if voice is enabled.
// If the counter reaches its max, the voice will be disabled and
// the counter will get cleared.
// - ROM is enabled, and outputs are written to the DAC.
// - Volume variation is configured (for voices that support it).
// - If the voice is not enabled, the ROM address will be 0, which
// according to the service manual always stores a 0.
// Tick 1: No-op. Waits for DAC to settle.
// Tick 2: Sample & Hold (S&H) for the selected voice is enabled.
// Tick 3: S&H is disabled. DAC is disabled, driving its output to 0.
// The emulation here does all of the above, for all voices, in a single
// timer tick. The timer period has been adjusted accordingly.
for (int voice = 0; voice < NUM_MUX_VOICES; ++voice)
{
if (m_mux_counting[voice])
{
++m_mux_counters[voice];
if (m_mux_counters[voice] >= m_mux_samples[voice]->bytes())
{
// All bits in the voice's D0 and D1 latch (74LS74) are cleared,
// resulting in:
m_mux_counting[voice] = false;
m_mux_counters[voice] = 0;
set_mux_drum_volume(voice, false);
}
}
const u8 sample = m_mux_samples[voice]->as_u8(m_mux_counters[voice]);
write_dac(*m_mux_dac[voice], sample);
}
}
TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::snare_timer_tick)
{
if (!m_snare_counting)
return;
++m_snare_counter;
if (BIT(m_snare_counter, 12)) // Counter reached 0x1000 (4096).
{
// All outputs of U41 and U42 (74LS74 flip-flops) are cleared, resulting in:
m_snare_counting = false;
m_snare_counter = 0;
m_sidestick_selected = false;
set_snare_volume(false, false);
write_dac(*m_snare_dac, 0); // DAC is disabled. Output goes to 0.
return;
}
u8 sample = 0;
if (m_sidestick_selected)
sample = m_sidestick_samples->as_u8(m_snare_counter);
else
sample = m_snare_samples->as_u8(m_snare_counter);
write_dac(*m_snare_dac, sample);
}
TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::click_timer_tick)
{
m_click->level_w(0);
}
TIMER_DEVICE_CALLBACK_MEMBER(linndrum_audio_device::tom_timer_tick)
{
if (!m_tom_counting)
return;
++m_tom_counter;
if (BIT(m_tom_counter, 13)) // Counter reached 0x2000 (8192).
{
// All outputs of U42B and U73B (74LS74 flip-flops) are cleared, resulting in:
m_tom_counting = false;
m_tom_counter = 0;
m_tom_selected = false;
m_tom_variation = 0;
write_dac(*m_tom_dac, 0); // DAC is disabled. Output goes to 0.
return;
}
u8 sample = 0;
if (m_tom_selected)
sample = m_tom_samples->as_u8(m_tom_counter);
else
sample = m_conga_samples->as_u8(m_tom_counter);
write_dac(*m_tom_dac, sample);
}
namespace {
constexpr const char MAINCPU_TAG[] = "z80";
constexpr const char NVRAM_TAG[] = "nvram";
class linndrum_state : public driver_device
{
public:
static constexpr feature_type unemulated_features() { return feature::TAPE; }
linndrum_state(const machine_config &mconfig, device_type type, const char *tag) ATTR_COLD
: driver_device(mconfig, type, tag)
, m_maincpu(*this, MAINCPU_TAG)
, m_audio(*this, "linndrum_audio")
, m_tempo_timer(*this, "tempo_timer_556_u30a")
, m_debounce_timer(*this, "debounce_timer_556_u30b")
, m_keyboard(*this, "keyboard_col_%d", 0)
@ -98,7 +575,7 @@ protected:
private:
u8 keyboard_r(offs_t offset);
u8 inport_r();
template<int DISPLAY> void display_w(u8 data);
template<int Display> void display_w(u8 data);
u8 start_debounce_r();
void start_debounce_w(u8 data);
@ -107,9 +584,6 @@ private:
void update_tempo_timer();
TIMER_DEVICE_CALLBACK_MEMBER(tempo_timer_tick);
void trigger_w(offs_t offset, u8 data);
void trigger_beep_w(int state);
void data_out_enable_w(int state);
void tape_sync_enable_w(int state);
void update_tape_sync_out();
@ -117,6 +591,7 @@ private:
void io_map(address_map &map) ATTR_COLD;
required_device<z80_device> m_maincpu;
required_device<linndrum_audio_device> m_audio;
required_device<timer_device> m_tempo_timer; // 556, U30A.
required_device<timer_device> m_debounce_timer; // 556, U30B.
required_ioport_array<6> m_keyboard;
@ -130,14 +605,6 @@ private:
bool m_debouncing = false;
bool m_tempo_state = false;
bool m_tape_sync_enabled = false;
bool m_data_out_enabled = false;
static constexpr const int NUM_VOICE_TRIGGERS = 11;
static constexpr const char *VOICE_TRIGGER_NAMES[NUM_VOICE_TRIGGERS] =
{
"BASS", "SNARE", "HI-HAT", "TOM / CGA", "RIDE", "CRASH", "CABASA",
"TAMB", "COWBELL", "CLAP", "CLICK"
};
static constexpr const int DISPLAY_STEP = 0;
static constexpr const int DISPLAY_PATTERN = 1;
@ -189,7 +656,7 @@ u8 linndrum_state::inport_r()
return (triggers << 3) | (d2 << 2) | (d1 << 1) | d0;
}
template<int DISPLAY> void linndrum_state::display_w(u8 data)
template<int Display> void linndrum_state::display_w(u8 data)
{
static constexpr const u8 PATTERNS[16] = // 4 x 74LS47 (U24-U27).
{
@ -200,8 +667,8 @@ template<int DISPLAY> void linndrum_state::display_w(u8 data)
const u8 ms_digit = PATTERNS[(data >> 4) & 0x0f];
const u8 ls_digit = PATTERNS[data & 0x0f];
static_assert(DISPLAY == DISPLAY_STEP || DISPLAY == DISPLAY_PATTERN);
if (DISPLAY == DISPLAY_STEP)
static_assert(Display == DISPLAY_STEP || Display == DISPLAY_PATTERN);
if (Display == DISPLAY_STEP)
{
m_step_display[0] = ms_digit;
m_step_display[1] = ls_digit;
@ -246,7 +713,7 @@ void linndrum_state::update_tempo_timer()
// Using `100 - pot value` because the higher (the more clockwise) the pot
// is turned, the lower the resistance and the fastest the tempo.
const float tempo_r = (100 - m_tempo_pot->read()) * P1_MAX / 100.0f;
const float tempo_r = (100 - m_tempo_pot->read()) * P1_MAX / 100.0F;
const attotime period = PERIOD_OF_555_ASTABLE(R1, R2 + tempo_r, C2);
m_tempo_timer->adjust(period, 0, period);
LOGMASKED(LOG_TEMPO_CHANGE, "Tempo adjusted: %f\n", period.as_double());
@ -262,30 +729,6 @@ TIMER_DEVICE_CALLBACK_MEMBER(linndrum_state::tempo_timer_tick)
LOGMASKED(LOG_TEMPO, "Tempo timer elapsed: %d\n", m_tempo_state);
}
void linndrum_state::trigger_w(offs_t offset, u8 data)
{
assert(offset >= 0 && offset < NUM_VOICE_TRIGGERS);
LOGMASKED(LOG_TRIGGERS, "Trigger %s (%02x), data: %02x, data enabled: %d\n",
VOICE_TRIGGER_NAMES[offset], offset, data, m_data_out_enabled);
// TODO: Implement.
}
void linndrum_state::trigger_beep_w(int state)
{
LOGMASKED(LOG_TRIGGERS, "Trigger BEEP: %d, data enabled: %d\n",
state, m_data_out_enabled);
// TODO: Implement.
}
void linndrum_state::data_out_enable_w(int state)
{
// Controls whether data (D0-D3) is transmitted to the voice circuits. This
// is done by U19 (74LS365 hex buffer. Enable inputs are active-low).
// This is usually disabled to prevent interference from the "noisy" data
// bus to the voice circuits.
m_data_out_enabled = !state;
}
void linndrum_state::tape_sync_enable_w(int state)
{
LOGMASKED(LOG_TAPE_SYNC_ENABLE, "Tape sync enable: %d\n", state);
@ -320,7 +763,19 @@ void linndrum_state::memory_map(address_map &map)
map(0x1f82, 0x1f82).mirror(0x0030).w("latch_u18", FUNC(output_latch_device::write)); // LEDs.
map(0x1f83, 0x1f83).mirror(0x0030).w("latch_u17", FUNC(output_latch_device::write)); // LEDs.
map(0x1f84, 0x1f84).mirror(0x0030).w("latch_u16", FUNC(output_latch_device::write)); // LEDs & outputs.
map(0x1f85, 0x1f8f).mirror(0x0030).w(FUNC(linndrum_state::trigger_w)); // Data out.
// Voice strobes.
map(0x1f85, 0x1f85).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<BASS>));
map(0x1f86, 0x1f86).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_snare_w));
map(0x1f87, 0x1f87).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<HAT>));
map(0x1f88, 0x1f88).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_tom_w));
map(0x1f89, 0x1f89).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<RIDE>));
map(0x1f8a, 0x1f8a).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<CRASH>));
map(0x1f8b, 0x1f8b).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<CABASA>));
map(0x1f8c, 0x1f8c).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<TAMBOURINE>));
map(0x1f8d, 0x1f8d).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<COWBELL>));
map(0x1f8e, 0x1f8e).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_mux_drum_w<CLAP>));
map(0x1f8f, 0x1f8f).mirror(0x0030).w(m_audio, FUNC(linndrum_audio_device::strobe_click_w));
map(0x1fc0, 0x1fff).r(FUNC(linndrum_state::keyboard_r)); // /READ KEYBD.
map(0x2000, 0x3fff).ram().share(NVRAM_TAG); // 4 x HM6116LP4. U12-U9.
@ -345,7 +800,6 @@ void linndrum_state::machine_start()
save_item(NAME(m_debouncing));
save_item(NAME(m_tempo_state));
save_item(NAME(m_tape_sync_enabled));
save_item(NAME(m_data_out_enabled));
}
void linndrum_state::machine_reset()
@ -367,6 +821,8 @@ void linndrum_state::linndrum(machine_config &config)
TIMER(config, m_debounce_timer).configure_generic( // 556, U30B.
FUNC(linndrum_state::debounce_timer_elapsed));
LINNDRUM_AUDIO(config, m_audio);
config.set_default_layout(layout_linn_linndrum);
// Latches connected to cathodes of LEDs (through resistors), so they are
@ -397,8 +853,8 @@ void linndrum_state::linndrum(machine_config &config)
u16.bit_handler<1>().set_output("led_ext_sync").invert();
u16.bit_handler<2>().set_output("led_pattern").invert();
u16.bit_handler<2>().append_output("led_song"); // Inverted by U31A.
u16.bit_handler<3>().set(FUNC(linndrum_state::data_out_enable_w));
u16.bit_handler<4>().set(FUNC(linndrum_state::trigger_beep_w));
u16.bit_handler<3>().set(m_audio, FUNC(linndrum_audio_device::voice_data_enable_w));
u16.bit_handler<4>().set(m_audio, FUNC(linndrum_audio_device::beep_w));
u16.bit_handler<5>().set(FUNC(linndrum_state::tape_sync_enable_w));
// Output voltage divided by R24/R25 to 0V - 2.5V.
u16.bit_handler<6>().set_output("output_cassette");
@ -433,8 +889,8 @@ INPUT_PORTS_START(linndrum)
PORT_BIT(0x01, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("<-") PORT_CODE(KEYCODE_Y)
PORT_BIT(0x02, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("->") PORT_CODE(KEYCODE_U)
PORT_BIT(0x04, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("ENTER") PORT_CODE(KEYCODE_I)
PORT_BIT(0x08, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("STORE") PORT_CODE(KEYCODE_P)
PORT_BIT(0x10, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("LOAD") PORT_CODE(KEYCODE_COLON)
PORT_BIT(0x08, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("STORE")
PORT_BIT(0x10, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("LOAD")
PORT_BIT(0x20, IP_ACTIVE_LOW, IPT_OTHER) PORT_NAME("EXT.SYNC") PORT_CODE(KEYCODE_L)
PORT_START("keyboard_col_3")
@ -481,8 +937,7 @@ INPUT_PORTS_START(linndrum)
PORT_BIT(0x10, IP_ACTIVE_HIGH, IPT_OTHER) PORT_NAME("TRIGGER 1")
PORT_START("pot_tempo")
PORT_ADJUSTER(50, "TEMPO")
PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(linndrum_state::tempo_pot_adjusted), 0)
PORT_ADJUSTER(50, "TEMPO") PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(linndrum_state::tempo_pot_adjusted), 0)
PORT_START("pot_volume")
PORT_ADJUSTER(100, "MASTER VOLUME")
@ -573,8 +1028,69 @@ ROM_START(linndrum)
ROM_REGION(0x2000, MAINCPU_TAG, 0)
ROM_LOAD("ld_2.1_a.u14", 0x000000, 0x001000, CRC(566d720e) SHA1(91b7a515e3d18a28b7f5428765ed79114a5a00fb))
ROM_LOAD("ld_2.1_b.u13", 0x001000, 0x001000, CRC(9c9a5520) SHA1(6e4573051254051c75f9071fd8dfcd5a9184f9cc))
// All sample ROMs are 2732.
// ROM file name format: sticker_label.silscreen_label.component_designation
ROM_REGION(0x1000, "sample_mux_drum_0", 0) // Tambourine.
ROM_LOAD("tamb1.tamb.u25", 0x000000, 0x001000, CRC(0309eba3) SHA1(89a1910b5224a1db91c31100cfe81ebd36610027))
ROM_REGION(0x1000, "sample_mux_drum_1", 0) // Cabasa.
ROM_LOAD("cbsa.u26", 0x000000, 0x001000, NO_DUMP)
ROM_FILL(0x000000, 0x001000, 0x00) // Silence. Remove if checksum becomes available.
ROM_REGION(0x1000, "sample_mux_drum_2", 0) // Claps.
ROM_LOAD("clps.u27", 0x000000, 0x001000, NO_DUMP)
ROM_FILL(0x000000, 0x001000, 0x00) // Silence. Remove if checksum becomes available.
ROM_REGION(0x1000, "sample_mux_drum_3", 0) // Cowbell.
ROM_LOAD("cwbl1.cwbl.u28", 0x000000, 0x001000, CRC(819d4a2c) SHA1(04d6fb88dd8751336617e50ce840fa63e6002942))
ROM_REGION(0x1000, "sample_mux_drum_4", 0) // Bass.
ROM_LOAD("bass.u29", 0x000000, 0x001000, NO_DUMP)
ROM_FILL(0x000000, 0x001000, 0x00) // Silence. Remove if checksum becomes available.
ROM_REGION(0x4000, "sample_mux_drum_5", 0) // Hi hat.
ROM_LOAD("hat1a.hat1.u30", 0x000000, 0x001000, CRC(20b35416) SHA1(9aed28369b9b3f4a088c3f2ee9c88ed4b029a3ae))
ROM_LOAD("hat1b.hat2.u31", 0x001000, 0x001000, CRC(fb4b3bac) SHA1(99a6cada1e741a7294b9aa08df36489644253acb))
ROM_LOAD("hat1c.hat3.u32", 0x002000, 0x001000, CRC(62d1e667) SHA1(516df46a6e1050151f4b1696568a4ffbde0eba7c))
ROM_LOAD("hat1d.hat4.u33", 0x003000, 0x001000, CRC(01819ab1) SHA1(b0cfece5568375340eab16f8523ddb8599013310))
ROM_REGION(0x8000, "sample_mux_drum_6", 0) // Ride cymbal.
ROM_LOAD("ride1a.rid1.u9", 0x000000, 0x001000, CRC(3d0a852f) SHA1(58ea6cda2ad1a8b6506ad89ba3f5c47584013ef0))
ROM_LOAD("ride1b.rid2.u10", 0x001000, 0x001000, CRC(5bb0e082) SHA1(6f4535d08ac013d804cc2d9fd9fedca2b8515c99))
ROM_LOAD("ride1c.rid3.u8", 0x002000, 0x001000, CRC(fa48f0e3) SHA1(a455b6d8d8dde9a7903919c87263b392f0510c8a))
ROM_LOAD("ride1d.rid4.u7", 0x003000, 0x001000, CRC(3a8b4133) SHA1(f0a2f7a0db1024e7e263000f74d127f53bf6df94))
ROM_LOAD("ride1e.rid5.u6", 0x004000, 0x001000, CRC(e30dc9a2) SHA1(933d2fa23a371831e779315f61e9a8f281ffab7c))
ROM_LOAD("ride1f.rid6.u5", 0x005000, 0x001000, CRC(ba63cc66) SHA1(ba4f2fd13d89c37eab30301b96338c6a9a69ad9d))
ROM_LOAD("ride1g.rid7.u4", 0x006000, 0x001000, CRC(d5e24b3e) SHA1(fba0fa24e8afd7686b480ace1838587fe2a6691c))
ROM_LOAD("ride1h.rid8.u3", 0x007000, 0x001000, CRC(4ac922bb) SHA1(f86b3fbd079ef59b5e260250b95ec75829a1c31d))
ROM_REGION(0x8000, "sample_mux_drum_7", 0) // Crash cymbal.
ROM_LOAD("crsh1a.crs1.u14", 0x000000, 0x001000, CRC(85ce8dc5) SHA1(a1fb4064f7d02df21ef898dab1bd4df9754f2420))
ROM_LOAD("crsh1b.crs2.u13", 0x001000, 0x001000, CRC(3681d6f0) SHA1(1ad7e202eb9a82af03949bcf3ba281932bf83f5b))
ROM_LOAD("crsh1c.crs3.u12", 0x002000, 0x001000, CRC(ff1a4d87) SHA1(6e10d141f9a8dbbf951d72fbdeda4e8386fd5bc8))
ROM_LOAD("crsh1d.crs4.u15", 0x003000, 0x001000, CRC(7f623944) SHA1(03c1294df1e057442aacdb004d3ebd8e94628b15))
ROM_LOAD("crsh1e.crs5.u16", 0x004000, 0x001000, CRC(851c306a) SHA1(4f41a31e2e8273df7664a65c5e5a3528b312627a))
ROM_LOAD("crsh1f.crs6.u17", 0x005000, 0x001000, CRC(2fee35fe) SHA1(53b68ffe940beee4dcf568af0ed129a02a2948b9))
ROM_LOAD("crsh1g.crs7.u18", 0x006000, 0x001000, CRC(53f939bb) SHA1(980368e122793d1318e3643b87e06e053690f0de))
ROM_LOAD("crsh1h.crs8.u11", 0x007000, 0x001000, CRC(7b9f55c2) SHA1(dd0eb89ad89e56a28d8dccb78a979acb954968a1))
ROM_REGION(0x1000, "sample_sidestick", 0)
ROM_LOAD("sstk1.sstk.u78", 0x000000, 0x001000, CRC(61af39e3) SHA1(5648674854a8db80656bf729c4f353b75d101d7b))
ROM_REGION(0x1000, "sample_snare", 0)
ROM_LOAD("snar9.snar.u79", 0x000000, 0x001000, CRC(83478583) SHA1(bac791208270eff1f2f362511d6418873c47827c))
ROM_REGION(0x2000, "sample_conga", 0)
ROM_LOAD("cnga1a.cga1.u66", 0x000000, 0x001000, CRC(1a579539) SHA1(169741786c44026f2b6ef4052cccb2a27ba41e19))
ROM_LOAD("cnga1b.cga2.u67", 0x001000, 0x001000, CRC(02434d69) SHA1(451398fbf9ac94a1773f4f40ef4ca32d3c857537))
ROM_REGION(0x2000, "sample_tom", 0)
ROM_LOAD("tom6a.tom1.u68", 0x000000, 0x001000, CRC(75f83e43) SHA1(386aa53311e6f8cea56e8021b19855a5ba586f52))
ROM_LOAD("tom6b.tom2.u69", 0x001000, 0x001000, CRC(c7633ca4) SHA1(60ab77bf21897b55cc8d2844ce1cc0c65958c939))
ROM_END
} // anonymous namespace
SYST(1982, linndrum, 0, 0, linndrum, linndrum, linndrum_state, empty_init, "Linn Electronics", "LinnDrum", MACHINE_SUPPORTS_SAVE | MACHINE_NOT_WORKING | MACHINE_NO_SOUND)
SYST(1982, linndrum, 0, 0, linndrum, linndrum, linndrum_state, empty_init, "Linn Electronics", "LinnDrum", MACHINE_SUPPORTS_SAVE | MACHINE_IMPERFECT_SOUND)