diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b71d8f70..e58a2794a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -324,6 +324,7 @@ src/engine/platform/sound/ymz280b.cpp src/engine/platform/sound/rf5c68.cpp src/engine/platform/sound/oki/okim6258.cpp +src/engine/platform/sound/oki/msm6295.cpp src/engine/platform/oplAInterface.cpp src/engine/platform/ym2608Interface.cpp diff --git a/src/engine/platform/sound/oki/msm6295.cpp b/src/engine/platform/sound/oki/msm6295.cpp new file mode 100644 index 000000000..29ca31f9d --- /dev/null +++ b/src/engine/platform/sound/oki/msm6295.cpp @@ -0,0 +1,209 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/blob/main/LICENSE for more details + + Copyright holder(s): cam900 + OKI MSM6295 emulation core + + It is 4 channel ADPCM playback chip from OKI semiconductor. + It was becomes de facto standard for ADPCM playback in arcade machine, due to cost performance. + + The chip itself is pretty barebone: there is no "register" in chip. + It can't control volume and pitch in currently playing channels, only stopable them. + And volume is must be determined at playback start command. + + Command format: + + Playback command (2 byte): + + Byte Bit Description + 76543210 + 0 1xxxxxxx Phrase select (Header stored in ROM) + 1 x000---- Play channel 4 + 0x00---- Play channel 3 + 00x0---- Play channel 2 + 000x---- Play channel 1 + ----xxxx Volume + ----0000 0.0dB + ----0001 -3.2dB + ----0010 -6.0dB + ----0011 -9.2dB + ----0100 -12.0dB + ----0101 -14.5dB + ----0110 -18.0dB + ----0111 -20.5dB + ----1000 -24.0dB + + Suspend command (1 byte): + + Byte Bit Description + 76543210 + 0 0x------ Suspend channel 4 + 0-x----- Suspend channel 3 + 0--x---- Suspend channel 2 + 0---x--- Suspend channel 1 + + Frequency calculation: + if (SS) then + Frequency = Input clock / 165 + else then + Frequency = Input clock / 132 + +*/ + +#include "msm6295.hpp" + +void msm6295_core::tick() +{ + // command handler + if (m_command_pending) + { + if (bitfield(m_command, 7)) // play voice + { + if ((++m_clock) >= ((15 * (m_ss ? 5 : 4)))) + { + m_clock = 0; + if (bitfield(m_next_command, 4, 4) != 0) + { + for (int i = 0; i < 4; i++) + { + if (bitfield(m_next_command, 4 + i)) + { + if (!m_voice[i].m_busy) + { + m_voice[i].m_command = m_command; + m_voice[i].m_volume = (bitfield(m_next_command, 0, 4) < 9) ? m_volume_table[std::min(8, bitfield(m_next_command, 0, 4))] : 0; + } + break; // voices aren't be playable simultaneously at once + } + } + } + m_command = 0; + m_command_pending = false; + } + } + else if (bitfield(m_next_command, 7)) // select phrase + { + if ((++m_clock) >= ((15 * (m_ss ? 5 : 4)))) + { + m_clock = 0; + m_command = m_next_command; + m_command_pending = false; + } + } + else + { + if (bitfield(m_next_command, 3, 4) != 0) // suspend voices + { + for (int i = 0; i < 4; i++) + { + if (bitfield(m_next_command, 3 + i)) + { + if (m_voice[i].m_busy) + m_voice[i].m_command = m_next_command; + } + } + m_next_command &= ~0x78; + } + m_command_pending = false; + } + } + m_out = 0; + for (int i = 0; i < 4; i++) + { + m_voice[i].tick(); + m_out += m_voice[i].m_out; + } +} + +void msm6295_core::reset() +{ + for (auto & elem : m_voice) + elem.reset(); + + m_command = 0; + m_next_command = 0; + m_command_pending = false; + m_clock = 0; + m_out = 0; +} + +void msm6295_core::voice_t::tick() +{ + if (!m_busy) + { + if (bitfield(m_command, 7)) + { + // get phrase header (stored in data memory) + const u32 phrase = bitfield(m_command, 0, 7) << 3; + // Start address + m_addr = (bitfield(m_host.m_intf.read_byte(phrase | 0), 0, 2) << 16) + | (m_host.m_intf.read_byte(phrase | 1) << 8) + | (m_host.m_intf.read_byte(phrase | 2) << 0); + // End address + m_end = (bitfield(m_host.m_intf.read_byte(phrase | 3), 0, 2) << 16) + | (m_host.m_intf.read_byte(phrase | 4) << 8) + | (m_host.m_intf.read_byte(phrase | 5) << 0); + m_nibble = 4; // MSB first, LSB second + m_command = 0; + m_busy = true; + vox_decoder_t::reset(); + } + m_out = 0; + } + else + { + // playback + if ((++m_clock) >= ((33 * (m_host.m_ss ? 5 : 4)))) + { + m_clock = 0; + bool is_end = (m_command != 0); + m_curr.decode(bitfield(m_host.m_intf.read_byte(m_addr), m_nibble, 4)); + if (m_nibble <= 0) + { + m_nibble = 4; + if (++m_addr > m_end) + is_end = true; + } + else + m_nibble -= 4; + if (is_end) + { + m_command = 0; + m_busy = false; + } + m_out = (out() * m_volume) >> 7; // scale out to 12 bit output + } + } +} + +void msm6295_core::voice_t::reset() +{ + vox_decoder_t::reset(); + m_clock = 0; + m_busy = false; + m_command = 0; + m_addr = 0; + m_nibble = 0; + m_end = 0; + m_volume = 0; + m_out = 0; +} + +// accessors +u8 msm6295_core::busy_r() +{ + return (m_voice[0].m_busy ? 0x01 : 0x00) + | (m_voice[1].m_busy ? 0x02 : 0x00) + | (m_voice[2].m_busy ? 0x04 : 0x00) + | (m_voice[3].m_busy ? 0x08 : 0x00); +} + +void msm6295_core::command_w(u8 data) +{ + if (!m_command_pending) + { + m_next_command = data; + m_command_pending = true; + } +} diff --git a/src/engine/platform/sound/oki/msm6295.hpp b/src/engine/platform/sound/oki/msm6295.hpp new file mode 100644 index 000000000..13dc61558 --- /dev/null +++ b/src/engine/platform/sound/oki/msm6295.hpp @@ -0,0 +1,91 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/blob/main/LICENSE for more details + + Copyright holder(s): cam900 + OKI MSM6295 emulation core + + See msm6295.cpp for more info. +*/ + +#include "util.hpp" +#include "vox.hpp" +#include +#include + +#ifndef _VGSOUND_EMU_MSM6295_HPP +#define _VGSOUND_EMU_MSM6295_HPP + +#pragma once + +class msm6295_core : public vox_core +{ + friend class vgsound_emu_mem_intf; // common memory interface +public: + // constructor + msm6295_core(vgsound_emu_mem_intf &intf) + : m_voice{{*this,*this},{*this,*this},{*this,*this},{*this,*this}} + , m_intf(intf) + { + } + // accessors, getters, setters + u8 busy_r(); + void command_w(u8 data); + void ss_w(bool ss) { m_ss = ss; } // SS pin + + // internal state + void reset(); + void tick(); + + s32 out() { return m_out; } // built in 12 bit DAC + +private: + // Internal volume table, 9 step + const s32 m_volume_table[9] = { + 32/* 0.0dB */, + 22/* -3.2dB */, + 16/* -6.0dB */, + 11/* -9.2dB */, + 8/* -12.0dB */, + 6/* -14.5dB */, + 4/* -18.0dB */, + 3/* -20.5dB */, + 2/* -24.0dB */ }; // scale out to 5 bit for optimization + + // msm6295 voice structs + struct voice_t : vox_decoder_t + { + // constructor + voice_t(vox_core &vox, msm6295_core &host) + : vox_decoder_t(vox) + , m_host(host) + {}; + + // internal state + virtual void reset() override; + void tick(); + + // accessors, getters, setters + // registers + msm6295_core &m_host; + u16 m_clock = 0; // clock counter + bool m_busy = false; // busy status + u8 m_command = 0; // current command + u32 m_addr = 0; // current address + s8 m_nibble = 0; // current nibble + u32 m_end = 0; // end address + s32 m_volume = 0; // volume + s32 m_out = 0; // output + }; + voice_t m_voice[4]; + vgsound_emu_mem_intf &m_intf; // common memory interface + + bool m_ss = false; // SS pin controls divider, input clock / 33 * (SS ? 5 : 4) + u8 m_command = 0; // Command byte + u8 m_next_command = 0; // Next command + bool m_command_pending = false; // command pending flag + u16 m_clock = 0; // clock counter + s32 m_out = 0; // 12 bit output +}; + +#endif diff --git a/src/engine/platform/sound/oki/util.hpp b/src/engine/platform/sound/oki/util.hpp new file mode 100644 index 000000000..6d6e235a7 --- /dev/null +++ b/src/engine/platform/sound/oki/util.hpp @@ -0,0 +1,158 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/blob/main/LICENSE for more details + + Copyright holders: cam900 + Various core utilities for vgsound_emu +*/ + +#include +#include +#include + +#ifndef _VGSOUND_EMU_CORE_UTIL_HPP +#define _VGSOUND_EMU_CORE_UTIL_HPP + +#pragma once + +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; +typedef signed char s8; +typedef signed short s16; +typedef signed int s32; +typedef signed long long s64; +typedef float f32; +typedef double f64; + +const f64 PI = 3.1415926535897932384626433832795; + +// get bitfield, bitfield(input, position, len) +template T bitfield(T in, u8 pos, u8 len = 1) +{ + return (in >> pos) & (len ? (T(1 << len) - 1) : 1); +} + +// get sign extended value, sign_ext(input, len) +template T sign_ext(T in, u8 len) +{ + len = std::max(0, (8 * sizeof(T)) - len); + return T(T(in) << len) >> len; +} + +// convert attenuation decibel value to gain +f32 dB_to_gain(f32 attenuation) +{ + return powf(10.0f, attenuation / 20.0f); +} + +class vgsound_emu_mem_intf +{ +public: + virtual u8 read_byte(u32 address) { return 0; } + virtual u16 read_word(u32 address) { return 0; } + virtual u32 read_dword(u32 address) { return 0; } + virtual u64 read_qword(u32 address) { return 0; } + virtual void write_byte(u32 address, u8 data) { } + virtual void write_word(u32 address, u16 data) { } + virtual void write_dword(u32 address, u32 data) { } + virtual void write_qword(u32 address, u64 data) { } +}; + +template +struct clock_pulse_t +{ + void reset(T init = InitWidth) + { + m_edge.reset(); + m_width = m_width_latch = m_counter = init; + m_cycle = 0; + } + + bool tick(T width = 0) + { + bool carry = ((--m_counter) <= 0); + if (carry) + { + if (!width) + m_width = m_width_latch; + else + m_width = width; // reset width + m_counter = m_width; + m_cycle = 0; + } + else + m_cycle++; + + m_edge.tick(carry); + return carry; + } + + void set_width(T width) { m_width = width; } + void set_width_latch(T width) { m_width_latch = width; } + + // Accessors + bool current_edge() { return m_edge.m_current; } + bool rising_edge() { return m_edge.m_rising; } + bool falling_edge() { return m_edge.m_rising; } + T cycle() { return m_cycle; } + + struct edge_t + { + edge_t() + : m_current(InitEdge ^ 1) + , m_previous(InitEdge) + , m_rising(0) + , m_falling(0) + , m_changed(0) + { + set(InitEdge); + } + + void tick(bool toggle) + { + u8 current = m_current; + if (toggle) + current ^= 1; + set(current); + } + + void set(u8 edge) + { + edge &= 1; + m_rising = m_falling = m_changed = 0; + if (m_current != edge) + { + m_changed = 1; + if (m_current && (!edge)) + m_falling = 1; + else if ((!m_current) && edge) + m_rising = 1; + m_current = edge; + } + m_previous = m_current; + } + + void reset() + { + m_previous = InitEdge; + m_current = InitEdge ^ 1; + set(InitEdge); + } + + u8 m_current : 1; // current edge + u8 m_previous : 1; // previous edge + u8 m_rising : 1; // rising edge + u8 m_falling : 1; // falling edge + u8 m_changed : 1; // changed flag + }; + + edge_t m_edge; + T m_width = InitWidth; // clock pulse width + T m_width_latch = InitWidth; // clock pulse width latch + T m_counter = InitWidth; // clock counter + T m_cycle = 0; // clock cycle +}; + +#endif diff --git a/src/engine/platform/sound/oki/vox.hpp b/src/engine/platform/sound/oki/vox.hpp new file mode 100644 index 000000000..b1fc6e236 --- /dev/null +++ b/src/engine/platform/sound/oki/vox.hpp @@ -0,0 +1,115 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/blob/main/LICENSE for more details + + Copyright holder(s): cam900 + Dialogic ADPCM core +*/ + +#include "util.hpp" +#include +#include + +#ifndef _VGSOUND_EMU_CORE_VOX_HPP +#define _VGSOUND_EMU_CORE_VOX_HPP + +#pragma once + +#define MODIFIED_CLAMP(x,xMin,xMax) (std::min(std::max((x),(xMin)),(xMax))) + +class vox_core +{ +protected: + struct vox_decoder_t + { + vox_decoder_t(vox_core &vox) + : m_curr(vox) + , m_loop(vox) + { }; + + virtual void reset() + { + m_curr.reset(); + m_loop.reset(); + m_loop_saved = false; + } + + void save() + { + if (!m_loop_saved) + { + m_loop.copy(m_curr); + m_loop_saved = true; + } + } + + void restore() + { + if (m_loop_saved) + m_curr.copy(m_loop); + } + + s32 out() { return m_curr.m_step; } + + struct decoder_state_t + { + decoder_state_t(vox_core &vox) + : m_vox(vox) + { }; + + void reset() + { + m_index = 0; + m_step = 16; + } + + void copy(decoder_state_t src) + { + m_index = src.m_index; + m_step = src.m_step; + } + + void decode(u8 nibble) + { + const u8 delta = bitfield(nibble, 0, 3); + s16 ss = m_vox.m_step_table[m_index]; // ss(n) + + // d(n) = (ss(n) * B2) + ((ss(n) / 2) * B1) + ((ss(n) / 4) * B0) + (ss(n) / 8) + s16 d = ss >> 3; + if (bitfield(delta, 2)) + d += ss; + if (bitfield(delta, 1)) + d += (ss >> 1); + if (bitfield(delta, 0)) + d += (ss >> 2); + + // if (B3 = 1) then d(n) = d(n) * (-1) X(n) = X(n-1) * d(n) + if (bitfield(nibble, 3)) + m_step = std::max(m_step - d, -2048); + else + m_step = std::min(m_step + d, 2047); + + // adjust step index + m_index = MODIFIED_CLAMP(m_index + m_vox.m_index_table[delta], 0, 48); + } + + vox_core &m_vox; + s8 m_index = 0; + s32 m_step = 16; + }; + + decoder_state_t m_curr; + decoder_state_t m_loop; + bool m_loop_saved = false; + }; + + s8 m_index_table[8] = {-1, -1, -1, -1, 2, 4, 6, 8}; + s32 m_step_table[49] = { + 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, + 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, + 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552 + }; +}; + +#endif