summaryrefslogtreecommitdiffstatshomepage
path: root/src/devices/sound/upd931.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/devices/sound/upd931.cpp')
-rw-r--r--src/devices/sound/upd931.cpp599
1 files changed, 599 insertions, 0 deletions
diff --git a/src/devices/sound/upd931.cpp b/src/devices/sound/upd931.cpp
new file mode 100644
index 00000000000..c5c1e1c22b4
--- /dev/null
+++ b/src/devices/sound/upd931.cpp
@@ -0,0 +1,599 @@
+// license:BSD-3-Clause
+// copyright-holders:Devin Acker
+
+/***************************************************************************
+ NEC/Casio uPD931 synthesis chip
+
+ Many details of this implementation are based on research and notes by Robin Whittle:
+ https://www.firstpr.com.au/rwi/casio/Casio-931-2006-06-17.txt
+ Any references to MT-65 behavior are based on this document.
+
+ TODO:
+ - implement vibrato register (CT-8000 doesn't use it)
+ - a few other unknown/unclear bits in the flags shift register
+***************************************************************************/
+
+#include "emu.h"
+#include "upd931.h"
+
+#include <algorithm>
+#include <cmath>
+
+DEFINE_DEVICE_TYPE(UPD931, upd931_device, "upd931", "NEC uPD931")
+
+upd931_device::upd931_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
+ : device_t(mconfig, UPD931, tag, owner, clock)
+ , device_sound_interface(mconfig, *this)
+ , device_memory_interface(mconfig, *this)
+ , m_io_config("io", ENDIANNESS_LITTLE, 8, 8, 0, address_map_constructor(FUNC(upd931_device::io_map), this))
+ , m_retrig_timer(nullptr)
+ , m_filter_cb(*this)
+ , m_sync_cb(*this)
+ , m_master(true)
+{
+}
+
+/**************************************************************************/
+device_memory_interface::space_config_vector upd931_device::memory_space_config() const
+{
+ return space_config_vector{std::make_pair(AS_IO, &m_io_config)};
+}
+
+/**************************************************************************/
+void upd931_device::io_map(address_map &map)
+{
+ map(0x20, 0x27).w(FUNC(upd931_device::note_w));
+ map(0x30, 0x37).w(FUNC(upd931_device::octave_w));
+ // waveform write position: ct8000 uses 40, mt65 uses 60
+ map(0x40, 0x40).mirror(0x20).w(FUNC(upd931_device::wave_pos_w));
+ map(0xa7, 0xa7).w(FUNC(upd931_device::wave_data_w));
+ map(0xb0, 0xb7).w(FUNC(upd931_device::flags_w)); // lower address bits are ignored
+ map(0xb8, 0xbf).w(FUNC(upd931_device::status_latch_w));
+ // vibrato/sustain: ct8000 uses c0-c7, mt65 uses d0-d7 (lower two bits are also ignored)
+ map(0xc0, 0xc3).mirror(0x10).w(FUNC(upd931_device::vibrato_w));
+ map(0xc4, 0xc7).mirror(0x10).w(FUNC(upd931_device::sustain_w));
+ map(0xe0, 0xe7).w(FUNC(upd931_device::note_on_w));
+ map(0xf4, 0xf4).lw8(NAME([this] (offs_t offset, u8 data) { m_filter_cb(data); }));
+}
+
+/**************************************************************************/
+void upd931_device::device_start()
+{
+ space(AS_IO).specific(m_io);
+
+ m_stream = stream_alloc(0, 1, clock() / CLOCKS_PER_SAMPLE);
+
+ if (m_master)
+ {
+ m_retrig_timer = timer_alloc(FUNC(upd931_device::timer_tick), this);
+ reset_timer();
+ }
+ m_last_clock = clock();
+
+ m_db = 0xf;
+ m_i1 = m_i2 = m_i3 = 1;
+ m_status = 0xf;
+
+ // generate note frequencies based on CT-8000 crystal frequency (probably not entirely accurate)
+ const unsigned sample_rate = 4'946'864 / CLOCKS_PER_SAMPLE;
+ for (int i = 0; i < 73; i++)
+ {
+ // A4 is note 33, 442 Hz
+ const double freq = 442.0 * pow(2, (i - 33) / 12.0);
+ m_pitch[i] = (1 << PITCH_SHIFT) * (freq * 16 / sample_rate);
+ }
+
+ save_item(NAME(m_db));
+ save_item(NAME(m_i1));
+ save_item(NAME(m_i2));
+ save_item(NAME(m_i3));
+
+ save_item(NAME(m_addr));
+ save_item(NAME(m_data));
+ save_item(NAME(m_status));
+
+ save_item(NAME(m_wave));
+ save_item(NAME(m_wave_pos));
+ save_item(NAME(m_vibrato));
+ save_item(NAME(m_sustain));
+ save_item(NAME(m_reverb));
+
+ save_item(NAME(m_flags));
+ save_item(NAME(m_last_clock));
+
+ save_item(STRUCT_MEMBER(m_voice, m_note));
+ save_item(STRUCT_MEMBER(m_voice, m_octave));
+ save_item(STRUCT_MEMBER(m_voice, m_pitch));
+ save_item(STRUCT_MEMBER(m_voice, m_pitch_counter));
+ save_item(STRUCT_MEMBER(m_voice, m_timbre_shift));
+
+ save_item(STRUCT_MEMBER(m_voice, m_wave_pos));
+ save_item(STRUCT_MEMBER(m_voice, m_wave_out));
+
+ save_item(STRUCT_MEMBER(m_voice, m_env_state));
+ save_item(STRUCT_MEMBER(m_voice, m_env_counter));
+ save_item(STRUCT_MEMBER(m_voice, m_env_level));
+ save_item(STRUCT_MEMBER(m_voice, m_force_release));
+}
+
+/**************************************************************************/
+TIMER_CALLBACK_MEMBER(upd931_device::timer_tick)
+{
+ m_sync_cb(1);
+ sync_w(1);
+}
+
+/**************************************************************************/
+void upd931_device::device_reset()
+{
+ std::fill(std::begin(m_wave[0]), std::end(m_wave[0]), 0);
+ std::fill(std::begin(m_wave[1]), std::end(m_wave[1]), 0);
+ m_wave_pos = 0;
+ m_vibrato = 0;
+ m_sustain = 0;
+ m_reverb = 0;
+
+ m_flags = 0;
+
+ std::fill(m_voice.begin(), m_voice.end(), voice_t());
+
+ m_filter_cb(0);
+ m_sync_cb(0);
+}
+
+/**************************************************************************/
+void upd931_device::device_clock_changed()
+{
+ m_stream->set_sample_rate(clock() / CLOCKS_PER_SAMPLE);
+
+ if (m_retrig_timer)
+ {
+ const u64 ticks_left = m_retrig_timer->remaining().as_ticks(m_last_clock);
+ const attotime remaining = attotime::from_ticks(ticks_left, clock());
+ const attotime period = attotime::from_ticks(RETRIG_RATE, clock());
+
+ m_retrig_timer->adjust(remaining, 0, period);
+ }
+
+ m_last_clock = clock();
+}
+
+/**************************************************************************/
+void upd931_device::sound_stream_update(sound_stream &stream, std::vector<read_stream_view> const &inputs, std::vector<write_stream_view> &outputs)
+{
+ for (int i = 0; i < outputs[0].samples(); i++)
+ {
+ s32 sample = 0;
+ for (voice_t &voice : m_voice)
+ {
+ update_env(voice);
+ update_wave(voice);
+
+ sample += voice.m_wave_out[0] * (voice.m_env_level[0] >> VOLUME_SHIFT);
+ sample += voice.m_wave_out[1] * (voice.m_env_level[1] >> VOLUME_SHIFT);
+ }
+ outputs[0].put_int_clamp(i, sample, 1 << 16);
+ }
+}
+
+/**************************************************************************/
+void upd931_device::i1_w(int state)
+{
+ if (!m_i3 && m_i1 && !state)
+ {
+ // address high nibble on I1 falling edge
+ m_addr &= 0x0f;
+ m_addr |= (m_db << 4);
+ }
+
+ m_i1 = state;
+}
+
+/**************************************************************************/
+void upd931_device::i2_w(int state)
+{
+ if (!m_i3)
+ {
+ if (!m_i2 && state)
+ {
+ // address low nibble on I2 rising edge
+ m_addr &= 0xf0;
+ m_addr |= (m_db & 0xf);
+
+ // apply register write
+ m_stream->update();
+ m_io.write_byte(m_addr, m_data & 0xf);
+ }
+ else if (m_i2 && !state)
+ {
+ // data on I2 falling edge
+ m_data = m_db;
+ }
+ }
+
+ m_i2 = state;
+}
+
+/**************************************************************************/
+void upd931_device::i3_w(int state)
+{
+ m_i3 = state;
+}
+
+/**************************************************************************/
+void upd931_device::db_w(u8 data)
+{
+ m_db = data;
+}
+
+/**************************************************************************/
+u8 upd931_device::db_r()
+{
+ if (m_i1 && m_i2 && !m_i3)
+ return m_status;
+ return 0xf;
+}
+
+/**************************************************************************/
+void upd931_device::sync_w(int state)
+{
+ if (BIT(m_flags, FLAG_RETRIGGER))
+ {
+ m_stream->update();
+
+ for (voice_t &voice : m_voice)
+ {
+ if (voice.m_env_state == ENV_DECAY1 || voice.m_env_state == ENV_DECAY2)
+ {
+ voice.m_env_state = ENV_ATTACK1;
+ voice.m_env_counter = 0;
+ }
+ }
+ }
+}
+
+/**************************************************************************/
+void upd931_device::note_w(offs_t offset, u8 data)
+{
+ m_voice[offset].m_note = data;
+}
+
+/**************************************************************************/
+void upd931_device::octave_w(offs_t offset, u8 data)
+{
+ m_voice[offset].m_octave = m_data;
+}
+
+/**************************************************************************/
+void upd931_device::wave_pos_w(u8 data)
+{
+ m_wave_pos = data;
+}
+
+/**************************************************************************/
+void upd931_device::wave_data_w(u8 data)
+{
+ const u8 sel = BIT(m_flags, FLAG_WAVE_SEL);
+ m_wave[sel][m_wave_pos & 0xf] = data;
+}
+
+/**************************************************************************/
+void upd931_device::flags_w(u8 data)
+{
+ m_flags >>= 1;
+ m_flags |= ((data & 1) << FLAG_WAVE_SEL);
+}
+
+/**************************************************************************/
+void upd931_device::status_latch_w(offs_t offset, u8 data)
+{
+ // TODO: more details. ct8000 only checks if bits 1-3 are all zero or not
+ if (m_voice[offset].m_env_state == ENV_IDLE)
+ m_status = 0xf;
+ else
+ m_status = 0;
+}
+
+/**************************************************************************/
+void upd931_device::vibrato_w(u8 data)
+{
+ // TODO: implement this. ct8000 always writes 0 since it uses the external VCO vibrato instead
+ m_vibrato = data & 3;
+}
+
+/**************************************************************************/
+void upd931_device::sustain_w(u8 data)
+{
+ m_sustain = data & 3;
+ m_reverb = BIT(data, 1, 2) ? 1 : 0;
+}
+
+/**************************************************************************/
+void upd931_device::note_on_w(offs_t offset, u8 data)
+{
+ voice_t &voice = m_voice[offset];
+ if (BIT(data, 0))
+ note_on(voice);
+ else
+ voice.m_env_state = ENV_RELEASE;
+
+ // mt65 turns off sustain and reverb when changing tones, ct8000 does this instead
+ // (and also when playing a new note that is already playing)
+ voice.m_force_release = BIT(data, 3);
+}
+
+/**************************************************************************/
+void upd931_device::note_on(voice_t &voice)
+{
+ if (voice.m_note >= 0x2 && voice.m_note <= 0xe)
+ {
+ const u8 note = voice.m_note - 2;
+ u8 octave = voice.m_octave & 7;
+
+ if (octave >= 2)
+ octave -= 2; // octave values 0-1 are the same as 2-3
+
+ /*
+ setting bit 3 of the octave reduces the duty cycle of individual notes, which is
+ implemented here by changing which part of the phase counter to use as the sample address.
+ ct8000 uses this for a few of its presets to produce a simple key-scaling effect.
+ */
+ if (BIT(voice.m_octave, 3))
+ voice.m_timbre_shift = 3 - octave;
+ else
+ voice.m_timbre_shift = 0;
+
+ voice.m_pitch = m_pitch[octave * 12 + note];
+ }
+ else
+ {
+ voice.m_pitch = 0;
+ }
+
+ voice.m_pitch_counter = 0;
+ voice.m_wave_pos = 0xff;
+ voice.m_wave_out[0] = voice.m_wave_out[1] = 0;
+ voice.m_env_state = ENV_ATTACK1;
+ voice.m_env_counter = 0;
+
+ if (m_master)
+ reset_timer();
+}
+
+/**************************************************************************/
+void upd931_device::reset_timer()
+{
+ const attotime period = attotime::from_ticks(RETRIG_RATE, clock());
+ m_retrig_timer->adjust(period, 0, period);
+}
+
+/**************************************************************************/
+void upd931_device::update_env(voice_t &voice)
+{
+ const unsigned shift = BIT(m_flags, FLAG_ENV_SHIFT, 2);
+
+ switch (voice.m_env_state)
+ {
+ case ENV_IDLE:
+ return;
+
+ case ENV_ATTACK1:
+ {
+ static const u16 rates[] =
+ {
+ 0, 2048, 512, 256, 160, 80, 32, 8
+ };
+
+ const u8 val = BIT(m_flags, FLAG_ATTACK1, 3);
+ u32 rate;
+
+ if (val == 0)
+ rate = VOLUME_MAX; // value 0 = instant
+ else if (val < 4 && voice.m_env_counter >= (0xe0 << VOLUME_SHIFT))
+ rate = 160 << shift; // values 1-3 slow down at 7/8 of max volume
+ else
+ rate = rates[val] << shift;
+ voice.m_env_counter = std::min(voice.m_env_counter + rate, VOLUME_MAX);
+
+ // only increase wave A level if it isn't set to rise during attack2 instead
+ if (BIT(m_flags, FLAG_ATTACK2_A))
+ voice.m_env_level[0] = 0;
+ else
+ voice.m_env_level[0] = voice.m_env_counter;
+ voice.m_env_level[1] = voice.m_env_counter;
+
+ if (voice.m_env_counter >= VOLUME_MAX)
+ {
+ voice.m_env_counter = 0;
+ voice.m_env_state = ENV_ATTACK2;
+ }
+ break;
+ }
+
+ case ENV_ATTACK2:
+ {
+ static const u32 rates[] =
+ {
+ 0, 2048, 256, 128, 64, 32, 16, 8
+ };
+
+ const u8 val = BIT(m_flags, FLAG_ATTACK2, 3);
+ u32 rate;
+
+ if (val == 0)
+ rate = VOLUME_MAX;
+ else
+ rate = rates[val] << shift;
+ voice.m_env_counter = std::min(voice.m_env_counter + rate, VOLUME_MAX);
+
+ // fade wave A in, if specified
+ if (BIT(m_flags, FLAG_ATTACK2_A))
+ voice.m_env_level[0] = voice.m_env_counter;
+
+ // fade wave B out, if specified
+ if (BIT(m_flags, FLAG_ATTACK2_B))
+ voice.m_env_level[1] = VOLUME_MAX - voice.m_env_counter;
+
+ if (voice.m_env_counter >= VOLUME_MAX)
+ voice.m_env_state = ENV_DECAY1;
+
+ break;
+ }
+
+ case ENV_DECAY1:
+ {
+ static const u32 rates[] =
+ {
+ 2048, 640, 160, 32, 16, 8, 2, 0
+ };
+
+ const u8 val = BIT(m_flags, FLAG_DECAY1, 3);
+ const u32 rate = rates[val] << shift;
+
+ if (voice.m_env_counter < rate)
+ {
+ voice.m_env_counter = 0;
+ voice.m_env_state = ENV_IDLE;
+ }
+ else
+ {
+ voice.m_env_counter -= rate;
+ }
+
+ voice.m_env_level[0] = voice.m_env_counter;
+ // only fade wave B if it didn't already fade out during attack2
+ if (voice.m_env_level[1])
+ voice.m_env_level[1] = voice.m_env_counter;
+
+ if (!BIT(m_flags, FLAG_DECAY2_DISABLE))
+ {
+ // transition to decay2 at 1/2 or 1/4 of max volume, if enabled
+ const u8 decay2_level = BIT(m_flags, FLAG_DECAY2_LEVEL) ? 0x40 : 0x80;
+ if (voice.m_env_counter < (decay2_level << VOLUME_SHIFT))
+ voice.m_env_state = ENV_DECAY2;
+ }
+
+ break;
+ }
+
+ case ENV_DECAY2:
+ {
+ // apply reverb rate (below 1/8 max volume) or decay2 rate
+ u16 rate;
+
+ if (m_reverb && voice.m_env_counter < (0x20 << VOLUME_SHIFT))
+ rate = 1 << shift;
+ else if (BIT(m_flags, FLAG_DECAY2))
+ rate = 3 << shift;
+ else
+ rate = 6 << shift;
+
+ if (voice.m_env_counter < rate)
+ {
+ voice.m_env_counter = 0;
+ voice.m_env_state = ENV_IDLE;
+ }
+ else
+ {
+ voice.m_env_counter -= rate;
+ }
+
+ voice.m_env_level[0] = voice.m_env_counter;
+ if (voice.m_env_level[1])
+ voice.m_env_level[1] = voice.m_env_counter;
+
+ break;
+ }
+
+ case ENV_RELEASE:
+ {
+ // apply reverb rate (below 1/8 max volume), sustain rate (if enabled) or default release rate
+ u16 rate = 512 << shift;
+
+ if (!voice.m_force_release)
+ {
+ if (m_reverb && voice.m_env_counter < (0x20 << VOLUME_SHIFT))
+ rate = 1 << shift;
+ else if (m_sustain == 1)
+ rate = 16 << shift;
+ else if (m_sustain == 2)
+ rate = 12 << shift;
+ }
+
+ if (voice.m_env_counter < rate)
+ {
+ voice.m_env_counter = 0;
+ voice.m_env_state = ENV_IDLE;
+ }
+ else
+ {
+ voice.m_env_counter -= rate;
+ }
+
+ // fade each wave individually because if keyed off during attack, they may be different from each other
+ voice.m_env_level[0] = std::min(voice.m_env_level[0], voice.m_env_counter);
+ voice.m_env_level[1] = std::min(voice.m_env_level[1], voice.m_env_counter);
+
+ break;
+ }
+
+ }
+}
+
+/**************************************************************************/
+void upd931_device::update_wave(voice_t &voice)
+{
+ voice.m_pitch_counter += voice.m_pitch;
+
+ const u8 cycle = BIT(voice.m_pitch_counter, PITCH_SHIFT + 4, 2);
+ /*
+ the part of the counter which is treated as the sample address depends on if key scaling is active.
+ if it is active, then the voice alternates between a narrowed waveform and silence, which alters
+ the timbre without affecting pitch
+ */
+ u8 pos = BIT(voice.m_pitch_counter, PITCH_SHIFT - voice.m_timbre_shift, 4 + voice.m_timbre_shift);
+ if (pos == voice.m_wave_pos || pos >= 0x10)
+ return;
+
+ voice.m_wave_pos = pos;
+
+ // play every other cycle backwards, if enabled
+ if (BIT(m_flags, FLAG_MIRROR) && BIT(cycle, 0))
+ pos ^= 0xf;
+
+ // flag bits determine which of 4 consecutive cycles to apply waveforms A and B
+ const unsigned cycle_mode[] =
+ {
+ BIT(m_flags, FLAG_MODE_A, 2),
+ BIT(m_flags, FLAG_MODE_B, 2)
+ };
+
+ static const u8 cycle_mask[] =
+ {
+ 0xf, // always on
+ 0x5, // on, off, on, off
+ 0x1, // on 1x, off 3x
+ 0x3 // on 2x, off 2x
+ };
+
+ static const s8 steps[] =
+ {
+ 0, 1, 2, 2, 4, 4, 8, 8,
+ 0, -1, -2, -2, -4, -4, -8, -8
+ };
+
+ for (int i = 0; i < 2; i++)
+ {
+ // check if this waveform is enabled for this cycle
+ if (!BIT(cycle_mask[cycle_mode[i]], cycle))
+ continue;
+
+ s8 step = steps[m_wave[i][pos] & 0xf];
+ // invert waveform on every other cycle, if enabled
+ if (BIT(m_flags, FLAG_INVERT) && BIT(cycle, 0))
+ step = -step;
+
+ voice.m_wave_out[i] += step;
+ voice.m_wave_out[i] = util::sext(voice.m_wave_out[i] & 0x3f, 6);
+ }
+}