diff options
| author | 2026-05-14 07:40:35 -0400 | |
|---|---|---|
| committer | 2026-05-14 07:40:35 -0400 | |
| commit | 3d11b5a17de05ea08f718a3a35192940a2c6c919 (patch) | |
| tree | ae8cc1bb2196b3eb7d97f837827869fd887847bf | |
| parent | 4a26071d6671b0a614b275ae31bf40b4c3eb476f (diff) | |
sound/l4003.cpp: Main speakers turn on. [R. Belmont]
* All registers hooked up and working thanks to very close similarity to the L6028.
| -rw-r--r-- | src/devices/sound/l4003.cpp | 245 | ||||
| -rw-r--r-- | src/devices/sound/l4003.h | 14 |
2 files changed, 187 insertions, 72 deletions
diff --git a/src/devices/sound/l4003.cpp b/src/devices/sound/l4003.cpp index 2eebd4e3dd5..60399f53a9d 100644 --- a/src/devices/sound/l4003.cpp +++ b/src/devices/sound/l4003.cpp @@ -6,8 +6,9 @@ Emulation by R. Belmont This is the audio chip used in the Akai MPC60, with 16 voices of 12-bit sample playback. - No filter, but some sort of "echo buffer" exists which is probably similar/identical to the - delay effect on the later L6028 chip. + There are many similarities to the later L6028 chip and many of the registers are either + binary identical or very similar. The main difference is the sense of the volume envelope + is reversed, where 0 here is max volume and 0xffff is silence. The chip has a total of 10 unique outputs, 8 individual outputs and a stereo pair. Each voice can be sent to an individual output, the stereo pair, or both. @@ -19,24 +20,35 @@ (768K or 1536K bytes). For ease of emulation, we make those 16-bit words. Voice registers (low 4 bits are the voice number) - 001x - ssss ssss ssss ssss - s - loop start offset in samples from the start of memory + 000x - ssss ssss ssss ssss + s - start offset in samples from the start of memory (low word) - 011x - ssss ssss ssss ssss - s - start offset in samples from the start of memory + 001x - ssss ssss ssss ssss + s - start offset in samples from the start of memory (high word) - 002x - key on/off + 002x - pitch in 4.12 fixed point where 0x1000 is 40.0 kHz 003x - eeee eeee ???? oooo - e - echo send level - o - mix output where 0 = 7, 1 = 8, 2 = 9, ... f = 8 + e - send level + o - send destination where 0 = 7, 1 = 8, 2 = 9, ... f = 8 + + 004x - Envelope volume (0 = max volume, 0xffff = silent) + + 005x - Volume envelope target + + 006x - Volume envelope rate in 8.8 fixed point where 0x100 is 1 step per sample + + 007x - Main stereo left and right volumes + + 011x - Readback of register 001x + + 015x - Readback of register 005x - 004x - unknown + 016x - Readback of register 006x - 005x - unknown + 0200 - Wave RAM write (uses voice 1's start address as the pointer) - 006x - ???? ???? ???? ???? - This is somehow both volume and pitch. Volume changes by 0x66 each step, pitch by 0x4c (102 and 76) + 0300 - Wave RAM read (uses voice 1's start address as the pointer) **************************************************************************************************/ @@ -56,6 +68,8 @@ DEFINE_DEVICE_TYPE(L4003, l4003_sound_device, "l4003", "Akai/Roger Linn L4003 wavetable sound") +static constexpr uint32_t OUTPUT_SCALE = 0x7fff; + l4003_sound_device::l4003_sound_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, L4003, tag, owner, clock) , device_sound_interface(mconfig, *this) @@ -64,9 +78,24 @@ l4003_sound_device::l4003_sound_device(const machine_config &mconfig, const char , m_mem_config("l4003", ENDIANNESS_LITTLE, 16, 21) , m_data(0) , m_control(0) - , m_key_status(0) , m_sample_rate(0) { + for (auto &voice : m_voices) + { + voice.start = 0; + voice.step = 0; + voice.pos = 0; + voice.frac = 0; + voice.l_volume = 0; + voice.r_volume = 0; + voice.env_volume = 0xffff; + voice.env_target = 0xffff; + voice.env_step = 0; + voice.env_pos = 0; + voice.send_dest = 0; + voice.send_level = 0; + voice.filt_state = 0; + } } void l4003_sound_device::map(address_map &map) @@ -86,7 +115,23 @@ void l4003_sound_device::device_start() // Allocate the stream m_sample_rate = clock() / 896.0f; // 35.84 MHz clock / 896 = 40.0 kHz sample rate - m_stream = stream_alloc(0, 10, m_sample_rate); + m_stream = stream_alloc(0, 10, m_sample_rate, STREAM_SYNCHRONOUS); + + save_item(STRUCT_MEMBER(m_voices, start)); + save_item(STRUCT_MEMBER(m_voices, step)); + save_item(STRUCT_MEMBER(m_voices, pos)); + save_item(STRUCT_MEMBER(m_voices, frac)); + save_item(STRUCT_MEMBER(m_voices, l_volume)); + save_item(STRUCT_MEMBER(m_voices, r_volume)); + save_item(STRUCT_MEMBER(m_voices, env_volume)); + save_item(STRUCT_MEMBER(m_voices, env_target)); + save_item(STRUCT_MEMBER(m_voices, env_step)); + save_item(STRUCT_MEMBER(m_voices, env_pos)); + save_item(STRUCT_MEMBER(m_voices, send_dest)); + save_item(STRUCT_MEMBER(m_voices, send_level)); + save_item(STRUCT_MEMBER(m_voices, filt_state)); + save_item(NAME(m_data)); + save_item(NAME(m_control)); } void l4003_sound_device::device_reset() @@ -97,60 +142,102 @@ void l4003_sound_device::sound_stream_update(sound_stream &stream) { for (int i = 0; i < NUM_VOICES; i++) { - if (m_key_status & (1 << i)) + l4003_voice &vptr = m_voices[i]; + + const uint32_t step = vptr.step; + uint32_t pos = vptr.pos; + uint32_t frac = vptr.frac; + + for (int j = 0; j < stream.samples(); j++) { - l4003_voice &vptr = m_voices[i]; + uint32_t address; + int32_t sample; - uint32_t start = vptr.start; - const uint32_t step = vptr.step; + pos += (frac >> 12); + frac &= 0xfff; - uint32_t pos = vptr.pos; - uint32_t frac = vptr.frac; + address = (vptr.start << 1) + pos; + sample = (int16_t)m_cache.read_word(address & 0x1fffff); - for (int j = 0; j < stream.samples(); j++) - { - uint32_t address; - int32_t sample; + frac += step; - pos += (frac >> 12); - frac &= 0xfff; + // The following is a pretty literal decompile of the assembly code in the MPC3000 + // which massages the MPC60 samples into 16 bits. It's presumed to be simulating internal + // processing of the L4003 since it sounds closer than just playing the 12-bit samples + // as 12-bit linear. + const int32_t temp = + (int32_t)(((uint32_t)(uint16_t)sample << 16) | (uint16_t)vptr.filt_state) >> 8; + const int32_t new_state = temp + (int32_t)(((int64_t)vptr.filt_state * 647719) >> 20); - address = start + pos; - sample = (int16_t)m_cache.read_word(address); + int32_t temp2 = + (int32_t)(((int64_t)new_state * 374536) >> 20) + + (int32_t)(((int64_t)vptr.filt_state * 25693) >> 20); - frac += step; + vptr.filt_state = new_state; - if (vptr.send_level == 0) + temp2 += (temp2 >> 3) + (temp2 >> 4); + if (temp2 >= 0) + { + if ((uint32_t)temp2 >= ((uint32_t)OUTPUT_SCALE << 8)) { - sample = 0; + sample = (int16_t)(OUTPUT_SCALE - 1); } + else + { + sample = (int16_t)((temp2 << 7) / (int32_t)OUTPUT_SCALE); + } + } + else + { + if ((temp2 >> 8) <= -(int32_t)OUTPUT_SCALE) + { + sample = (int16_t)-(int16_t)(OUTPUT_SCALE - 1); + } + else + { + sample = (int16_t)((temp2 << 7) / (int32_t)OUTPUT_SCALE); + } + } - const int64_t left = (sample * (uint64_t(vptr.volume) * uint64_t(vptr.env_volume))) >> 24; - const int64_t right = (sample * (uint64_t(vptr.volume) * uint64_t(vptr.env_volume))) >> 24; - stream.add_int(0, j, left, 32768); - stream.add_int(1, j, right, 32768); - - if (vptr.send_level > 0) + // volume envelope processing + vptr.env_pos += vptr.env_step; + const int steps = ((uint32_t)vptr.env_pos / 0x100); + if (steps > 0) + { + if (vptr.env_volume < vptr.env_target) { - const int dest = vptr.send_dest & 0xf; - if (dest != 0xf) - { - const int64_t send = (sample * (uint64_t(vptr.send_level) * uint64_t(vptr.env_volume))) >> 24; - stream.add_int(2 + (dest - 7), j, send, 32768); - } + vptr.env_volume += std::min(steps, (vptr.env_target - vptr.env_volume)); + } + else if (vptr.env_volume > vptr.env_target) + { + vptr.env_volume -= std::min(steps, (vptr.env_volume - vptr.env_target)); } } + vptr.env_pos &= 0xff; + + const int64_t left = (sample * (uint64_t(vptr.l_volume) * uint64_t(0xffff - vptr.env_volume))) >> 24; + const int64_t right = (sample * (uint64_t(vptr.r_volume) * uint64_t(0xffff - vptr.env_volume))) >> 24; + stream.add_int(0, j, left, 32768); + stream.add_int(1, j, right, 32768); - vptr.pos = pos; - vptr.frac = frac; + if (vptr.send_level > 0) + { + const int dest = vptr.send_dest & 0xf; + if (dest != 0xf) + { + const int64_t send = (sample * (uint64_t(vptr.send_level) * uint64_t(0xffff - vptr.env_volume))) >> 24; + stream.add_int(2 + (dest - 7), j, send, 32768); + } + } } + + vptr.pos = pos; + vptr.frac = frac; } } uint16_t l4003_sound_device::data_r() { - m_stream->update(); - LOGMASKED(LOG_REGISTERS, "%s: read data, control %04x (%s)\n", tag(), m_control, machine().describe_context()); return m_data; @@ -158,8 +245,6 @@ uint16_t l4003_sound_device::data_r() void l4003_sound_device::data_w(uint16_t data) { - m_stream->update(); - LOGMASKED(LOG_REGISTERS_HIFREQ, "%s: %04x to data\n", tag(), data); m_data = data; @@ -203,39 +288,64 @@ void l4003_sound_device::control_w(uint16_t data) LOGMASKED(LOG_REGISTERS, "ch %d Sample ptr high %04x => %04x\n", voice, m_data, v.start); break; - // key on/off + // pitch in 4.12 fixed-point format, similar to the L6028 case 0x0002: - if (m_data & 0x1000) + // if the pitch is being set from 0 to nonzero, it's a key-on so reset some state + if ((m_data != 0) && (v.step == 0)) { - m_key_status |= (1 << voice); - v.pos = 0; v.frac = 0; - v.step = 0x1000; - v.env_volume = 0x7fff; - } - else - { - m_key_status &= ~(1 << voice); + v.env_pos = 0; + v.filt_state = 0; } - LOGMASKED(LOG_REGISTERS, "ch %d: Key %s %04x (%s)\n", voice, (m_data & 0x1000) ? "on" : "off", m_data, machine().describe_context()); + v.step = m_data; + + LOGMASKED(LOG_REGISTERS, "ch %d: Pitch %04x (%s)\n", voice, m_data, machine().describe_context()); break; case 0x0003: v.send_level = (m_data >> 8) & 0xff; - v.send_dest = m_data & 0x0f; - LOGMASKED(LOG_REGISTERS, "ch %d: Echo send volume %02x, dest %x (%s)\n", voice, v.send_level, v.send_dest, machine().describe_context()); + v.send_dest = m_data & 0xf; + LOGMASKED(LOG_REGISTERS, "ch %d: Send volume %02x, dest %x (%s)\n", voice, v.send_level, v.send_dest, machine().describe_context()); + break; + + case 0x0004: + v.env_volume = m_data; + LOGMASKED(LOG_REGISTERS, "ch %d: Envelope volume %04x (%s)\n", voice, v.env_volume, machine().describe_context()); + break; + + case 0x0005: + v.env_target = m_data; + LOGMASKED(LOG_REGISTERS, "ch %d: Envelope target %04x (%s)\n", voice, v.env_target, machine().describe_context()); break; case 0x0006: - v.volume = (m_data / 0x66) & 0xff; - LOGMASKED(LOG_REGISTERS, "ch %d: Volume %02x (%s)\n", voice, v.volume, machine().describe_context()); + v.env_step = m_data; + LOGMASKED(LOG_REGISTERS, "ch %d: Envelope rate %04x (%s)\n", voice, v.env_step, machine().describe_context()); + break; + + case 0x0007: + v.l_volume = m_data & 0xff; + v.r_volume = (m_data >> 8) & 0xff; + LOGMASKED(LOG_REGISTERS, "ch %d: Volume L %02x R %02x (%s)\n", voice, v.l_volume, v.r_volume, machine().describe_context()); break; +// ----- Readbacks ---------------------------- + case 0x0011: - v.loop_start = m_data; - LOGMASKED(LOG_REGISTERS, "ch %d Loop start %04x\n", voice, m_data); + m_data = v.start >> 4; + LOGMASKED(LOG_REGISTERS, "ch %d: Sample ptr readback %04x (%s)\n", voice, v.start, machine().describe_context()); + break; + + case 0x0015: + m_data = v.env_target; + LOGMASKED(LOG_REGISTERS, "ch %d: Envelope target readback %04x (%s)\n", voice, v.env_target, machine().describe_context()); + break; + + case 0x0016: + m_data = v.env_step; + LOGMASKED(LOG_REGISTERS, "ch %d: Envelope rate readback %04x (%s)\n", voice, v.env_step, machine().describe_context()); break; default: @@ -265,3 +375,4 @@ void l4003_sound_device::control_w(uint16_t data) } } } + diff --git a/src/devices/sound/l4003.h b/src/devices/sound/l4003.h index 6086fff06d4..f4cee16975f 100644 --- a/src/devices/sound/l4003.h +++ b/src/devices/sound/l4003.h @@ -31,14 +31,18 @@ private: constexpr l4003_voice() {} uint32_t start = 0; - uint32_t loop_start = 0; + uint32_t step = 0; uint32_t pos = 0; uint32_t frac = 0; - uint32_t step = 0; + uint16_t l_volume = 0; + uint16_t r_volume = 0; uint16_t env_volume = 0; - uint8_t volume = 0; - uint8_t send_level = 0; + uint16_t env_target = 0; + uint16_t env_step = 0; + uint32_t env_pos = 0; uint8_t send_dest = 0; + uint8_t send_level = 0; + int32_t filt_state = 0; }; sound_stream *m_stream; @@ -47,7 +51,7 @@ private: memory_access<21, 1, 0, ENDIANNESS_LITTLE>::cache m_cache; - uint16_t m_data, m_control, m_key_status; + uint16_t m_data, m_control; uint32_t m_sample_rate; uint16_t data_r(); |
