From 75584bff34ce28fe1e8b0cbd554b94c72462ad14 Mon Sep 17 00:00:00 2001 From: vampi Date: Wed, 31 Dec 2025 01:16:13 +0200 Subject: [PATCH 1/3] Add YM2414 (OPZ) support via ymfm - Add DEVID_YM2414 (0x30) device ID - Vendor ymfm source files (commit 17decfa) in emu/cores/ymfm/ - ymfm.h - Core infrastructure - ymfm_opz.h - YM2414 (OPZ) implementation - ymfm_fm.h/ipp - FM engine - Create C++ wrapper (ymfmintf.cpp/h) with extern C interface - Integrate into build system (CMakeLists.txt, Makefile) - Register device in SoundEmu.c - YM2414 specs: 8 FM channels, 4 operators per channel - Used in Yamaha TX81Z, DX11, YS200 synthesizers The implementation follows existing patterns (similar to ICS2115) and is designed to support future ymfm chip additions (OPN, OPM, OPLL, etc.). --- Makefile | 1 + emu/CMakeLists.txt | 12 + emu/SoundDevs.h | 1 + emu/SoundEmu.c | 7 + emu/cores/ymfm/README.md | 26 + emu/cores/ymfm/ymfm.h | 566 +++++++++++++ emu/cores/ymfm/ymfm_fm.h | 463 +++++++++++ emu/cores/ymfm/ymfm_fm.ipp | 1592 ++++++++++++++++++++++++++++++++++++ emu/cores/ymfm/ymfm_opz.h | 332 ++++++++ emu/cores/ymfmintf.cpp | 271 ++++++ emu/cores/ymfmintf.h | 14 + 11 files changed, 3285 insertions(+) create mode 100644 emu/cores/ymfm/README.md create mode 100644 emu/cores/ymfm/ymfm.h create mode 100644 emu/cores/ymfm/ymfm_fm.h create mode 100644 emu/cores/ymfm/ymfm_fm.ipp create mode 100644 emu/cores/ymfm/ymfm_opz.h create mode 100644 emu/cores/ymfmintf.cpp create mode 100644 emu/cores/ymfmintf.h diff --git a/Makefile b/Makefile index 6acbc2e8..8cb8f205 100644 --- a/Makefile +++ b/Makefile @@ -230,6 +230,7 @@ LIBEMUOBJS = \ $(LIBEMUOBJ)/cores/x1_010.o \ $(LIBEMUOBJ)/cores/c352.o \ $(LIBEMUOBJ)/cores/iremga20.o \ + $(LIBEMUOBJ)/cores/ymfmintf.o \ $(LIBEMUOBJ)/Resampler.o \ $(LIBEMUOBJ)/panning.o \ $(LIBEMUOBJ)/dac_control.o diff --git a/emu/CMakeLists.txt b/emu/CMakeLists.txt index 823e5272..e2505b65 100644 --- a/emu/CMakeLists.txt +++ b/emu/CMakeLists.txt @@ -86,6 +86,7 @@ option(SNDEMU_MSM5205_ALL "Sound Device MSM5205: all cores" OFF) option(SNDEMU_MSM5232_ALL "Sound Device MSM5232: all cores" OFF) option(SNDEMU_BSMT2000_ALL "Sound Device BSMT2000: all cores" OFF) option(SNDEMU_ICS2115_ALL "Sound Device ICS2115: all cores" OFF) +option(SNDEMU_YM2414_YMFM "Sound Device YM2414 (OPZ): ymfm core" OFF) # console/computer presets option(SNDEMU__PRESET_SMS "Sound Dev. Preset: Sega Master System/Game Gear" OFF) @@ -151,6 +152,7 @@ if(SNDEMU__ALL) set(SNDEMU_MSM5232_ALL ON) set(SNDEMU_BSMT2000_ALL ON) set(SNDEMU_ICS2115_ALL ON) + set(SNDEMU_YM2414_YMFM ON) endif() @@ -648,6 +650,16 @@ if(SNDEMU_ICS2115_ALL) set(EMU_FILES ${EMU_FILES} cores/ics2115.c) set(EMU_CORE_HEADERS ${EMU_CORE_HEADERS} cores/ics2115.h) endif() +if(SNDEMU_YM2414_YMFM) + set(EMU_DEFS ${EMU_DEFS} " SNDDEV_YM2414" " EC_YM2414_YMFM") + set(EMU_FILES ${EMU_FILES} cores/ymfmintf.cpp) + set(EMU_CORE_HEADERS ${EMU_CORE_HEADERS} cores/ymfmintf.h) + set(EMU_CORE_HEADERS ${EMU_CORE_HEADERS} + cores/ymfm/ymfm.h + cores/ymfm/ymfm_opz.h + cores/ymfm/ymfm_fm.h + ) +endif() add_library(${PROJECT_NAME} ${LIBRARY_TYPE} ${EMU_FILES}) set_target_properties(${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/emu/SoundDevs.h b/emu/SoundDevs.h index 6ef51243..d2e91b91 100644 --- a/emu/SoundDevs.h +++ b/emu/SoundDevs.h @@ -49,4 +49,5 @@ #define DEVID_MSM5232 0x2D #define DEVID_BSMT2000 0x2E #define DEVID_ICS2115 0x2F +#define DEVID_YM2414 0x30 // YM2414 (OPZ), used in TX81Z #endif // __SOUNDDEVS_H__ diff --git a/emu/SoundEmu.c b/emu/SoundEmu.c index 6426658c..ae9905c7 100644 --- a/emu/SoundEmu.c +++ b/emu/SoundEmu.c @@ -56,6 +56,7 @@ #define SNDDEV_MSM5232 #define SNDDEV_BSMT2000 #define SNDDEV_ICS2115 +#define SNDDEV_YM2414 #endif #ifdef SNDDEV_SN76496 @@ -190,6 +191,9 @@ #ifdef SNDDEV_ICS2115 #include "cores/ics2115.h" #endif +#ifdef SNDDEV_YM2414 +#include "cores/ymfmintf.h" +#endif const DEV_DECL* sndEmu_Devices[] = { #ifdef SNDDEV_SN76496 @@ -335,6 +339,9 @@ const DEV_DECL* sndEmu_Devices[] = { #endif #ifdef SNDDEV_ICS2115 &sndDev_ICS2115, +#endif +#ifdef SNDDEV_YM2414 + &sndDev_YM2414, #endif NULL // list end }; diff --git a/emu/cores/ymfm/README.md b/emu/cores/ymfm/README.md new file mode 100644 index 00000000..c65cba94 --- /dev/null +++ b/emu/cores/ymfm/README.md @@ -0,0 +1,26 @@ +# ymfm - Yamaha FM sound cores + +This directory contains vendored source files from the ymfm project. + +## Source +- **Project**: ymfm (Yamaha FM sound cores) +- **Author**: Aaron Giles +- **Repository**: https://github.com/aaronsgiles/ymfm +- **Commit**: 17decfae857b92ab55fbb30ade2287ace095a381 +- **Date**: 2025-12-31 + +## License +BSD-3-Clause + +Copyright (c) 2020-2021 Aaron Giles + +See the header comments in each file for full license text. + +## Files +- `ymfm.h` - Base classes and core infrastructure +- `ymfm_opz.h` - YM2414 (OPZ) implementation +- `ymfm_fm.h` - FM engine base classes +- `ymfm_fm.ipp` - FM engine template implementations + +## Integration +These files are used by `ymfmintf.cpp` to provide YM2414 (OPZ) emulation support in libvgm. diff --git a/emu/cores/ymfm/ymfm.h b/emu/cores/ymfm/ymfm.h new file mode 100644 index 00000000..6427e34c --- /dev/null +++ b/emu/cores/ymfm/ymfm.h @@ -0,0 +1,566 @@ +// BSD 3-Clause License +// +// Copyright (c) 2021, Aaron Giles +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef YMFM_H +#define YMFM_H + +#pragma once + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) + #define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ymfm +{ + +//********************************************************* +// DEBUGGING +//********************************************************* + +class debug +{ +public: + // masks to help isolate specific channels + static constexpr uint32_t GLOBAL_FM_CHANNEL_MASK = 0xffffffff; + static constexpr uint32_t GLOBAL_ADPCM_A_CHANNEL_MASK = 0xffffffff; + static constexpr uint32_t GLOBAL_ADPCM_B_CHANNEL_MASK = 0xffffffff; + static constexpr uint32_t GLOBAL_PCM_CHANNEL_MASK = 0xffffffff; + + // types of logging + static constexpr bool LOG_FM_WRITES = false; + static constexpr bool LOG_KEYON_EVENTS = false; + static constexpr bool LOG_UNEXPECTED_READ_WRITES = false; + + // helpers to write based on the log type + template static void log_fm_write(Params &&... args) { if (LOG_FM_WRITES) log(args...); } + template static void log_keyon(Params &&... args) { if (LOG_KEYON_EVENTS) log(args...); } + template static void log_unexpected_read_write(Params &&... args) { if (LOG_UNEXPECTED_READ_WRITES) log(args...); } + + // downstream helper to output log data; defaults to printf + template static void log(Params &&... args) { printf(args...); } +}; + + + +//********************************************************* +// GLOBAL HELPERS +//********************************************************* + +//------------------------------------------------- +// bitfield - extract a bitfield from the given +// value, starting at bit 'start' for a length of +// 'length' bits +//------------------------------------------------- + +inline uint32_t bitfield(uint32_t value, int start, int length = 1) +{ + return (value >> start) & ((1 << length) - 1); +} + + +//------------------------------------------------- +// clamp - clamp between the minimum and maximum +// values provided +//------------------------------------------------- + +inline int32_t clamp(int32_t value, int32_t minval, int32_t maxval) +{ + if (value < minval) + return minval; + if (value > maxval) + return maxval; + return value; +} + + +//------------------------------------------------- +// count_leading_zeros - return the number of +// leading zeros in a 32-bit value; CPU-optimized +// versions for various architectures are included +// below +//------------------------------------------------- + +#if defined(__GNUC__) + +inline uint8_t count_leading_zeros(uint32_t value) +{ + if (value == 0) + return 32; + return __builtin_clz(value); +} + +#elif defined(_MSC_VER) + +inline uint8_t count_leading_zeros(uint32_t value) +{ + unsigned long index; + return _BitScanReverse(&index, value) ? uint8_t(31U - index) : 32U; +} + +#else + +inline uint8_t count_leading_zeros(uint32_t value) +{ + if (value == 0) + return 32; + uint8_t count; + for (count = 0; int32_t(value) >= 0; count++) + value <<= 1; + return count; +} + +#endif + + +// Many of the Yamaha FM chips emit a floating-point value, which is sent to +// a DAC for processing. The exact format of this floating-point value is +// documented below. This description only makes sense if the "internal" +// format treats sign as 1=positive and 0=negative, so the helpers below +// presume that. +// +// Internal OPx data 16-bit signed data Exp Sign Mantissa +// ================= ================= === ==== ======== +// 1 1xxxxxxxx------ -> 0 1xxxxxxxx------ -> 111 1 1xxxxxxx +// 1 01xxxxxxxx----- -> 0 01xxxxxxxx----- -> 110 1 1xxxxxxx +// 1 001xxxxxxxx---- -> 0 001xxxxxxxx---- -> 101 1 1xxxxxxx +// 1 0001xxxxxxxx--- -> 0 0001xxxxxxxx--- -> 100 1 1xxxxxxx +// 1 00001xxxxxxxx-- -> 0 00001xxxxxxxx-- -> 011 1 1xxxxxxx +// 1 000001xxxxxxxx- -> 0 000001xxxxxxxx- -> 010 1 1xxxxxxx +// 1 000000xxxxxxxxx -> 0 000000xxxxxxxxx -> 001 1 xxxxxxxx +// 0 111111xxxxxxxxx -> 1 111111xxxxxxxxx -> 001 0 xxxxxxxx +// 0 111110xxxxxxxx- -> 1 111110xxxxxxxx- -> 010 0 0xxxxxxx +// 0 11110xxxxxxxx-- -> 1 11110xxxxxxxx-- -> 011 0 0xxxxxxx +// 0 1110xxxxxxxx--- -> 1 1110xxxxxxxx--- -> 100 0 0xxxxxxx +// 0 110xxxxxxxx---- -> 1 110xxxxxxxx---- -> 101 0 0xxxxxxx +// 0 10xxxxxxxx----- -> 1 10xxxxxxxx----- -> 110 0 0xxxxxxx +// 0 0xxxxxxxx------ -> 1 0xxxxxxxx------ -> 111 0 0xxxxxxx + +//------------------------------------------------- +// encode_fp - given a 32-bit signed input value +// convert it to a signed 3.10 floating-point +// value +//------------------------------------------------- + +inline int16_t encode_fp(int32_t value) +{ + // handle overflows first + if (value < -32768) + return (7 << 10) | 0x000; + if (value > 32767) + return (7 << 10) | 0x3ff; + + // we need to count the number of leading sign bits after the sign + // we can use count_leading_zeros if we invert negative values + int32_t scanvalue = value ^ (int32_t(value) >> 31); + + // exponent is related to the number of leading bits starting from bit 14 + int exponent = 7 - count_leading_zeros(scanvalue << 17); + + // smallest exponent value allowed is 1 + exponent = std::max(exponent, 1); + + // mantissa + int32_t mantissa = value >> (exponent - 1); + + // assemble into final form, inverting the sign + return ((exponent << 10) | (mantissa & 0x3ff)) ^ 0x200; +} + + +//------------------------------------------------- +// decode_fp - given a 3.10 floating-point value, +// convert it to a signed 16-bit value +//------------------------------------------------- + +inline int16_t decode_fp(int16_t value) +{ + // invert the sign and the exponent + value ^= 0x1e00; + + // shift mantissa up to 16 bits then apply inverted exponent + return int16_t(value << 6) >> bitfield(value, 10, 3); +} + + +//------------------------------------------------- +// roundtrip_fp - compute the result of a round +// trip through the encode/decode process above +//------------------------------------------------- + +inline int16_t roundtrip_fp(int32_t value) +{ + // handle overflows first + if (value < -32768) + return -32768; + if (value > 32767) + return 32767; + + // we need to count the number of leading sign bits after the sign + // we can use count_leading_zeros if we invert negative values + int32_t scanvalue = value ^ (int32_t(value) >> 31); + + // exponent is related to the number of leading bits starting from bit 14 + int exponent = 7 - count_leading_zeros(scanvalue << 17); + + // smallest exponent value allowed is 1 + exponent = std::max(exponent, 1); + + // apply the shift back and forth to zero out bits that are lost + exponent -= 1; + int32_t mask = (1 << exponent) - 1; + return value & ~mask; +} + + + +//********************************************************* +// HELPER CLASSES +//********************************************************* + +// various envelope states +enum envelope_state : uint32_t +{ + EG_DEPRESS = 0, // OPLL only; set EG_HAS_DEPRESS to enable + EG_ATTACK = 1, + EG_DECAY = 2, + EG_SUSTAIN = 3, + EG_RELEASE = 4, + EG_REVERB = 5, // OPQ/OPZ only; set EG_HAS_REVERB to enable + EG_STATES = 6 +}; + +// external I/O access classes +enum access_class : uint32_t +{ + ACCESS_IO = 0, + ACCESS_ADPCM_A, + ACCESS_ADPCM_B, + ACCESS_PCM, + ACCESS_CLASSES +}; + + + +//********************************************************* +// HELPER CLASSES +//********************************************************* + +// ======================> ymfm_output + +// struct containing an array of output values +template +struct ymfm_output +{ + // clear all outputs to 0 + ymfm_output &clear() + { + for (uint32_t index = 0; index < NumOutputs; index++) + data[index] = 0; + return *this; + } + + // clamp all outputs to a 16-bit signed value + ymfm_output &clamp16() + { + for (uint32_t index = 0; index < NumOutputs; index++) + data[index] = clamp(data[index], -32768, 32767); + return *this; + } + + // run each output value through the floating-point processor + ymfm_output &roundtrip_fp() + { + for (uint32_t index = 0; index < NumOutputs; index++) + data[index] = ymfm::roundtrip_fp(data[index]); + return *this; + } + + // internal state + int32_t data[NumOutputs]; +}; + + +// ======================> ymfm_wavfile + +// this class is a debugging helper that accumulates data and writes it to wav files +template +class ymfm_wavfile +{ +public: + // construction + ymfm_wavfile(uint32_t samplerate = 44100) : + m_samplerate(samplerate) + { + } + + // configuration + ymfm_wavfile &set_index(uint32_t index) { m_index = index; return *this; } + ymfm_wavfile &set_samplerate(uint32_t samplerate) { m_samplerate = samplerate; return *this; } + + // destruction + ~ymfm_wavfile() + { + if (!m_buffer.empty()) + { + // create file + char name[20]; + snprintf(&name[0], sizeof(name), "wavlog-%02d.wav", m_index); + FILE *out = fopen(name, "wb"); + + // make the wav file header + uint8_t header[44]; + memcpy(&header[0], "RIFF", 4); + *(uint32_t *)&header[4] = m_buffer.size() * 2 + 44 - 8; + memcpy(&header[8], "WAVE", 4); + memcpy(&header[12], "fmt ", 4); + *(uint32_t *)&header[16] = 16; + *(uint16_t *)&header[20] = 1; + *(uint16_t *)&header[22] = Channels; + *(uint32_t *)&header[24] = m_samplerate; + *(uint32_t *)&header[28] = m_samplerate * 2 * Channels; + *(uint16_t *)&header[32] = 2 * Channels; + *(uint16_t *)&header[34] = 16; + memcpy(&header[36], "data", 4); + *(uint32_t *)&header[40] = m_buffer.size() * 2 + 44 - 44; + + // write header then data + fwrite(&header[0], 1, sizeof(header), out); + fwrite(&m_buffer[0], 2, m_buffer.size(), out); + fclose(out); + } + } + + // add data to the file + template + void add(ymfm_output output) + { + int16_t sum[Channels] = { 0 }; + for (int index = 0; index < Outputs; index++) + sum[index % Channels] += output.data[index]; + for (int index = 0; index < Channels; index++) + m_buffer.push_back(sum[index]); + } + + // add data to the file, using a reference + template + void add(ymfm_output output, ymfm_output const &ref) + { + int16_t sum[Channels] = { 0 }; + for (int index = 0; index < Outputs; index++) + sum[index % Channels] += output.data[index] - ref.data[index]; + for (int index = 0; index < Channels; index++) + m_buffer.push_back(sum[index]); + } + +private: + // internal state + uint32_t m_index; + uint32_t m_samplerate; + std::vector m_buffer; +}; + + +// ======================> ymfm_saved_state + +// this class contains a managed vector of bytes that is used to save and +// restore state +class ymfm_saved_state +{ +public: + // construction + ymfm_saved_state(std::vector &buffer, bool saving) : + m_buffer(buffer), + m_offset(saving ? -1 : 0) + { + if (saving) + buffer.resize(0); + } + + // are we saving or restoring? + bool saving() const { return (m_offset < 0); } + + // generic save/restore + template + void save_restore(DataType &data) + { + if (saving()) + save(data); + else + restore(data); + } + +public: + // save data to the buffer + void save(bool &data) { write(data ? 1 : 0); } + void save(int8_t &data) { write(data); } + void save(uint8_t &data) { write(data); } + void save(int16_t &data) { write(uint8_t(data)).write(data >> 8); } + void save(uint16_t &data) { write(uint8_t(data)).write(data >> 8); } + void save(int32_t &data) { write(data).write(data >> 8).write(data >> 16).write(data >> 24); } + void save(uint32_t &data) { write(data).write(data >> 8).write(data >> 16).write(data >> 24); } + void save(envelope_state &data) { write(uint8_t(data)); } + template + void save(DataType (&data)[Count]) { for (uint32_t index = 0; index < Count; index++) save(data[index]); } + + // restore data from the buffer + void restore(bool &data) { data = read() ? true : false; } + void restore(int8_t &data) { data = read(); } + void restore(uint8_t &data) { data = read(); } + void restore(int16_t &data) { data = read(); data |= read() << 8; } + void restore(uint16_t &data) { data = read(); data |= read() << 8; } + void restore(int32_t &data) { data = read(); data |= read() << 8; data |= read() << 16; data |= read() << 24; } + void restore(uint32_t &data) { data = read(); data |= read() << 8; data |= read() << 16; data |= read() << 24; } + void restore(envelope_state &data) { data = envelope_state(read()); } + template + void restore(DataType (&data)[Count]) { for (uint32_t index = 0; index < Count; index++) restore(data[index]); } + + // internal helper + ymfm_saved_state &write(uint8_t data) { m_buffer.push_back(data); return *this; } + uint8_t read() { return (m_offset < int32_t(m_buffer.size())) ? m_buffer[m_offset++] : 0; } + + // internal state + std::vector &m_buffer; + int32_t m_offset; +}; + + + +//********************************************************* +// INTERFACE CLASSES +//********************************************************* + +// ======================> ymfm_engine_callbacks + +// this class represents functions in the engine that the ymfm_interface +// needs to be able to call; it is represented here as a separate interface +// that is independent of the actual engine implementation +class ymfm_engine_callbacks +{ +public: + virtual ~ymfm_engine_callbacks() = default; + + // timer callback; called by the interface when a timer fires + virtual void engine_timer_expired(uint32_t tnum) = 0; + + // check interrupts; called by the interface after synchronization + virtual void engine_check_interrupts() = 0; + + // mode register write; called by the interface after synchronization + virtual void engine_mode_write(uint8_t data) = 0; +}; + + +// ======================> ymfm_interface + +// this class represents the interface between the fm_engine and the outside +// world; it provides hooks for timers, synchronization, and I/O +class ymfm_interface +{ + // the engine is our friend + template friend class fm_engine_base; + +public: + virtual ~ymfm_interface() = default; + + // the following functions must be implemented by any derived classes; the + // default implementations are sufficient for some minimal operation, but will + // likely need to be overridden to integrate with the outside world; they are + // all prefixed with ymfm_ to reduce the likelihood of namespace collisions + + // + // timing and synchronizaton + // + + // the chip implementation calls this when a write happens to the mode + // register, which could affect timers and interrupts; our responsibility + // is to ensure the system is up to date before calling the engine's + // engine_mode_write() method + virtual void ymfm_sync_mode_write(uint8_t data) { m_engine->engine_mode_write(data); } + + // the chip implementation calls this when the chip's status has changed, + // which may affect the interrupt state; our responsibility is to ensure + // the system is up to date before calling the engine's + // engine_check_interrupts() method + virtual void ymfm_sync_check_interrupts() { m_engine->engine_check_interrupts(); } + + // the chip implementation calls this when one of the two internal timers + // has changed state; our responsibility is to arrange to call the engine's + // engine_timer_expired() method after the provided number of clocks; if + // duration_in_clocks is negative, we should cancel any outstanding timers + virtual void ymfm_set_timer(uint32_t tnum, int32_t duration_in_clocks) { } + + // the chip implementation calls this to indicate that the chip should be + // considered in a busy state until the given number of clocks has passed; + // our responsibility is to compute and remember the ending time based on + // the chip's clock for later checking + virtual void ymfm_set_busy_end(uint32_t clocks) { } + + // the chip implementation calls this to see if the chip is still currently + // is a busy state, as specified by a previous call to ymfm_set_busy_end(); + // our responsibility is to compare the current time against the previously + // noted busy end time and return true if we haven't yet passed it + virtual bool ymfm_is_busy() { return false; } + + // + // I/O functions + // + + // the chip implementation calls this when the state of the IRQ signal has + // changed due to a status change; our responsibility is to respond as + // needed to the change in IRQ state, signaling any consumers + virtual void ymfm_update_irq(bool asserted) { } + + // the chip implementation calls this whenever data is read from outside + // of the chip; our responsibility is to provide the data requested + virtual uint8_t ymfm_external_read(access_class type, uint32_t address) { return 0; } + + // the chip implementation calls this whenever data is written outside + // of the chip; our responsibility is to pass the written data on to any consumers + virtual void ymfm_external_write(access_class type, uint32_t address, uint8_t data) { } + +protected: + // pointer to engine callbacks -- this is set directly by the engine at + // construction time + ymfm_engine_callbacks *m_engine; +}; + +} + +#endif // YMFM_H diff --git a/emu/cores/ymfm/ymfm_fm.h b/emu/cores/ymfm/ymfm_fm.h new file mode 100644 index 00000000..d40409fd --- /dev/null +++ b/emu/cores/ymfm/ymfm_fm.h @@ -0,0 +1,463 @@ +// BSD 3-Clause License +// +// Copyright (c) 2021, Aaron Giles +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef YMFM_FM_H +#define YMFM_FM_H + +#pragma once + +#define YMFM_DEBUG_LOG_WAVFILES (0) + +namespace ymfm +{ + +//********************************************************* +// GLOBAL ENUMERATORS +//********************************************************* + +// three different keyon sources; actual keyon is an OR over all of these +enum keyon_type : uint32_t +{ + KEYON_NORMAL = 0, + KEYON_RHYTHM = 1, + KEYON_CSM = 2 +}; + + + +//********************************************************* +// CORE IMPLEMENTATION +//********************************************************* + +// ======================> opdata_cache + +// this class holds data that is computed once at the start of clocking +// and remains static during subsequent sound generation +struct opdata_cache +{ + // set phase_step to this value to recalculate it each sample; needed + // in the case of PM LFO changes + static constexpr uint32_t PHASE_STEP_DYNAMIC = 1; + + uint16_t const *waveform; // base of sine table + uint32_t phase_step; // phase step, or PHASE_STEP_DYNAMIC if PM is active + uint32_t total_level; // total level * 8 + KSL + uint32_t block_freq; // raw block frequency value (used to compute phase_step) + int32_t detune; // detuning value (used to compute phase_step) + uint32_t multiple; // multiple value (x.1, used to compute phase_step) + uint32_t eg_sustain; // sustain level, shifted up to envelope values + uint8_t eg_rate[EG_STATES]; // envelope rate, including KSR + uint8_t eg_shift = 0; // envelope shift amount +}; + + +// ======================> fm_registers_base + +// base class for family-specific register classes; this provides a few +// constants, common defaults, and helpers, but mostly each derived class is +// responsible for defining all commonly-called methods +class fm_registers_base +{ +public: + // this value is returned from the write() function for rhythm channels + static constexpr uint32_t RHYTHM_CHANNEL = 0xff; + + // this is the size of a full sin waveform + static constexpr uint32_t WAVEFORM_LENGTH = 0x400; + + // + // the following constants need to be defined per family: + // uint32_t OUTPUTS: The number of outputs exposed (1-4) + // uint32_t CHANNELS: The number of channels on the chip + // uint32_t ALL_CHANNELS: A bitmask of all channels + // uint32_t OPERATORS: The number of operators on the chip + // uint32_t WAVEFORMS: The number of waveforms offered + // uint32_t REGISTERS: The number of 8-bit registers allocated + // uint32_t DEFAULT_PRESCALE: The starting clock prescale + // uint32_t EG_CLOCK_DIVIDER: The clock divider of the envelope generator + // uint32_t CSM_TRIGGER_MASK: Mask of channels to trigger in CSM mode + // uint32_t REG_MODE: The address of the "mode" register controlling timers + // uint8_t STATUS_TIMERA: Status bit to set when timer A fires + // uint8_t STATUS_TIMERB: Status bit to set when tiemr B fires + // uint8_t STATUS_BUSY: Status bit to set when the chip is busy + // uint8_t STATUS_IRQ: Status bit to set when an IRQ is signalled + // + // the following constants are uncommon: + // bool DYNAMIC_OPS: True if ops/channel can be changed at runtime (OPL3+) + // bool EG_HAS_DEPRESS: True if the chip has a DP ("depress"?) envelope stage (OPLL) + // bool EG_HAS_REVERB: True if the chip has a faux reverb envelope stage (OPQ/OPZ) + // bool EG_HAS_SSG: True if the chip has SSG envelope support (OPN) + // bool MODULATOR_DELAY: True if the modulator is delayed by 1 sample (OPL pre-OPL3) + // + static constexpr bool DYNAMIC_OPS = false; + static constexpr bool EG_HAS_DEPRESS = false; + static constexpr bool EG_HAS_REVERB = false; + static constexpr bool EG_HAS_SSG = false; + static constexpr bool MODULATOR_DELAY = false; + + // system-wide register defaults + uint32_t status_mask() const { return 0; } // OPL only + uint32_t irq_reset() const { return 0; } // OPL only + uint32_t noise_enable() const { return 0; } // OPM only + uint32_t rhythm_enable() const { return 0; } // OPL only + + // per-operator register defaults + uint32_t op_ssg_eg_enable(uint32_t opoffs) const { return 0; } // OPN(A) only + uint32_t op_ssg_eg_mode(uint32_t opoffs) const { return 0; } // OPN(A) only + +protected: + // helper to encode four operator numbers into a 32-bit value in the + // operator maps for each register class + static constexpr uint32_t operator_list(uint8_t o1 = 0xff, uint8_t o2 = 0xff, uint8_t o3 = 0xff, uint8_t o4 = 0xff) + { + return o1 | (o2 << 8) | (o3 << 16) | (o4 << 24); + } + + // helper to apply KSR to the raw ADSR rate, ignoring ksr if the + // raw value is 0, and clamping to 63 + static constexpr uint32_t effective_rate(uint32_t rawrate, uint32_t ksr) + { + return (rawrate == 0) ? 0 : std::min(rawrate + ksr, 63); + } +}; + + + +//********************************************************* +// CORE ENGINE CLASSES +//********************************************************* + +// forward declarations +template class fm_engine_base; + +// ======================> fm_operator + +// fm_operator represents an FM operator (or "slot" in FM parlance), which +// produces an output sine wave modulated by an envelope +template +class fm_operator +{ + // "quiet" value, used to optimize when we can skip doing work + static constexpr uint32_t EG_QUIET = 0x380; + +public: + // constructor + fm_operator(fm_engine_base &owner, uint32_t opoffs); + + // save/restore + void save_restore(ymfm_saved_state &state); + + // reset the operator state + void reset(); + + // return the operator/channel offset + uint32_t opoffs() const { return m_opoffs; } + uint32_t choffs() const { return m_choffs; } + + // set the current channel + void set_choffs(uint32_t choffs) { m_choffs = choffs; } + + // prepare prior to clocking + bool prepare(); + + // master clocking function + void clock(uint32_t env_counter, int32_t lfo_raw_pm); + + // return the current phase value + uint32_t phase() const { return m_phase >> 10; } + + // compute operator volume + int32_t compute_volume(uint32_t phase, uint32_t am_offset) const; + + // compute volume for the OPM noise channel + int32_t compute_noise_volume(uint32_t am_offset) const; + + // key state control + void keyonoff(uint32_t on, keyon_type type); + + // return a reference to our registers + RegisterType ®s() const { return m_regs; } + + // simple getters for debugging + envelope_state debug_eg_state() const { return m_env_state; } + uint16_t debug_eg_attenuation() const { return m_env_attenuation; } + uint8_t debug_ssg_inverted() const { return m_ssg_inverted; } + opdata_cache &debug_cache() { return m_cache; } + +private: + // start the attack phase + void start_attack(bool is_restart = false); + + // start the release phase + void start_release(); + + // clock phases + void clock_keystate(uint32_t keystate); + void clock_ssg_eg_state(); + void clock_envelope(uint32_t env_counter); + void clock_phase(int32_t lfo_raw_pm); + + // return effective attenuation of the envelope + uint32_t envelope_attenuation(uint32_t am_offset) const; + + // internal state + uint32_t m_choffs; // channel offset in registers + uint32_t m_opoffs; // operator offset in registers + uint32_t m_phase; // current phase value (10.10 format) + uint16_t m_env_attenuation; // computed envelope attenuation (4.6 format) + envelope_state m_env_state; // current envelope state + uint8_t m_ssg_inverted; // non-zero if the output should be inverted (bit 0) + uint8_t m_key_state; // current key state: on or off (bit 0) + uint8_t m_keyon_live; // live key on state (bit 0 = direct, bit 1 = rhythm, bit 2 = CSM) + opdata_cache m_cache; // cached values for performance + RegisterType &m_regs; // direct reference to registers + fm_engine_base &m_owner; // reference to the owning engine +}; + + +// ======================> fm_channel + +// fm_channel represents an FM channel which combines the output of 2 or 4 +// operators into a final result +template +class fm_channel +{ + using output_data = ymfm_output; + +public: + // constructor + fm_channel(fm_engine_base &owner, uint32_t choffs); + + // save/restore + void save_restore(ymfm_saved_state &state); + + // reset the channel state + void reset(); + + // return the channel offset + uint32_t choffs() const { return m_choffs; } + + // assign operators + void assign(uint32_t index, fm_operator *op) + { + assert(index < m_op.size()); + m_op[index] = op; + if (op != nullptr) + op->set_choffs(m_choffs); + } + + // signal key on/off to our operators + void keyonoff(uint32_t states, keyon_type type, uint32_t chnum); + + // prepare prior to clocking + bool prepare(); + + // master clocking function + void clock(uint32_t env_counter, int32_t lfo_raw_pm); + + // specific 2-operator and 4-operator output handlers + void output_2op(output_data &output, uint32_t rshift, int32_t clipmax) const; + void output_4op(output_data &output, uint32_t rshift, int32_t clipmax) const; + + // compute the special OPL rhythm channel outputs + void output_rhythm_ch6(output_data &output, uint32_t rshift, int32_t clipmax) const; + void output_rhythm_ch7(uint32_t phase_select, output_data &output, uint32_t rshift, int32_t clipmax) const; + void output_rhythm_ch8(uint32_t phase_select, output_data &output, uint32_t rshift, int32_t clipmax) const; + + // are we a 4-operator channel or a 2-operator one? + bool is4op() const + { + if (RegisterType::DYNAMIC_OPS) + return (m_op[2] != nullptr); + return (RegisterType::OPERATORS / RegisterType::CHANNELS == 4); + } + + // return a reference to our registers + RegisterType ®s() const { return m_regs; } + + // simple getters for debugging + fm_operator *debug_operator(uint32_t index) const { return m_op[index]; } + +private: + // helper to add values to the outputs based on channel enables + void add_to_output(uint32_t choffs, output_data &output, int32_t value) const + { + // create these constants to appease overzealous compilers checking array + // bounds in unreachable code (looking at you, clang) + constexpr int out0_index = 0; + constexpr int out1_index = 1 % RegisterType::OUTPUTS; + constexpr int out2_index = 2 % RegisterType::OUTPUTS; + constexpr int out3_index = 3 % RegisterType::OUTPUTS; + + if (RegisterType::OUTPUTS == 1 || m_regs.ch_output_0(choffs)) + output.data[out0_index] += value; + if (RegisterType::OUTPUTS >= 2 && m_regs.ch_output_1(choffs)) + output.data[out1_index] += value; + if (RegisterType::OUTPUTS >= 3 && m_regs.ch_output_2(choffs)) + output.data[out2_index] += value; + if (RegisterType::OUTPUTS >= 4 && m_regs.ch_output_3(choffs)) + output.data[out3_index] += value; + } + + // internal state + uint32_t m_choffs; // channel offset in registers + int16_t m_feedback[2]; // feedback memory for operator 1 + mutable int16_t m_feedback_in; // next input value for op 1 feedback (set in output) + std::array *, 4> m_op; // up to 4 operators + RegisterType &m_regs; // direct reference to registers + fm_engine_base &m_owner; // reference to the owning engine +}; + + +// ======================> fm_engine_base + +// fm_engine_base represents a set of operators and channels which together +// form a Yamaha FM core; chips that implement other engines (ADPCM, wavetable, +// etc) take this output and combine it with the others externally +template +class fm_engine_base : public ymfm_engine_callbacks +{ +public: + // expose some constants from the registers + static constexpr uint32_t OUTPUTS = RegisterType::OUTPUTS; + static constexpr uint32_t CHANNELS = RegisterType::CHANNELS; + static constexpr uint32_t ALL_CHANNELS = RegisterType::ALL_CHANNELS; + static constexpr uint32_t OPERATORS = RegisterType::OPERATORS; + + // also expose status flags for consumers that inject additional bits + static constexpr uint8_t STATUS_TIMERA = RegisterType::STATUS_TIMERA; + static constexpr uint8_t STATUS_TIMERB = RegisterType::STATUS_TIMERB; + static constexpr uint8_t STATUS_BUSY = RegisterType::STATUS_BUSY; + static constexpr uint8_t STATUS_IRQ = RegisterType::STATUS_IRQ; + + // expose the correct output class + using output_data = ymfm_output; + + // constructor + fm_engine_base(ymfm_interface &intf); + + // save/restore + void save_restore(ymfm_saved_state &state); + + // reset the overall state + void reset(); + + // master clocking function + uint32_t clock(uint32_t chanmask); + + // compute sum of channel outputs + void output(output_data &output, uint32_t rshift, int32_t clipmax, uint32_t chanmask) const; + + // write to the OPN registers + void write(uint16_t regnum, uint8_t data); + + // return the current status + uint8_t status() const; + + // set/reset bits in the status register, updating the IRQ status + uint8_t set_reset_status(uint8_t set, uint8_t reset) + { + m_status = (m_status | set) & ~(reset | STATUS_BUSY); + m_intf.ymfm_sync_check_interrupts(); + return m_status & ~m_regs.status_mask(); + } + + // set the IRQ mask + void set_irq_mask(uint8_t mask) { m_irq_mask = mask; m_intf.ymfm_sync_check_interrupts(); } + + // return the current clock prescale + uint32_t clock_prescale() const { return m_clock_prescale; } + + // set prescale factor (2/3/6) + void set_clock_prescale(uint32_t prescale) { m_clock_prescale = prescale; } + + // compute sample rate + uint32_t sample_rate(uint32_t baseclock) const + { +#if (YMFM_DEBUG_LOG_WAVFILES) + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + m_wavfile[chnum].set_samplerate(baseclock / (m_clock_prescale * OPERATORS)); +#endif + return baseclock / (m_clock_prescale * OPERATORS); + } + + // return the owning device + ymfm_interface &intf() const { return m_intf; } + + // return a reference to our registers + RegisterType ®s() { return m_regs; } + + // invalidate any caches + void invalidate_caches() { m_modified_channels = RegisterType::ALL_CHANNELS; } + + // simple getters for debugging + fm_channel *debug_channel(uint32_t index) const { return m_channel[index].get(); } + fm_operator *debug_operator(uint32_t index) const { return m_operator[index].get(); } + +public: + // timer callback; called by the interface when a timer fires + virtual void engine_timer_expired(uint32_t tnum) override; + + // check interrupts; called by the interface after synchronization + virtual void engine_check_interrupts() override; + + // mode register write; called by the interface after synchronization + virtual void engine_mode_write(uint8_t data) override; + +protected: + // assign the current set of operators to channels + void assign_operators(); + + // update the state of the given timer + void update_timer(uint32_t which, uint32_t enable, int32_t delta_clocks); + + // internal state + ymfm_interface &m_intf; // reference to the system interface + uint32_t m_env_counter; // envelope counter; low 2 bits are sub-counter + uint8_t m_status; // current status register + uint8_t m_clock_prescale; // prescale factor (2/3/6) + uint8_t m_irq_mask; // mask of which bits signal IRQs + uint8_t m_irq_state; // current IRQ state + uint8_t m_timer_running[2]; // current timer running state + uint8_t m_total_clocks; // low 8 bits of the total number of clocks processed + uint32_t m_active_channels; // mask of active channels (computed by prepare) + uint32_t m_modified_channels; // mask of channels that have been modified + uint32_t m_prepare_count; // counter to do periodic prepare sweeps + RegisterType m_regs; // register accessor + std::unique_ptr> m_channel[CHANNELS]; // channel pointers + std::unique_ptr> m_operator[OPERATORS]; // operator pointers +#if (YMFM_DEBUG_LOG_WAVFILES) + mutable ymfm_wavfile<1> m_wavfile[CHANNELS]; // for debugging +#endif +}; + +} + +#endif // YMFM_FM_H diff --git a/emu/cores/ymfm/ymfm_fm.ipp b/emu/cores/ymfm/ymfm_fm.ipp new file mode 100644 index 00000000..67542483 --- /dev/null +++ b/emu/cores/ymfm/ymfm_fm.ipp @@ -0,0 +1,1592 @@ +// BSD 3-Clause License +// +// Copyright (c) 2021, Aaron Giles +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +namespace ymfm +{ + +//********************************************************* +// GLOBAL TABLE LOOKUPS +//********************************************************* + +//------------------------------------------------- +// abs_sin_attenuation - given a sin (phase) input +// where the range 0-2*PI is mapped onto 10 bits, +// return the absolute value of sin(input), +// logarithmically-adjusted and treated as an +// attenuation value, in 4.8 fixed point format +//------------------------------------------------- + +inline uint32_t abs_sin_attenuation(uint32_t input) +{ + // the values here are stored as 4.8 logarithmic values for 1/4 phase + // this matches the internal format of the OPN chip, extracted from the die + static uint16_t const s_sin_table[256] = + { + 0x859,0x6c3,0x607,0x58b,0x52e,0x4e4,0x4a6,0x471,0x443,0x41a,0x3f5,0x3d3,0x3b5,0x398,0x37e,0x365, + 0x34e,0x339,0x324,0x311,0x2ff,0x2ed,0x2dc,0x2cd,0x2bd,0x2af,0x2a0,0x293,0x286,0x279,0x26d,0x261, + 0x256,0x24b,0x240,0x236,0x22c,0x222,0x218,0x20f,0x206,0x1fd,0x1f5,0x1ec,0x1e4,0x1dc,0x1d4,0x1cd, + 0x1c5,0x1be,0x1b7,0x1b0,0x1a9,0x1a2,0x19b,0x195,0x18f,0x188,0x182,0x17c,0x177,0x171,0x16b,0x166, + 0x160,0x15b,0x155,0x150,0x14b,0x146,0x141,0x13c,0x137,0x133,0x12e,0x129,0x125,0x121,0x11c,0x118, + 0x114,0x10f,0x10b,0x107,0x103,0x0ff,0x0fb,0x0f8,0x0f4,0x0f0,0x0ec,0x0e9,0x0e5,0x0e2,0x0de,0x0db, + 0x0d7,0x0d4,0x0d1,0x0cd,0x0ca,0x0c7,0x0c4,0x0c1,0x0be,0x0bb,0x0b8,0x0b5,0x0b2,0x0af,0x0ac,0x0a9, + 0x0a7,0x0a4,0x0a1,0x09f,0x09c,0x099,0x097,0x094,0x092,0x08f,0x08d,0x08a,0x088,0x086,0x083,0x081, + 0x07f,0x07d,0x07a,0x078,0x076,0x074,0x072,0x070,0x06e,0x06c,0x06a,0x068,0x066,0x064,0x062,0x060, + 0x05e,0x05c,0x05b,0x059,0x057,0x055,0x053,0x052,0x050,0x04e,0x04d,0x04b,0x04a,0x048,0x046,0x045, + 0x043,0x042,0x040,0x03f,0x03e,0x03c,0x03b,0x039,0x038,0x037,0x035,0x034,0x033,0x031,0x030,0x02f, + 0x02e,0x02d,0x02b,0x02a,0x029,0x028,0x027,0x026,0x025,0x024,0x023,0x022,0x021,0x020,0x01f,0x01e, + 0x01d,0x01c,0x01b,0x01a,0x019,0x018,0x017,0x017,0x016,0x015,0x014,0x014,0x013,0x012,0x011,0x011, + 0x010,0x00f,0x00f,0x00e,0x00d,0x00d,0x00c,0x00c,0x00b,0x00a,0x00a,0x009,0x009,0x008,0x008,0x007, + 0x007,0x007,0x006,0x006,0x005,0x005,0x005,0x004,0x004,0x004,0x003,0x003,0x003,0x002,0x002,0x002, + 0x002,0x001,0x001,0x001,0x001,0x001,0x001,0x001,0x000,0x000,0x000,0x000,0x000,0x000,0x000,0x000 + }; + + // if the top bit is set, we're in the second half of the curve + // which is a mirror image, so invert the index + if (bitfield(input, 8)) + input = ~input; + + // return the value from the table + return s_sin_table[input & 0xff]; +} + + +//------------------------------------------------- +// attenuation_to_volume - given a 5.8 fixed point +// logarithmic attenuation value, return a 13-bit +// linear volume +//------------------------------------------------- + +inline uint32_t attenuation_to_volume(uint32_t input) +{ + // the values here are 10-bit mantissas with an implied leading bit + // this matches the internal format of the OPN chip, extracted from the die + + // as a nod to performance, the implicit 0x400 bit is pre-incorporated, and + // the values are left-shifted by 2 so that a simple right shift is all that + // is needed; also the order is reversed to save a NOT on the input +#define X(a) (((a) | 0x400) << 2) + static uint16_t const s_power_table[256] = + { + X(0x3fa),X(0x3f5),X(0x3ef),X(0x3ea),X(0x3e4),X(0x3df),X(0x3da),X(0x3d4), + X(0x3cf),X(0x3c9),X(0x3c4),X(0x3bf),X(0x3b9),X(0x3b4),X(0x3ae),X(0x3a9), + X(0x3a4),X(0x39f),X(0x399),X(0x394),X(0x38f),X(0x38a),X(0x384),X(0x37f), + X(0x37a),X(0x375),X(0x370),X(0x36a),X(0x365),X(0x360),X(0x35b),X(0x356), + X(0x351),X(0x34c),X(0x347),X(0x342),X(0x33d),X(0x338),X(0x333),X(0x32e), + X(0x329),X(0x324),X(0x31f),X(0x31a),X(0x315),X(0x310),X(0x30b),X(0x306), + X(0x302),X(0x2fd),X(0x2f8),X(0x2f3),X(0x2ee),X(0x2e9),X(0x2e5),X(0x2e0), + X(0x2db),X(0x2d6),X(0x2d2),X(0x2cd),X(0x2c8),X(0x2c4),X(0x2bf),X(0x2ba), + X(0x2b5),X(0x2b1),X(0x2ac),X(0x2a8),X(0x2a3),X(0x29e),X(0x29a),X(0x295), + X(0x291),X(0x28c),X(0x288),X(0x283),X(0x27f),X(0x27a),X(0x276),X(0x271), + X(0x26d),X(0x268),X(0x264),X(0x25f),X(0x25b),X(0x257),X(0x252),X(0x24e), + X(0x249),X(0x245),X(0x241),X(0x23c),X(0x238),X(0x234),X(0x230),X(0x22b), + X(0x227),X(0x223),X(0x21e),X(0x21a),X(0x216),X(0x212),X(0x20e),X(0x209), + X(0x205),X(0x201),X(0x1fd),X(0x1f9),X(0x1f5),X(0x1f0),X(0x1ec),X(0x1e8), + X(0x1e4),X(0x1e0),X(0x1dc),X(0x1d8),X(0x1d4),X(0x1d0),X(0x1cc),X(0x1c8), + X(0x1c4),X(0x1c0),X(0x1bc),X(0x1b8),X(0x1b4),X(0x1b0),X(0x1ac),X(0x1a8), + X(0x1a4),X(0x1a0),X(0x19c),X(0x199),X(0x195),X(0x191),X(0x18d),X(0x189), + X(0x185),X(0x181),X(0x17e),X(0x17a),X(0x176),X(0x172),X(0x16f),X(0x16b), + X(0x167),X(0x163),X(0x160),X(0x15c),X(0x158),X(0x154),X(0x151),X(0x14d), + X(0x149),X(0x146),X(0x142),X(0x13e),X(0x13b),X(0x137),X(0x134),X(0x130), + X(0x12c),X(0x129),X(0x125),X(0x122),X(0x11e),X(0x11b),X(0x117),X(0x114), + X(0x110),X(0x10c),X(0x109),X(0x106),X(0x102),X(0x0ff),X(0x0fb),X(0x0f8), + X(0x0f4),X(0x0f1),X(0x0ed),X(0x0ea),X(0x0e7),X(0x0e3),X(0x0e0),X(0x0dc), + X(0x0d9),X(0x0d6),X(0x0d2),X(0x0cf),X(0x0cc),X(0x0c8),X(0x0c5),X(0x0c2), + X(0x0be),X(0x0bb),X(0x0b8),X(0x0b5),X(0x0b1),X(0x0ae),X(0x0ab),X(0x0a8), + X(0x0a4),X(0x0a1),X(0x09e),X(0x09b),X(0x098),X(0x094),X(0x091),X(0x08e), + X(0x08b),X(0x088),X(0x085),X(0x082),X(0x07e),X(0x07b),X(0x078),X(0x075), + X(0x072),X(0x06f),X(0x06c),X(0x069),X(0x066),X(0x063),X(0x060),X(0x05d), + X(0x05a),X(0x057),X(0x054),X(0x051),X(0x04e),X(0x04b),X(0x048),X(0x045), + X(0x042),X(0x03f),X(0x03c),X(0x039),X(0x036),X(0x033),X(0x030),X(0x02d), + X(0x02a),X(0x028),X(0x025),X(0x022),X(0x01f),X(0x01c),X(0x019),X(0x016), + X(0x014),X(0x011),X(0x00e),X(0x00b),X(0x008),X(0x006),X(0x003),X(0x000) + }; +#undef X + + // look up the fractional part, then shift by the whole + return s_power_table[input & 0xff] >> (input >> 8); +} + + +//------------------------------------------------- +// attenuation_increment - given a 6-bit ADSR +// rate value and a 3-bit stepping index, +// return a 4-bit increment to the attenutaion +// for this step (or for the attack case, the +// fractional scale factor to decrease by) +//------------------------------------------------- + +inline uint32_t attenuation_increment(uint32_t rate, uint32_t index) +{ + static uint32_t const s_increment_table[64] = + { + 0x00000000, 0x00000000, 0x10101010, 0x10101010, // 0-3 (0x00-0x03) + 0x10101010, 0x10101010, 0x11101110, 0x11101110, // 4-7 (0x04-0x07) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 8-11 (0x08-0x0B) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 12-15 (0x0C-0x0F) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 16-19 (0x10-0x13) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 20-23 (0x14-0x17) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 24-27 (0x18-0x1B) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 28-31 (0x1C-0x1F) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 32-35 (0x20-0x23) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 36-39 (0x24-0x27) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 40-43 (0x28-0x2B) + 0x10101010, 0x10111010, 0x11101110, 0x11111110, // 44-47 (0x2C-0x2F) + 0x11111111, 0x21112111, 0x21212121, 0x22212221, // 48-51 (0x30-0x33) + 0x22222222, 0x42224222, 0x42424242, 0x44424442, // 52-55 (0x34-0x37) + 0x44444444, 0x84448444, 0x84848484, 0x88848884, // 56-59 (0x38-0x3B) + 0x88888888, 0x88888888, 0x88888888, 0x88888888 // 60-63 (0x3C-0x3F) + }; + return bitfield(s_increment_table[rate], 4*index, 4); +} + + +//------------------------------------------------- +// detune_adjustment - given a 5-bit key code +// value and a 3-bit detune parameter, return a +// 6-bit signed phase displacement; this table +// has been verified against Nuked's equations, +// but the equations are rather complicated, so +// we'll keep the simplicity of the table +//------------------------------------------------- + +inline int32_t detune_adjustment(uint32_t detune, uint32_t keycode) +{ + static uint8_t const s_detune_adjustment[32][4] = + { + { 0, 0, 1, 2 }, { 0, 0, 1, 2 }, { 0, 0, 1, 2 }, { 0, 0, 1, 2 }, + { 0, 1, 2, 2 }, { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { 0, 1, 2, 4 }, { 0, 1, 3, 4 }, { 0, 1, 3, 4 }, { 0, 1, 3, 5 }, + { 0, 2, 4, 5 }, { 0, 2, 4, 6 }, { 0, 2, 4, 6 }, { 0, 2, 5, 7 }, + { 0, 2, 5, 8 }, { 0, 3, 6, 8 }, { 0, 3, 6, 9 }, { 0, 3, 7, 10 }, + { 0, 4, 8, 11 }, { 0, 4, 8, 12 }, { 0, 4, 9, 13 }, { 0, 5, 10, 14 }, + { 0, 5, 11, 16 }, { 0, 6, 12, 17 }, { 0, 6, 13, 19 }, { 0, 7, 14, 20 }, + { 0, 8, 16, 22 }, { 0, 8, 16, 22 }, { 0, 8, 16, 22 }, { 0, 8, 16, 22 } + }; + int32_t result = s_detune_adjustment[keycode][detune & 3]; + return bitfield(detune, 2) ? -result : result; +} + + +//------------------------------------------------- +// opm_key_code_to_phase_step - converts an +// OPM concatenated block (3 bits), keycode +// (4 bits) and key fraction (6 bits) to a 0.10 +// phase step, after applying the given delta; +// this applies to OPM and OPZ, so it lives here +// in a central location +//------------------------------------------------- + +inline uint32_t opm_key_code_to_phase_step(uint32_t block_freq, int32_t delta) +{ + // The phase step is essentially the fnum in OPN-speak. To compute this table, + // we used the standard formula for computing the frequency of a note, and + // then converted that frequency to fnum using the formula documented in the + // YM2608 manual. + // + // However, the YM2608 manual describes everything in terms of a nominal 8MHz + // clock, which produces an FM clock of: + // + // 8000000 / 24(operators) / 6(prescale) = 55555Hz FM clock + // + // Whereas the descriptions for the YM2151 use a nominal 3.579545MHz clock: + // + // 3579545 / 32(operators) / 2(prescale) = 55930Hz FM clock + // + // To correct for this, the YM2608 formula was adjusted to use a clock of + // 8053920Hz, giving this equation for the fnum: + // + // fnum = (double(144) * freq * (1 << 20)) / double(8053920) / 4; + // + // Unfortunately, the computed table differs in a few spots from the data + // verified from an actual chip. The table below comes from David Viens' + // analysis, used with his permission. + static const uint32_t s_phase_step[12*64] = + { + 41568,41600,41632,41664,41696,41728,41760,41792,41856,41888,41920,41952,42016,42048,42080,42112, + 42176,42208,42240,42272,42304,42336,42368,42400,42464,42496,42528,42560,42624,42656,42688,42720, + 42784,42816,42848,42880,42912,42944,42976,43008,43072,43104,43136,43168,43232,43264,43296,43328, + 43392,43424,43456,43488,43552,43584,43616,43648,43712,43744,43776,43808,43872,43904,43936,43968, + 44032,44064,44096,44128,44192,44224,44256,44288,44352,44384,44416,44448,44512,44544,44576,44608, + 44672,44704,44736,44768,44832,44864,44896,44928,44992,45024,45056,45088,45152,45184,45216,45248, + 45312,45344,45376,45408,45472,45504,45536,45568,45632,45664,45728,45760,45792,45824,45888,45920, + 45984,46016,46048,46080,46144,46176,46208,46240,46304,46336,46368,46400,46464,46496,46528,46560, + 46656,46688,46720,46752,46816,46848,46880,46912,46976,47008,47072,47104,47136,47168,47232,47264, + 47328,47360,47392,47424,47488,47520,47552,47584,47648,47680,47744,47776,47808,47840,47904,47936, + 48032,48064,48096,48128,48192,48224,48288,48320,48384,48416,48448,48480,48544,48576,48640,48672, + 48736,48768,48800,48832,48896,48928,48992,49024,49088,49120,49152,49184,49248,49280,49344,49376, + 49440,49472,49504,49536,49600,49632,49696,49728,49792,49824,49856,49888,49952,49984,50048,50080, + 50144,50176,50208,50240,50304,50336,50400,50432,50496,50528,50560,50592,50656,50688,50752,50784, + 50880,50912,50944,50976,51040,51072,51136,51168,51232,51264,51328,51360,51424,51456,51488,51520, + 51616,51648,51680,51712,51776,51808,51872,51904,51968,52000,52064,52096,52160,52192,52224,52256, + 52384,52416,52448,52480,52544,52576,52640,52672,52736,52768,52832,52864,52928,52960,52992,53024, + 53120,53152,53216,53248,53312,53344,53408,53440,53504,53536,53600,53632,53696,53728,53792,53824, + 53920,53952,54016,54048,54112,54144,54208,54240,54304,54336,54400,54432,54496,54528,54592,54624, + 54688,54720,54784,54816,54880,54912,54976,55008,55072,55104,55168,55200,55264,55296,55360,55392, + 55488,55520,55584,55616,55680,55712,55776,55808,55872,55936,55968,56032,56064,56128,56160,56224, + 56288,56320,56384,56416,56480,56512,56576,56608,56672,56736,56768,56832,56864,56928,56960,57024, + 57120,57152,57216,57248,57312,57376,57408,57472,57536,57568,57632,57664,57728,57792,57824,57888, + 57952,57984,58048,58080,58144,58208,58240,58304,58368,58400,58464,58496,58560,58624,58656,58720, + 58784,58816,58880,58912,58976,59040,59072,59136,59200,59232,59296,59328,59392,59456,59488,59552, + 59648,59680,59744,59776,59840,59904,59936,60000,60064,60128,60160,60224,60288,60320,60384,60416, + 60512,60544,60608,60640,60704,60768,60800,60864,60928,60992,61024,61088,61152,61184,61248,61280, + 61376,61408,61472,61536,61600,61632,61696,61760,61824,61856,61920,61984,62048,62080,62144,62208, + 62272,62304,62368,62432,62496,62528,62592,62656,62720,62752,62816,62880,62944,62976,63040,63104, + 63200,63232,63296,63360,63424,63456,63520,63584,63648,63680,63744,63808,63872,63904,63968,64032, + 64096,64128,64192,64256,64320,64352,64416,64480,64544,64608,64672,64704,64768,64832,64896,64928, + 65024,65056,65120,65184,65248,65312,65376,65408,65504,65536,65600,65664,65728,65792,65856,65888, + 65984,66016,66080,66144,66208,66272,66336,66368,66464,66496,66560,66624,66688,66752,66816,66848, + 66944,66976,67040,67104,67168,67232,67296,67328,67424,67456,67520,67584,67648,67712,67776,67808, + 67904,67936,68000,68064,68128,68192,68256,68288,68384,68448,68512,68544,68640,68672,68736,68800, + 68896,68928,68992,69056,69120,69184,69248,69280,69376,69440,69504,69536,69632,69664,69728,69792, + 69920,69952,70016,70080,70144,70208,70272,70304,70400,70464,70528,70560,70656,70688,70752,70816, + 70912,70976,71040,71104,71136,71232,71264,71360,71424,71488,71552,71616,71648,71744,71776,71872, + 71968,72032,72096,72160,72192,72288,72320,72416,72480,72544,72608,72672,72704,72800,72832,72928, + 72992,73056,73120,73184,73216,73312,73344,73440,73504,73568,73632,73696,73728,73824,73856,73952, + 74080,74144,74208,74272,74304,74400,74432,74528,74592,74656,74720,74784,74816,74912,74944,75040, + 75136,75200,75264,75328,75360,75456,75488,75584,75648,75712,75776,75840,75872,75968,76000,76096, + 76224,76288,76352,76416,76448,76544,76576,76672,76736,76800,76864,76928,77024,77120,77152,77248, + 77344,77408,77472,77536,77568,77664,77696,77792,77856,77920,77984,78048,78144,78240,78272,78368, + 78464,78528,78592,78656,78688,78784,78816,78912,78976,79040,79104,79168,79264,79360,79392,79488, + 79616,79680,79744,79808,79840,79936,79968,80064,80128,80192,80256,80320,80416,80512,80544,80640, + 80768,80832,80896,80960,80992,81088,81120,81216,81280,81344,81408,81472,81568,81664,81696,81792, + 81952,82016,82080,82144,82176,82272,82304,82400,82464,82528,82592,82656,82752,82848,82880,82976 + }; + + // extract the block (octave) first + uint32_t block = bitfield(block_freq, 10, 3); + + // the keycode (bits 6-9) is "gappy", mapping 12 values over 16 in each + // octave; to correct for this, we multiply the 4-bit value by 3/4 (or + // rather subtract 1/4); note that a (invalid) value of 15 will bleed into + // the next octave -- this is confirmed + uint32_t adjusted_code = bitfield(block_freq, 6, 4) - bitfield(block_freq, 8, 2); + + // now re-insert the 6-bit fraction + int32_t eff_freq = (adjusted_code << 6) | bitfield(block_freq, 0, 6); + + // now that the gaps are removed, add the delta + eff_freq += delta; + + // handle over/underflow by adjusting the block: + if (uint32_t(eff_freq) >= 768) + { + // minimum delta is -512 (PM), so we can only underflow by 1 octave + if (eff_freq < 0) + { + eff_freq += 768; + if (block-- == 0) + return s_phase_step[0] >> 7; + } + + // maximum delta is +512+608 (PM+detune), so we can overflow by up to 2 octaves + else + { + eff_freq -= 768; + if (eff_freq >= 768) + block++, eff_freq -= 768; + if (block++ >= 7) + return s_phase_step[767]; + } + } + + // look up the phase shift for the key code, then shift by octave + return s_phase_step[eff_freq] >> (block ^ 7); +} + + +//------------------------------------------------- +// opn_lfo_pm_phase_adjustment - given the 7 most +// significant frequency number bits, plus a 3-bit +// PM depth value and a signed 5-bit raw PM value, +// return a signed PM adjustment to the frequency; +// algorithm written to match Nuked behavior +//------------------------------------------------- + +inline int32_t opn_lfo_pm_phase_adjustment(uint32_t fnum_bits, uint32_t pm_sensitivity, int32_t lfo_raw_pm) +{ + // this table encodes 2 shift values to apply to the top 7 bits + // of fnum; it is effectively a cheap multiply by a constant + // value containing 0-2 bits + static uint8_t const s_lfo_pm_shifts[8][8] = + { + { 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77 }, + { 0x77, 0x77, 0x77, 0x77, 0x72, 0x72, 0x72, 0x72 }, + { 0x77, 0x77, 0x77, 0x72, 0x72, 0x72, 0x17, 0x17 }, + { 0x77, 0x77, 0x72, 0x72, 0x17, 0x17, 0x12, 0x12 }, + { 0x77, 0x77, 0x72, 0x17, 0x17, 0x17, 0x12, 0x07 }, + { 0x77, 0x77, 0x17, 0x12, 0x07, 0x07, 0x02, 0x01 }, + { 0x77, 0x77, 0x17, 0x12, 0x07, 0x07, 0x02, 0x01 }, + { 0x77, 0x77, 0x17, 0x12, 0x07, 0x07, 0x02, 0x01 } + }; + + // look up the relevant shifts + int32_t abs_pm = (lfo_raw_pm < 0) ? -lfo_raw_pm : lfo_raw_pm; + uint32_t const shifts = s_lfo_pm_shifts[pm_sensitivity][bitfield(abs_pm, 0, 3)]; + + // compute the adjustment + int32_t adjust = (fnum_bits >> bitfield(shifts, 0, 4)) + (fnum_bits >> bitfield(shifts, 4, 4)); + if (pm_sensitivity > 5) + adjust <<= pm_sensitivity - 5; + adjust >>= 2; + + // every 16 cycles it inverts sign + return (lfo_raw_pm < 0) ? -adjust : adjust; +} + + + +//********************************************************* +// FM OPERATOR +//********************************************************* + +//------------------------------------------------- +// fm_operator - constructor +//------------------------------------------------- + +template +fm_operator::fm_operator(fm_engine_base &owner, uint32_t opoffs) : + m_choffs(0), + m_opoffs(opoffs), + m_phase(0), + m_env_attenuation(0x3ff), + m_env_state(EG_RELEASE), + m_ssg_inverted(false), + m_key_state(0), + m_keyon_live(0), + m_regs(owner.regs()), + m_owner(owner) +{ +} + + +//------------------------------------------------- +// reset - reset the channel state +//------------------------------------------------- + +template +void fm_operator::reset() +{ + // reset our data + m_phase = 0; + m_env_attenuation = 0x3ff; + m_env_state = EG_RELEASE; + m_ssg_inverted = 0; + m_key_state = 0; + m_keyon_live = 0; +} + + +//------------------------------------------------- +// save_restore - save or restore the data +//------------------------------------------------- + +template +void fm_operator::save_restore(ymfm_saved_state &state) +{ + state.save_restore(m_phase); + state.save_restore(m_env_attenuation); + state.save_restore(m_env_state); + state.save_restore(m_ssg_inverted); + state.save_restore(m_key_state); + state.save_restore(m_keyon_live); +} + + +//------------------------------------------------- +// prepare - prepare for clocking +//------------------------------------------------- + +template +bool fm_operator::prepare() +{ + // cache the data + m_regs.cache_operator_data(m_choffs, m_opoffs, m_cache); + + // clock the key state + clock_keystate(uint32_t(m_keyon_live != 0)); + m_keyon_live &= ~(1 << KEYON_CSM); + + // we're active until we're quiet after the release + return (m_env_state != (RegisterType::EG_HAS_REVERB ? EG_REVERB : EG_RELEASE) || m_env_attenuation < EG_QUIET); +} + + +//------------------------------------------------- +// clock - master clocking function +//------------------------------------------------- + +template +void fm_operator::clock(uint32_t env_counter, int32_t lfo_raw_pm) +{ + // clock the SSG-EG state (OPN/OPNA) + if (m_regs.op_ssg_eg_enable(m_opoffs)) + clock_ssg_eg_state(); + else + m_ssg_inverted = false; + + // clock the envelope if on an envelope cycle; env_counter is a x.2 value + if (bitfield(env_counter, 0, 2) == 0) + clock_envelope(env_counter >> 2); + + // clock the phase + clock_phase(lfo_raw_pm); +} + + +//------------------------------------------------- +// compute_volume - compute the 14-bit signed +// volume of this operator, given a phase +// modulation and an AM LFO offset +//------------------------------------------------- + +template +int32_t fm_operator::compute_volume(uint32_t phase, uint32_t am_offset) const +{ + // the low 10 bits of phase represents a full 2*PI period over + // the full sin wave + + // early out if the envelope is effectively off + if (m_env_attenuation > EG_QUIET) + return 0; + + // get the absolute value of the sin, as attenuation, as a 4.8 fixed point value + uint32_t sin_attenuation = m_cache.waveform[phase & (RegisterType::WAVEFORM_LENGTH - 1)]; + + // get the attenuation from the evelope generator as a 4.6 value, shifted up to 4.8 + uint32_t env_attenuation = envelope_attenuation(am_offset) << 2; + + // combine into a 5.8 value, then convert from attenuation to 13-bit linear volume + int32_t result = attenuation_to_volume((sin_attenuation & 0x7fff) + env_attenuation); + + // negate if in the negative part of the sin wave (sign bit gives 14 bits) + return bitfield(sin_attenuation, 15) ? -result : result; +} + + +//------------------------------------------------- +// compute_noise_volume - compute the 14-bit +// signed noise volume of this operator, given a +// noise input value and an AM offset +//------------------------------------------------- + +template +int32_t fm_operator::compute_noise_volume(uint32_t am_offset) const +{ + // application manual says the logarithmic transform is not applied here, so we + // just use the raw envelope attenuation, inverted (since 0 attenuation should be + // maximum), and shift it up from a 10-bit value to an 11-bit value + int32_t result = (envelope_attenuation(am_offset) ^ 0x3ff) << 1; + + // QUESTION: is AM applied still? + + // negate based on the noise state + return bitfield(m_regs.noise_state(), 0) ? -result : result; +} + + +//------------------------------------------------- +// keyonoff - signal a key on/off event +//------------------------------------------------- + +template +void fm_operator::keyonoff(uint32_t on, keyon_type type) +{ + m_keyon_live = (m_keyon_live & ~(1 << int(type))) | (bitfield(on, 0) << int(type)); +} + + +//------------------------------------------------- +// start_attack - start the attack phase; called +// when a keyon happens or when an SSG-EG cycle +// is complete and restarts +//------------------------------------------------- + +template +void fm_operator::start_attack(bool is_restart) +{ + // don't change anything if already in attack state + if (m_env_state == EG_ATTACK) + return; + m_env_state = EG_ATTACK; + + // generally not inverted at start, except if SSG-EG is enabled and + // one of the inverted modes is specified; leave this alone on a + // restart, as it is managed by the clock_ssg_eg_state() code + if (RegisterType::EG_HAS_SSG && !is_restart) + m_ssg_inverted = m_regs.op_ssg_eg_enable(m_opoffs) & bitfield(m_regs.op_ssg_eg_mode(m_opoffs), 2); + + // reset the phase when we start an attack due to a key on + // (but not when due to an SSG-EG restart except in certain cases + // managed directly by the SSG-EG code) + if (!is_restart) + m_phase = 0; + + // if the attack rate >= 62 then immediately go to max attenuation + if (m_cache.eg_rate[EG_ATTACK] >= 62) + m_env_attenuation = 0; +} + + +//------------------------------------------------- +// start_release - start the release phase; +// called when a keyoff happens +//------------------------------------------------- + +template +void fm_operator::start_release() +{ + // don't change anything if already in release state + if (m_env_state >= EG_RELEASE) + return; + m_env_state = EG_RELEASE; + + // if attenuation if inverted due to SSG-EG, snap the inverted attenuation + // as the starting point + if (RegisterType::EG_HAS_SSG && m_ssg_inverted) + { + m_env_attenuation = (0x200 - m_env_attenuation) & 0x3ff; + m_ssg_inverted = false; + } +} + + +//------------------------------------------------- +// clock_keystate - clock the keystate to match +// the incoming keystate +//------------------------------------------------- + +template +void fm_operator::clock_keystate(uint32_t keystate) +{ + assert(keystate == 0 || keystate == 1); + + // has the key changed? + if ((keystate ^ m_key_state) != 0) + { + m_key_state = keystate; + + // if the key has turned on, start the attack + if (keystate != 0) + { + // OPLL has a DP ("depress"?) state to bring the volume + // down before starting the attack + if (RegisterType::EG_HAS_DEPRESS && m_env_attenuation < 0x200) + m_env_state = EG_DEPRESS; + else + start_attack(); + } + + // otherwise, start the release + else + start_release(); + } +} + + +//------------------------------------------------- +// clock_ssg_eg_state - clock the SSG-EG state; +// should only be called if SSG-EG is enabled +//------------------------------------------------- + +template +void fm_operator::clock_ssg_eg_state() +{ + // work only happens once the attenuation crosses above 0x200 + if (!bitfield(m_env_attenuation, 9)) + return; + + // 8 SSG-EG modes: + // 000: repeat normally + // 001: run once, hold low + // 010: repeat, alternating between inverted/non-inverted + // 011: run once, hold high + // 100: inverted repeat normally + // 101: inverted run once, hold low + // 110: inverted repeat, alternating between inverted/non-inverted + // 111: inverted run once, hold high + uint32_t mode = m_regs.op_ssg_eg_mode(m_opoffs); + + // hold modes (1/3/5/7) + if (bitfield(mode, 0)) + { + // set the inverted flag to the end state (0 for modes 1/7, 1 for modes 3/5) + m_ssg_inverted = bitfield(mode, 2) ^ bitfield(mode, 1); + + // if holding, force the attenuation to the expected value once we're + // past the attack phase + if (m_env_state != EG_ATTACK) + m_env_attenuation = m_ssg_inverted ? 0x200 : 0x3ff; + } + + // continuous modes (0/2/4/6) + else + { + // toggle invert in alternating mode (even in attack state) + m_ssg_inverted ^= bitfield(mode, 1); + + // restart attack if in decay/sustain states + if (m_env_state == EG_DECAY || m_env_state == EG_SUSTAIN) + start_attack(true); + + // phase is reset to 0 in modes 0/4 + if (bitfield(mode, 1) == 0) + m_phase = 0; + } + + // in all modes, once we hit release state, attenuation is forced to maximum + if (m_env_state == EG_RELEASE) + m_env_attenuation = 0x3ff; +} + + +//------------------------------------------------- +// clock_envelope - clock the envelope state +// according to the given count +//------------------------------------------------- + +template +void fm_operator::clock_envelope(uint32_t env_counter) +{ + // handle attack->decay transitions + if (m_env_state == EG_ATTACK && m_env_attenuation == 0) + m_env_state = EG_DECAY; + + // handle decay->sustain transitions; it is important to do this immediately + // after the attack->decay transition above in the event that the sustain level + // is set to 0 (in which case we will skip right to sustain without doing any + // decay); as an example where this can be heard, check the cymbals sound + // in channel 0 of shinobi's test mode sound #5 + if (m_env_state == EG_DECAY && m_env_attenuation >= m_cache.eg_sustain) + m_env_state = EG_SUSTAIN; + + // fetch the appropriate 6-bit rate value from the cache + uint32_t rate = m_cache.eg_rate[m_env_state]; + + // compute the rate shift value; this is the shift needed to + // apply to the env_counter such that it becomes a 5.11 fixed + // point number + uint32_t rate_shift = rate >> 2; + env_counter <<= rate_shift; + + // see if the fractional part is 0; if not, it's not time to clock + if (bitfield(env_counter, 0, 11) != 0) + return; + + // determine the increment based on the non-fractional part of env_counter + uint32_t relevant_bits = bitfield(env_counter, (rate_shift <= 11) ? 11 : rate_shift, 3); + uint32_t increment = attenuation_increment(rate, relevant_bits); + + // attack is the only one that increases + if (m_env_state == EG_ATTACK) + { + // glitch means that attack rates of 62/63 don't increment if + // changed after the initial key on (where they are handled + // specially); nukeykt confirms this happens on OPM, OPN, OPL/OPLL + // at least so assuming it is true for everyone + if (rate < 62) + m_env_attenuation += (~m_env_attenuation * increment) >> 4; + } + + // all other cases are similar + else + { + // non-SSG-EG cases just apply the increment + if (!m_regs.op_ssg_eg_enable(m_opoffs)) + m_env_attenuation += increment; + + // SSG-EG only applies if less than mid-point, and then at 4x + else if (m_env_attenuation < 0x200) + m_env_attenuation += 4 * increment; + + // clamp the final attenuation + if (m_env_attenuation >= 0x400) + m_env_attenuation = 0x3ff; + + // transition from depress to attack + if (RegisterType::EG_HAS_DEPRESS && m_env_state == EG_DEPRESS && m_env_attenuation >= 0x200) + start_attack(); + + // transition from release to reverb, should switch at -18dB + if (RegisterType::EG_HAS_REVERB && m_env_state == EG_RELEASE && m_env_attenuation >= 0xc0) + m_env_state = EG_REVERB; + } +} + + +//------------------------------------------------- +// clock_phase - clock the 10.10 phase value; the +// OPN version of the logic has been verified +// against the Nuked phase generator +//------------------------------------------------- + +template +void fm_operator::clock_phase(int32_t lfo_raw_pm) +{ + // read from the cache, or recalculate if PM active + uint32_t phase_step = m_cache.phase_step; + if (phase_step == opdata_cache::PHASE_STEP_DYNAMIC) + phase_step = m_regs.compute_phase_step(m_choffs, m_opoffs, m_cache, lfo_raw_pm); + + // finally apply the step to the current phase value + m_phase += phase_step; +} + + +//------------------------------------------------- +// envelope_attenuation - return the effective +// attenuation of the envelope +//------------------------------------------------- + +template +uint32_t fm_operator::envelope_attenuation(uint32_t am_offset) const +{ + uint32_t result = m_env_attenuation >> m_cache.eg_shift; + + // invert if necessary due to SSG-EG + if (RegisterType::EG_HAS_SSG && m_ssg_inverted) + result = (0x200 - result) & 0x3ff; + + // add in LFO AM modulation + if (m_regs.op_lfo_am_enable(m_opoffs)) + result += am_offset; + + // add in total level and KSL from the cache + result += m_cache.total_level; + + // clamp to max, apply shift, and return + return std::min(result, 0x3ff); +} + + + +//********************************************************* +// FM CHANNEL +//********************************************************* + +//------------------------------------------------- +// fm_channel - constructor +//------------------------------------------------- + +template +fm_channel::fm_channel(fm_engine_base &owner, uint32_t choffs) : + m_choffs(choffs), + m_feedback{ 0, 0 }, + m_feedback_in(0), + m_op{ nullptr, nullptr, nullptr, nullptr }, + m_regs(owner.regs()), + m_owner(owner) +{ +} + + +//------------------------------------------------- +// reset - reset the channel state +//------------------------------------------------- + +template +void fm_channel::reset() +{ + // reset our data + m_feedback[0] = m_feedback[1] = 0; + m_feedback_in = 0; +} + + +//------------------------------------------------- +// save_restore - save or restore the data +//------------------------------------------------- + +template +void fm_channel::save_restore(ymfm_saved_state &state) +{ + state.save_restore(m_feedback[0]); + state.save_restore(m_feedback[1]); + state.save_restore(m_feedback_in); +} + + +//------------------------------------------------- +// keyonoff - signal key on/off to our operators +//------------------------------------------------- + +template +void fm_channel::keyonoff(uint32_t states, keyon_type type, uint32_t chnum) +{ + for (uint32_t opnum = 0; opnum < m_op.size(); opnum++) + if (m_op[opnum] != nullptr) + m_op[opnum]->keyonoff(bitfield(states, opnum), type); + + if (debug::LOG_KEYON_EVENTS && ((debug::GLOBAL_FM_CHANNEL_MASK >> chnum) & 1) != 0) + for (uint32_t opnum = 0; opnum < m_op.size(); opnum++) + if (m_op[opnum] != nullptr) + debug::log_keyon("%c%s\n", bitfield(states, opnum) ? '+' : '-', m_regs.log_keyon(m_choffs, m_op[opnum]->opoffs()).c_str()); +} + + +//------------------------------------------------- +// prepare - prepare for clocking +//------------------------------------------------- + +template +bool fm_channel::prepare() +{ + uint32_t active_mask = 0; + + // prepare all operators and determine if they are active + for (uint32_t opnum = 0; opnum < m_op.size(); opnum++) + if (m_op[opnum] != nullptr) + if (m_op[opnum]->prepare()) + active_mask |= 1 << opnum; + + return (active_mask != 0); +} + + +//------------------------------------------------- +// clock - master clock of all operators +//------------------------------------------------- + +template +void fm_channel::clock(uint32_t env_counter, int32_t lfo_raw_pm) +{ + // clock the feedback through + m_feedback[0] = m_feedback[1]; + m_feedback[1] = m_feedback_in; + + for (uint32_t opnum = 0; opnum < m_op.size(); opnum++) + if (m_op[opnum] != nullptr) + m_op[opnum]->clock(env_counter, lfo_raw_pm); + +/* +useful temporary code for envelope debugging +if (m_choffs == 0x101) +{ + for (uint32_t opnum = 0; opnum < m_op.size(); opnum++) + { + auto &op = *m_op[((opnum & 1) << 1) | ((opnum >> 1) & 1)]; + printf(" %c%03X%c%c ", + "PADSRV"[op.debug_eg_state()], + op.debug_eg_attenuation(), + op.debug_ssg_inverted() ? '-' : '+', + m_regs.op_ssg_eg_enable(op.opoffs()) ? '0' + m_regs.op_ssg_eg_mode(op.opoffs()) : ' '); + } +printf(" -- "); +} +*/ +} + + +//------------------------------------------------- +// output_2op - combine 4 operators according to +// the specified algorithm, returning a sum +// according to the rshift and clipmax parameters, +// which vary between different implementations +//------------------------------------------------- + +template +void fm_channel::output_2op(output_data &output, uint32_t rshift, int32_t clipmax) const +{ + // The first 2 operators should be populated + assert(m_op[0] != nullptr); + assert(m_op[1] != nullptr); + + // AM amount is the same across all operators; compute it once + uint32_t am_offset = m_regs.lfo_am_offset(m_choffs); + + // operator 1 has optional self-feedback + int32_t opmod = 0; + uint32_t feedback = m_regs.ch_feedback(m_choffs); + if (feedback != 0) + opmod = (m_feedback[0] + m_feedback[1]) >> (10 - feedback); + + // compute the 14-bit volume/value of operator 1 and update the feedback + int32_t op1value = m_feedback_in = m_op[0]->compute_volume(m_op[0]->phase() + opmod, am_offset); + + // now that the feedback has been computed, skip the rest if all volumes + // are clear; no need to do all this work for nothing + if (m_regs.ch_output_any(m_choffs) == 0) + return; + + // Algorithms for two-operator case: + // 0: O1 -> O2 -> out + // 1: (O1 + O2) -> out + int32_t result; + if (bitfield(m_regs.ch_algorithm(m_choffs), 0) == 0) + { + // some OPL chips use the previous sample for modulation instead of + // the current sample + opmod = (RegisterType::MODULATOR_DELAY ? m_feedback[1] : op1value) >> 1; + result = m_op[1]->compute_volume(m_op[1]->phase() + opmod, am_offset) >> rshift; + } + else + { + result = (RegisterType::MODULATOR_DELAY ? m_feedback[1] : op1value) >> rshift; + result += m_op[1]->compute_volume(m_op[1]->phase(), am_offset) >> rshift; + int32_t clipmin = -clipmax - 1; + result = clamp(result, clipmin, clipmax); + } + + // add to the output + add_to_output(m_choffs, output, result); +} + + +//------------------------------------------------- +// output_4op - combine 4 operators according to +// the specified algorithm, returning a sum +// according to the rshift and clipmax parameters, +// which vary between different implementations +//------------------------------------------------- + +template +void fm_channel::output_4op(output_data &output, uint32_t rshift, int32_t clipmax) const +{ + // all 4 operators should be populated + assert(m_op[0] != nullptr); + assert(m_op[1] != nullptr); + assert(m_op[2] != nullptr); + assert(m_op[3] != nullptr); + + // AM amount is the same across all operators; compute it once + uint32_t am_offset = m_regs.lfo_am_offset(m_choffs); + + // operator 1 has optional self-feedback + int32_t opmod = 0; + uint32_t feedback = m_regs.ch_feedback(m_choffs); + if (feedback != 0) + opmod = (m_feedback[0] + m_feedback[1]) >> (10 - feedback); + + // compute the 14-bit volume/value of operator 1 and update the feedback + int32_t op1value = m_feedback_in = m_op[0]->compute_volume(m_op[0]->phase() + opmod, am_offset); + + // now that the feedback has been computed, skip the rest if all volumes + // are clear; no need to do all this work for nothing + if (m_regs.ch_output_any(m_choffs) == 0) + return; + + // OPM/OPN offer 8 different connection algorithms for 4 operators, + // and OPL3 offers 4 more, which we designate here as 8-11. + // + // The operators are computed in order, with the inputs pulled from + // an array of values (opout) that is populated as we go: + // 0 = 0 + // 1 = O1 + // 2 = O2 + // 3 = O3 + // 4 = (O4) + // 5 = O1+O2 + // 6 = O1+O3 + // 7 = O2+O3 + // + // The s_algorithm_ops table describes the inputs and outputs of each + // algorithm as follows: + // + // ---------x use opout[x] as operator 2 input + // ------xxx- use opout[x] as operator 3 input + // ---xxx---- use opout[x] as operator 4 input + // --x------- include opout[1] in final sum + // -x-------- include opout[2] in final sum + // x--------- include opout[3] in final sum + #define ALGORITHM(op2in, op3in, op4in, op1out, op2out, op3out) \ + ((op2in) | ((op3in) << 1) | ((op4in) << 4) | ((op1out) << 7) | ((op2out) << 8) | ((op3out) << 9)) + static uint16_t const s_algorithm_ops[8+4] = + { + ALGORITHM(1,2,3, 0,0,0), // 0: O1 -> O2 -> O3 -> O4 -> out (O4) + ALGORITHM(0,5,3, 0,0,0), // 1: (O1 + O2) -> O3 -> O4 -> out (O4) + ALGORITHM(0,2,6, 0,0,0), // 2: (O1 + (O2 -> O3)) -> O4 -> out (O4) + ALGORITHM(1,0,7, 0,0,0), // 3: ((O1 -> O2) + O3) -> O4 -> out (O4) + ALGORITHM(1,0,3, 0,1,0), // 4: ((O1 -> O2) + (O3 -> O4)) -> out (O2+O4) + ALGORITHM(1,1,1, 0,1,1), // 5: ((O1 -> O2) + (O1 -> O3) + (O1 -> O4)) -> out (O2+O3+O4) + ALGORITHM(1,0,0, 0,1,1), // 6: ((O1 -> O2) + O3 + O4) -> out (O2+O3+O4) + ALGORITHM(0,0,0, 1,1,1), // 7: (O1 + O2 + O3 + O4) -> out (O1+O2+O3+O4) + ALGORITHM(1,2,3, 0,0,0), // 8: O1 -> O2 -> O3 -> O4 -> out (O4) [same as 0] + ALGORITHM(0,2,3, 1,0,0), // 9: (O1 + (O2 -> O3 -> O4)) -> out (O1+O4) [unique] + ALGORITHM(1,0,3, 0,1,0), // 10: ((O1 -> O2) + (O3 -> O4)) -> out (O2+O4) [same as 4] + ALGORITHM(0,2,0, 1,0,1) // 11: (O1 + (O2 -> O3) + O4) -> out (O1+O3+O4) [unique] + }; + uint32_t algorithm_ops = s_algorithm_ops[m_regs.ch_algorithm(m_choffs)]; + + // populate the opout table + int16_t opout[8]; + opout[0] = 0; + opout[1] = op1value; + + // compute the 14-bit volume/value of operator 2 + opmod = opout[bitfield(algorithm_ops, 0, 1)] >> 1; + opout[2] = m_op[1]->compute_volume(m_op[1]->phase() + opmod, am_offset); + opout[5] = opout[1] + opout[2]; + + // compute the 14-bit volume/value of operator 3 + opmod = opout[bitfield(algorithm_ops, 1, 3)] >> 1; + opout[3] = m_op[2]->compute_volume(m_op[2]->phase() + opmod, am_offset); + opout[6] = opout[1] + opout[3]; + opout[7] = opout[2] + opout[3]; + + // compute the 14-bit volume/value of operator 4; this could be a noise + // value on the OPM; all algorithms consume OP4 output at a minimum + int32_t result; + if (m_regs.noise_enable() && m_choffs == 7) + result = m_op[3]->compute_noise_volume(am_offset); + else + { + opmod = opout[bitfield(algorithm_ops, 4, 3)] >> 1; + result = m_op[3]->compute_volume(m_op[3]->phase() + opmod, am_offset); + } + result >>= rshift; + + // optionally add OP1, OP2, OP3 + int32_t clipmin = -clipmax - 1; + if (bitfield(algorithm_ops, 7) != 0) + result = clamp(result + (opout[1] >> rshift), clipmin, clipmax); + if (bitfield(algorithm_ops, 8) != 0) + result = clamp(result + (opout[2] >> rshift), clipmin, clipmax); + if (bitfield(algorithm_ops, 9) != 0) + result = clamp(result + (opout[3] >> rshift), clipmin, clipmax); + + // add to the output + add_to_output(m_choffs, output, result); +} + + +//------------------------------------------------- +// output_rhythm_ch6 - special case output +// computation for OPL channel 6 in rhythm mode, +// which outputs a Bass Drum instrument +//------------------------------------------------- + +template +void fm_channel::output_rhythm_ch6(output_data &output, uint32_t rshift, int32_t clipmax) const +{ + // AM amount is the same across all operators; compute it once + uint32_t am_offset = m_regs.lfo_am_offset(m_choffs); + + // Bass Drum: this uses operators 12 and 15 (i.e., channel 6) + // in an almost-normal way, except that if the algorithm is 1, + // the first operator is ignored instead of added in + + // operator 1 has optional self-feedback + int32_t opmod = 0; + uint32_t feedback = m_regs.ch_feedback(m_choffs); + if (feedback != 0) + opmod = (m_feedback[0] + m_feedback[1]) >> (10 - feedback); + + // compute the 14-bit volume/value of operator 1 and update the feedback + int32_t opout1 = m_feedback_in = m_op[0]->compute_volume(m_op[0]->phase() + opmod, am_offset); + + // compute the 14-bit volume/value of operator 2, which is the result + opmod = bitfield(m_regs.ch_algorithm(m_choffs), 0) ? 0 : (opout1 >> 1); + int32_t result = m_op[1]->compute_volume(m_op[1]->phase() + opmod, am_offset) >> rshift; + + // add to the output + add_to_output(m_choffs, output, result * 2); +} + + +//------------------------------------------------- +// output_rhythm_ch7 - special case output +// computation for OPL channel 7 in rhythm mode, +// which outputs High Hat and Snare Drum +// instruments +//------------------------------------------------- + +template +void fm_channel::output_rhythm_ch7(uint32_t phase_select, output_data &output, uint32_t rshift, int32_t clipmax) const +{ + // AM amount is the same across all operators; compute it once + uint32_t am_offset = m_regs.lfo_am_offset(m_choffs); + uint32_t noise_state = bitfield(m_regs.noise_state(), 0); + + // High Hat: this uses the envelope from operator 13 (channel 7), + // and a combination of noise and the operator 13/17 phase select + // to compute the phase + uint32_t phase = (phase_select << 9) | (0xd0 >> (2 * (noise_state ^ phase_select))); + int32_t result = m_op[0]->compute_volume(phase, am_offset) >> rshift; + + // Snare Drum: this uses the envelope from operator 16 (channel 7), + // and a combination of noise and operator 13 phase to pick a phase + uint32_t op13phase = m_op[0]->phase(); + phase = (0x100 << bitfield(op13phase, 8)) ^ (noise_state << 8); + result += m_op[1]->compute_volume(phase, am_offset) >> rshift; + result = clamp(result, -clipmax - 1, clipmax); + + // add to the output + add_to_output(m_choffs, output, result * 2); +} + + +//------------------------------------------------- +// output_rhythm_ch8 - special case output +// computation for OPL channel 8 in rhythm mode, +// which outputs Tom Tom and Top Cymbal instruments +//------------------------------------------------- + +template +void fm_channel::output_rhythm_ch8(uint32_t phase_select, output_data &output, uint32_t rshift, int32_t clipmax) const +{ + // AM amount is the same across all operators; compute it once + uint32_t am_offset = m_regs.lfo_am_offset(m_choffs); + + // Tom Tom: this is just a single operator processed normally + int32_t result = m_op[0]->compute_volume(m_op[0]->phase(), am_offset) >> rshift; + + // Top Cymbal: this uses the envelope from operator 17 (channel 8), + // and the operator 13/17 phase select to compute the phase + uint32_t phase = 0x100 | (phase_select << 9); + result += m_op[1]->compute_volume(phase, am_offset) >> rshift; + result = clamp(result, -clipmax - 1, clipmax); + + // add to the output + add_to_output(m_choffs, output, result * 2); +} + + + +//********************************************************* +// FM ENGINE BASE +//********************************************************* + +//------------------------------------------------- +// fm_engine_base - constructor +//------------------------------------------------- + +template +fm_engine_base::fm_engine_base(ymfm_interface &intf) : + m_intf(intf), + m_env_counter(0), + m_status(0), + m_clock_prescale(RegisterType::DEFAULT_PRESCALE), + m_irq_mask(STATUS_TIMERA | STATUS_TIMERB), + m_irq_state(0), + m_timer_running{0,0}, + m_total_clocks(0), + m_active_channels(ALL_CHANNELS), + m_modified_channels(ALL_CHANNELS), + m_prepare_count(0) +{ + // inform the interface of their engine + m_intf.m_engine = this; + + // create the channels + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + m_channel[chnum] = std::make_unique>(*this, RegisterType::channel_offset(chnum)); + + // create the operators + for (uint32_t opnum = 0; opnum < OPERATORS; opnum++) + m_operator[opnum] = std::make_unique>(*this, RegisterType::operator_offset(opnum)); + +#if (YMFM_DEBUG_LOG_WAVFILES) + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + m_wavfile[chnum].set_index(chnum); +#endif + + // do the initial operator assignment + assign_operators(); +} + + +//------------------------------------------------- +// reset - reset the overall state +//------------------------------------------------- + +template +void fm_engine_base::reset() +{ + // reset all status bits + set_reset_status(0, 0xff); + + // register type-specific initialization + m_regs.reset(); + + // explicitly write to the mode register since it has side-effects + // QUESTION: old cores initialize this to 0x30 -- who is right? + write(RegisterType::REG_MODE, 0); + + // reset the channels + for (auto &chan : m_channel) + chan->reset(); + + // reset the operators + for (auto &op : m_operator) + op->reset(); +} + + +//------------------------------------------------- +// save_restore - save or restore the data +//------------------------------------------------- + +template +void fm_engine_base::save_restore(ymfm_saved_state &state) +{ + // save our data + state.save_restore(m_env_counter); + state.save_restore(m_status); + state.save_restore(m_clock_prescale); + state.save_restore(m_irq_mask); + state.save_restore(m_irq_state); + state.save_restore(m_timer_running[0]); + state.save_restore(m_timer_running[1]); + state.save_restore(m_total_clocks); + + // save the register/family data + m_regs.save_restore(state); + + // save channel data + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + m_channel[chnum]->save_restore(state); + + // save operator data + for (uint32_t opnum = 0; opnum < OPERATORS; opnum++) + m_operator[opnum]->save_restore(state); + + // invalidate any caches + invalidate_caches(); +} + + +//------------------------------------------------- +// clock - iterate over all channels, clocking +// them forward one step +//------------------------------------------------- + +template +uint32_t fm_engine_base::clock(uint32_t chanmask) +{ + // update the clock counter + m_total_clocks++; + + // if something was modified, prepare + // also prepare every 4k samples to catch ending notes + if (m_modified_channels != 0 || m_prepare_count++ >= 4096) + { + // reassign operators to channels if dynamic + if (RegisterType::DYNAMIC_OPS) + assign_operators(); + + // call each channel to prepare + m_active_channels = 0; + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + if (bitfield(chanmask, chnum)) + if (m_channel[chnum]->prepare()) + m_active_channels |= 1 << chnum; + + // reset the modified channels and prepare count + m_modified_channels = m_prepare_count = 0; + } + + // if the envelope clock divider is 1, just increment by 4; + // otherwise, increment by 1 and manually wrap when we reach the divide count + if (RegisterType::EG_CLOCK_DIVIDER == 1) + m_env_counter += 4; + else if (bitfield(++m_env_counter, 0, 2) == RegisterType::EG_CLOCK_DIVIDER) + m_env_counter += 4 - RegisterType::EG_CLOCK_DIVIDER; + + // clock the noise generator + int32_t lfo_raw_pm = m_regs.clock_noise_and_lfo(); + + // now update the state of all the channels and operators + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + if (bitfield(chanmask, chnum)) + m_channel[chnum]->clock(m_env_counter, lfo_raw_pm); + + // return the envelope counter as it is used to clock ADPCM-A + return m_env_counter; +} + + +//------------------------------------------------- +// output - compute a sum over the relevant +// channels +//------------------------------------------------- + +template +void fm_engine_base::output(output_data &output, uint32_t rshift, int32_t clipmax, uint32_t chanmask) const +{ + // mask out some channels for debug purposes + chanmask &= debug::GLOBAL_FM_CHANNEL_MASK; + + // mask out inactive channels + if (!YMFM_DEBUG_LOG_WAVFILES) + chanmask &= m_active_channels; + + // handle the rhythm case, where some of the operators are dedicated + // to percussion (this is an OPL-specific feature) + if (m_regs.rhythm_enable()) + { + // we don't support the OPM noise channel here; ensure it is off + assert(m_regs.noise_enable() == 0); + + // precompute the operator 13+17 phase selection value + uint32_t op13phase = m_operator[13]->phase(); + uint32_t op17phase = m_operator[17]->phase(); + uint32_t phase_select = (bitfield(op13phase, 2) ^ bitfield(op13phase, 7)) | bitfield(op13phase, 3) | (bitfield(op17phase, 5) ^ bitfield(op17phase, 3)); + + // sum over all the desired channels + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + if (bitfield(chanmask, chnum)) + { +#if (YMFM_DEBUG_LOG_WAVFILES) + auto reference = output; +#endif + if (chnum == 6) + m_channel[chnum]->output_rhythm_ch6(output, rshift, clipmax); + else if (chnum == 7) + m_channel[chnum]->output_rhythm_ch7(phase_select, output, rshift, clipmax); + else if (chnum == 8) + m_channel[chnum]->output_rhythm_ch8(phase_select, output, rshift, clipmax); + else if (m_channel[chnum]->is4op()) + m_channel[chnum]->output_4op(output, rshift, clipmax); + else + m_channel[chnum]->output_2op(output, rshift, clipmax); +#if (YMFM_DEBUG_LOG_WAVFILES) + m_wavfile[chnum].add(output, reference); +#endif + } + } + else + { + // sum over all the desired channels + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + if (bitfield(chanmask, chnum)) + { +#if (YMFM_DEBUG_LOG_WAVFILES) + auto reference = output; +#endif + if (m_channel[chnum]->is4op()) + m_channel[chnum]->output_4op(output, rshift, clipmax); + else + m_channel[chnum]->output_2op(output, rshift, clipmax); +#if (YMFM_DEBUG_LOG_WAVFILES) + m_wavfile[chnum].add(output, reference); +#endif + } + } +} + + +//------------------------------------------------- +// write - handle writes to the OPN registers +//------------------------------------------------- + +template +void fm_engine_base::write(uint16_t regnum, uint8_t data) +{ + debug::log_fm_write("%03X = %02X\n", regnum, data); + + // special case: writes to the mode register can impact IRQs; + // schedule these writes to ensure ordering with timers + if (regnum == RegisterType::REG_MODE) + { + m_intf.ymfm_sync_mode_write(data); + return; + } + + // for now just mark all channels as modified + m_modified_channels = ALL_CHANNELS; + + // most writes are passive, consumed only when needed + uint32_t keyon_channel; + uint32_t keyon_opmask; + if (m_regs.write(regnum, data, keyon_channel, keyon_opmask)) + { + // handle writes to the keyon register(s) + if (keyon_channel < CHANNELS) + { + // normal channel on/off + m_channel[keyon_channel]->keyonoff(keyon_opmask, KEYON_NORMAL, keyon_channel); + } + else if (CHANNELS >= 9 && keyon_channel == RegisterType::RHYTHM_CHANNEL) + { + // special case for the OPL rhythm channels + m_channel[6]->keyonoff(bitfield(keyon_opmask, 4) ? 3 : 0, KEYON_RHYTHM, 6); + m_channel[7]->keyonoff(bitfield(keyon_opmask, 0) | (bitfield(keyon_opmask, 3) << 1), KEYON_RHYTHM, 7); + m_channel[8]->keyonoff(bitfield(keyon_opmask, 2) | (bitfield(keyon_opmask, 1) << 1), KEYON_RHYTHM, 8); + } + } +} + + +//------------------------------------------------- +// status - return the current state of the +// status flags +//------------------------------------------------- + +template +uint8_t fm_engine_base::status() const +{ + return m_status & ~STATUS_BUSY & ~m_regs.status_mask(); +} + + +//------------------------------------------------- +// assign_operators - get the current mapping of +// operators to channels and assign them all +//------------------------------------------------- + +template +void fm_engine_base::assign_operators() +{ + typename RegisterType::operator_mapping map; + m_regs.operator_map(map); + + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + for (uint32_t index = 0; index < 4; index++) + { + uint32_t opnum = bitfield(map.chan[chnum], 8 * index, 8); + m_channel[chnum]->assign(index, (opnum == 0xff) ? nullptr : m_operator[opnum].get()); + } +} + + +//------------------------------------------------- +// update_timer - update the state of the given +// timer +//------------------------------------------------- + +template +void fm_engine_base::update_timer(uint32_t tnum, uint32_t enable, int32_t delta_clocks) +{ + // if the timer is live, but not currently enabled, set the timer + if (enable && !m_timer_running[tnum]) + { + // period comes from the registers, and is different for each + uint32_t period = (tnum == 0) ? (1024 - m_regs.timer_a_value()) : 16 * (256 - m_regs.timer_b_value()); + + // caller can also specify a delta to account for other effects + period += delta_clocks; + + // reset it + m_intf.ymfm_set_timer(tnum, period * OPERATORS * m_clock_prescale); + m_timer_running[tnum] = 1; + } + + // if the timer is not live, ensure it is not enabled + else if (!enable) + { + m_intf.ymfm_set_timer(tnum, -1); + m_timer_running[tnum] = 0; + } +} + + +//------------------------------------------------- +// engine_timer_expired - timer has expired - signal +// status and possibly IRQs +//------------------------------------------------- + +template +void fm_engine_base::engine_timer_expired(uint32_t tnum) +{ + assert(tnum == 0 || tnum == 1); + + // update status + if (tnum == 0 && m_regs.enable_timer_a()) + set_reset_status(STATUS_TIMERA, 0); + else if (tnum == 1 && m_regs.enable_timer_b()) + set_reset_status(STATUS_TIMERB, 0); + + // if timer A fired in CSM mode, trigger CSM on all relevant channels + if (tnum == 0 && m_regs.csm()) + for (uint32_t chnum = 0; chnum < CHANNELS; chnum++) + if (bitfield(RegisterType::CSM_TRIGGER_MASK, chnum)) + { + m_channel[chnum]->keyonoff(0xf, KEYON_CSM, chnum); + m_modified_channels |= 1 << chnum; + } + + // reset + m_timer_running[tnum] = false; + update_timer(tnum, 1, 0); +} + + +//------------------------------------------------- +// check_interrupts - check the interrupt sources +// for interrupts +//------------------------------------------------- + +template +void fm_engine_base::engine_check_interrupts() +{ + // update the state + uint8_t old_state = m_irq_state; + m_irq_state = ((m_status & m_irq_mask & ~m_regs.status_mask()) != 0); + + // set the IRQ status bit + if (m_irq_state) + m_status |= STATUS_IRQ; + else + m_status &= ~STATUS_IRQ; + + // if changed, signal the new state + if (old_state != m_irq_state) + m_intf.ymfm_update_irq(m_irq_state ? true : false); +} + + +//------------------------------------------------- +// engine_mode_write - handle a mode register write +// via timer callback +//------------------------------------------------- + +template +void fm_engine_base::engine_mode_write(uint8_t data) +{ + // mark all channels as modified + m_modified_channels = ALL_CHANNELS; + + // actually write the mode register now + uint32_t dummy1, dummy2; + m_regs.write(RegisterType::REG_MODE, data, dummy1, dummy2); + + // reset IRQ status -- when written, all other bits are ignored + // QUESTION: should this maybe just reset the IRQ bit and not all the bits? + // That is, check_interrupts would only set, this would only clear? + if (m_regs.irq_reset()) + set_reset_status(0, 0x78); + else + { + // reset timer status + uint8_t reset_mask = 0; + if (m_regs.reset_timer_b()) + reset_mask |= RegisterType::STATUS_TIMERB; + if (m_regs.reset_timer_a()) + reset_mask |= RegisterType::STATUS_TIMERA; + set_reset_status(0, reset_mask); + + // load timers; note that timer B gets a small negative adjustment because + // the *16 multiplier is free-running, so the first tick of the clock + // is a bit shorter + update_timer(1, m_regs.load_timer_b(), -(m_total_clocks & 15)); + update_timer(0, m_regs.load_timer_a(), 0); + } +} + +} diff --git a/emu/cores/ymfm/ymfm_opz.h b/emu/cores/ymfm/ymfm_opz.h new file mode 100644 index 00000000..997ba32f --- /dev/null +++ b/emu/cores/ymfm/ymfm_opz.h @@ -0,0 +1,332 @@ +// BSD 3-Clause License +// +// Copyright (c) 2021, Aaron Giles +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#ifndef YMFM_OPZ_H +#define YMFM_OPZ_H + +#pragma once + +#include "ymfm.h" +#include "ymfm_fm.h" + +namespace ymfm +{ + +//********************************************************* +// REGISTER CLASSES +//********************************************************* + +// ======================> opz_registers + +// +// OPZ register map: +// +// System-wide registers: +// 08 -----xxx Load preset (not sure how it gets saved) +// 0F x------- Noise enable +// ---xxxxx Noise frequency +// 10 xxxxxxxx Timer A value (upper 8 bits) +// 11 ------xx Timer A value (lower 2 bits) +// 12 xxxxxxxx Timer B value +// 14 x------- CSM mode +// --x----- Reset timer B +// ---x---- Reset timer A +// ----x--- Enable timer B +// -----x-- Enable timer A +// ------x- Load timer B +// -------x Load timer A +// 16 xxxxxxxx LFO #2 frequency +// 17 0xxxxxxx AM LFO #2 depth +// 1xxxxxxx PM LFO #2 depth +// 18 xxxxxxxx LFO frequency +// 19 0xxxxxxx AM LFO depth +// 1xxxxxxx PM LFO depth +// 1B xx------ CT (2 output data lines) +// --x----- LFO #2 sync +// ---x---- LFO sync +// ----xx-- LFO #2 waveform +// ------xx LFO waveform +// +// Per-channel registers (channel in address bits 0-2) +// 00-07 xxxxxxxx Channel volume +// 20-27 x------- Pan right +// -x------ Key on (0)/off(1) +// --xxx--- Feedback level for operator 1 (0-7) +// -----xxx Operator connection algorithm (0-7) +// 28-2F -xxxxxxx Key code +// 30-37 xxxxxx-- Key fraction +// -------x Mono? mode +// 38-3F 0xxx---- LFO PM sensitivity +// -----0xx LFO AM shift +// 1xxx---- LFO #2 PM sensitivity +// -----1xx LFO #2 AM shift +// +// Per-operator registers (channel in address bits 0-2, operator in bits 3-4) +// 40-5F 0xxx---- Detune value (0-7) +// 0---xxxx Multiple value (0-15) +// 0xxx---- Fix range (0-15) +// 0---xxxx Fix frequency (0-15) +// 1xxx---- Oscillator waveform (0-7) +// 1---xxxx Fine? (0-15) +// 60-7F -xxxxxxx Total level (0-127) +// 80-9F xx------ Key scale rate (0-3) +// --x----- Fix frequency mode +// ---xxxxx Attack rate (0-31) +// A0-BF x------- LFO AM enable +// ---xxxxx Decay rate (0-31) +// C0-DF xx0----- Detune 2 value (0-3) +// --0xxxxx Sustain rate (0-31) +// xx1----- Envelope generator shift? (0-3) +// --1--xxx Rev? (0-7) +// E0-FF xxxx---- Sustain level (0-15) +// ----xxxx Release rate (0-15) +// +// Internal (fake) registers: +// 100-11F -xxx---- Oscillator waveform (0-7) +// ----xxxx Fine? (0-15) +// 120-13F xx------ Envelope generator shift (0-3) +// -----xxx Reverb rate (0-7) +// 140-15F xxxx---- Preset sustain level (0-15) +// ----xxxx Preset release rate (0-15) +// 160-17F xx------ Envelope generator shift (0-3) +// -----xxx Reverb rate (0-7) +// 180-187 -xxx---- LFO #2 PM sensitivity +// ---- xxx LFO #2 AM shift +// 188 -xxxxxxx LFO #2 PM depth +// 189 -xxxxxxx LFO PM depth +// + +class opz_registers : public fm_registers_base +{ + // LFO waveforms are 256 entries long + static constexpr uint32_t LFO_WAVEFORM_LENGTH = 256; + +public: + // constants + static constexpr uint32_t OUTPUTS = 2; + static constexpr uint32_t CHANNELS = 8; + static constexpr uint32_t ALL_CHANNELS = (1 << CHANNELS) - 1; + static constexpr uint32_t OPERATORS = CHANNELS * 4; + static constexpr uint32_t WAVEFORMS = 8; + static constexpr uint32_t REGISTERS = 0x190; + static constexpr uint32_t DEFAULT_PRESCALE = 2; + static constexpr uint32_t EG_CLOCK_DIVIDER = 3; + static constexpr bool EG_HAS_REVERB = true; + static constexpr uint32_t CSM_TRIGGER_MASK = ALL_CHANNELS; + static constexpr uint32_t REG_MODE = 0x14; + static constexpr uint8_t STATUS_TIMERA = 0x01; + static constexpr uint8_t STATUS_TIMERB = 0x02; + static constexpr uint8_t STATUS_BUSY = 0x80; + static constexpr uint8_t STATUS_IRQ = 0; + + // constructor + opz_registers(); + + // reset to initial state + void reset(); + + // save/restore + void save_restore(ymfm_saved_state &state); + + // map channel number to register offset + static constexpr uint32_t channel_offset(uint32_t chnum) + { + assert(chnum < CHANNELS); + return chnum; + } + + // map operator number to register offset + static constexpr uint32_t operator_offset(uint32_t opnum) + { + assert(opnum < OPERATORS); + return opnum; + } + + // return an array of operator indices for each channel + struct operator_mapping { uint32_t chan[CHANNELS]; }; + void operator_map(operator_mapping &dest) const; + + // handle writes to the register array + bool write(uint16_t index, uint8_t data, uint32_t &chan, uint32_t &opmask); + + // clock the noise and LFO, if present, returning LFO PM value + int32_t clock_noise_and_lfo(); + + // return the AM offset from LFO for the given channel + uint32_t lfo_am_offset(uint32_t choffs) const; + + // return the current noise state, gated by the noise clock + uint32_t noise_state() const { return m_noise_state; } + + // caching helpers + void cache_operator_data(uint32_t choffs, uint32_t opoffs, opdata_cache &cache); + + // compute the phase step, given a PM value + uint32_t compute_phase_step(uint32_t choffs, uint32_t opoffs, opdata_cache const &cache, int32_t lfo_raw_pm); + + // log a key-on event + std::string log_keyon(uint32_t choffs, uint32_t opoffs); + + // system-wide registers + uint32_t noise_frequency() const { return byte(0x0f, 0, 5); } + uint32_t noise_enable() const { return byte(0x0f, 7, 1); } + uint32_t timer_a_value() const { return word(0x10, 0, 8, 0x11, 0, 2); } + uint32_t timer_b_value() const { return byte(0x12, 0, 8); } + uint32_t csm() const { return byte(0x14, 7, 1); } + uint32_t reset_timer_b() const { return byte(0x14, 5, 1); } + uint32_t reset_timer_a() const { return byte(0x14, 4, 1); } + uint32_t enable_timer_b() const { return byte(0x14, 3, 1); } + uint32_t enable_timer_a() const { return byte(0x14, 2, 1); } + uint32_t load_timer_b() const { return byte(0x14, 1, 1); } + uint32_t load_timer_a() const { return byte(0x14, 0, 1); } + uint32_t lfo2_pm_depth() const { return byte(0x188, 0, 7); } // fake + uint32_t lfo2_rate() const { return byte(0x16, 0, 8); } + uint32_t lfo2_am_depth() const { return byte(0x17, 0, 7); } + uint32_t lfo_rate() const { return byte(0x18, 0, 8); } + uint32_t lfo_am_depth() const { return byte(0x19, 0, 7); } + uint32_t lfo_pm_depth() const { return byte(0x189, 0, 7); } // fake + uint32_t output_bits() const { return byte(0x1b, 6, 2); } + uint32_t lfo2_sync() const { return byte(0x1b, 5, 1); } + uint32_t lfo_sync() const { return byte(0x1b, 4, 1); } + uint32_t lfo2_waveform() const { return byte(0x1b, 2, 2); } + uint32_t lfo_waveform() const { return byte(0x1b, 0, 2); } + + // per-channel registers + uint32_t ch_volume(uint32_t choffs) const { return byte(0x00, 0, 8, choffs); } + uint32_t ch_output_any(uint32_t choffs) const { return byte(0x20, 7, 1, choffs) | byte(0x30, 0, 1, choffs); } + uint32_t ch_output_0(uint32_t choffs) const { return byte(0x30, 0, 1, choffs); } + uint32_t ch_output_1(uint32_t choffs) const { return byte(0x20, 7, 1, choffs) | byte(0x30, 0, 1, choffs); } + uint32_t ch_output_2(uint32_t choffs) const { return 0; } + uint32_t ch_output_3(uint32_t choffs) const { return 0; } + uint32_t ch_key_on(uint32_t choffs) const { return byte(0x20, 6, 1, choffs); } + uint32_t ch_feedback(uint32_t choffs) const { return byte(0x20, 3, 3, choffs); } + uint32_t ch_algorithm(uint32_t choffs) const { return byte(0x20, 0, 3, choffs); } + uint32_t ch_block_freq(uint32_t choffs) const { return word(0x28, 0, 7, 0x30, 2, 6, choffs); } + uint32_t ch_lfo_pm_sens(uint32_t choffs) const { return byte(0x38, 4, 3, choffs); } + uint32_t ch_lfo_am_sens(uint32_t choffs) const { return byte(0x38, 0, 2, choffs); } + uint32_t ch_lfo2_pm_sens(uint32_t choffs) const { return byte(0x180, 4, 3, choffs); } // fake + uint32_t ch_lfo2_am_sens(uint32_t choffs) const { return byte(0x180, 0, 2, choffs); } // fake + + // per-operator registers + uint32_t op_detune(uint32_t opoffs) const { return byte(0x40, 4, 3, opoffs); } + uint32_t op_multiple(uint32_t opoffs) const { return byte(0x40, 0, 4, opoffs); } + uint32_t op_fix_range(uint32_t opoffs) const { return byte(0x40, 4, 3, opoffs); } + uint32_t op_fix_frequency(uint32_t opoffs) const { return byte(0x40, 0, 4, opoffs); } + uint32_t op_waveform(uint32_t opoffs) const { return byte(0x100, 4, 3, opoffs); } // fake + uint32_t op_fine(uint32_t opoffs) const { return byte(0x100, 0, 4, opoffs); } // fake + uint32_t op_total_level(uint32_t opoffs) const { return byte(0x60, 0, 7, opoffs); } + uint32_t op_ksr(uint32_t opoffs) const { return byte(0x80, 6, 2, opoffs); } + uint32_t op_fix_mode(uint32_t opoffs) const { return byte(0x80, 5, 1, opoffs); } + uint32_t op_attack_rate(uint32_t opoffs) const { return byte(0x80, 0, 5, opoffs); } + uint32_t op_lfo_am_enable(uint32_t opoffs) const { return byte(0xa0, 7, 1, opoffs); } + uint32_t op_decay_rate(uint32_t opoffs) const { return byte(0xa0, 0, 5, opoffs); } + uint32_t op_detune2(uint32_t opoffs) const { return byte(0xc0, 6, 2, opoffs); } + uint32_t op_sustain_rate(uint32_t opoffs) const { return byte(0xc0, 0, 5, opoffs); } + uint32_t op_eg_shift(uint32_t opoffs) const { return byte(0x120, 6, 2, opoffs); } // fake + uint32_t op_reverb_rate(uint32_t opoffs) const { return byte(0x120, 0, 3, opoffs); } // fake + uint32_t op_sustain_level(uint32_t opoffs) const { return byte(0xe0, 4, 4, opoffs); } + uint32_t op_release_rate(uint32_t opoffs) const { return byte(0xe0, 0, 4, opoffs); } + +protected: + // return a bitfield extracted from a byte + uint32_t byte(uint32_t offset, uint32_t start, uint32_t count, uint32_t extra_offset = 0) const + { + return bitfield(m_regdata[offset + extra_offset], start, count); + } + + // return a bitfield extracted from a pair of bytes, MSBs listed first + uint32_t word(uint32_t offset1, uint32_t start1, uint32_t count1, uint32_t offset2, uint32_t start2, uint32_t count2, uint32_t extra_offset = 0) const + { + return (byte(offset1, start1, count1, extra_offset) << count2) | byte(offset2, start2, count2, extra_offset); + } + + // internal state + uint32_t m_lfo_counter[2]; // LFO counter + uint32_t m_noise_lfsr; // noise LFSR state + uint8_t m_noise_counter; // noise counter + uint8_t m_noise_state; // latched noise state + uint8_t m_noise_lfo; // latched LFO noise value + uint8_t m_lfo_am[2]; // current LFO AM value + uint8_t m_regdata[REGISTERS]; // register data + uint16_t m_phase_substep[OPERATORS]; // phase substep for fixed frequency + int16_t m_lfo_waveform[4][LFO_WAVEFORM_LENGTH]; // LFO waveforms; AM in low 8, PM in upper 8 + uint16_t m_waveform[WAVEFORMS][WAVEFORM_LENGTH]; // waveforms +}; + + + +//********************************************************* +// IMPLEMENTATION CLASSES +//********************************************************* + +// ======================> ym2414 + +class ym2414 +{ +public: + using fm_engine = fm_engine_base; + static constexpr uint32_t OUTPUTS = fm_engine::OUTPUTS; + using output_data = fm_engine::output_data; + + // constructor + ym2414(ymfm_interface &intf); + + // reset + void reset(); + + // save/restore + void save_restore(ymfm_saved_state &state); + + // pass-through helpers + uint32_t sample_rate(uint32_t input_clock) const { return m_fm.sample_rate(input_clock); } + void invalidate_caches() { m_fm.invalidate_caches(); } + + // read access + uint8_t read_status(); + uint8_t read(uint32_t offset); + + // write access + void write_address(uint8_t data); + void write_data(uint8_t data); + void write(uint32_t offset, uint8_t data); + + // generate one sample of sound + void generate(output_data *output, uint32_t numsamples = 1); + +protected: + // internal state + uint8_t m_address; // address register + fm_engine m_fm; // core FM engine +}; + +} + + +#endif // YMFM_OPZ_H diff --git a/emu/cores/ymfmintf.cpp b/emu/cores/ymfmintf.cpp new file mode 100644 index 00000000..3dab5dc2 --- /dev/null +++ b/emu/cores/ymfmintf.cpp @@ -0,0 +1,271 @@ +// ymfm wrapper for libvgm +// Provides C interface to ymfm YM2414 (OPZ) emulation + +#include +#include +#include + +#include "../../stdtype.h" + +extern "C" +{ +#include "../SoundDevs.h" +#include "../EmuStructs.h" +#include "../EmuCores.h" +#include "../snddef.h" +#include "../EmuHelper.h" +#include "ymfmintf.h" +} + +#include "ymfm/ymfm.h" +#include "ymfm/ymfm_opz.h" + + +// FourCC for ymfm cores +#define FCC_YMFM 0x594D464D // "YMFM" + + +//********************************************************* +// INTERFACE IMPLEMENTATION +//********************************************************* + +// Simple ymfm interface implementation +class libvgm_ymfm_interface : public ymfm::ymfm_interface +{ +public: + libvgm_ymfm_interface() { } + virtual ~libvgm_ymfm_interface() { } + + // We use the default implementations for all virtual methods + // as libvgm doesn't need timer or IRQ functionality for playback +}; + + +//********************************************************* +// CHIP STATE +//********************************************************* + +struct ymfm_ym2414_chip +{ + DEV_DATA _devData; // Must be first member + + libvgm_ymfm_interface* intf; // Interface instance + ymfm::ym2414* chip; // Chip instance + + UINT32 clock; // Input clock + UINT32 sample_rate; // Output sample rate + UINT8 address; // Current register address +}; + + +//********************************************************* +// HELPER MACROS +//********************************************************* + +#define CHP_GET_PTR(devData) ((ymfm_ym2414_chip*)(devData)) + + +//********************************************************* +// DEVICE FUNCTION DECLARATIONS +//********************************************************* + +static UINT8 device_start_ym2414(const DEV_GEN_CFG* cfg, DEV_INFO* retDevInf); +static void device_stop_ym2414(void* chip); +static void device_reset_ym2414(void* chip); +static void ym2414_update(void* chip, UINT32 samples, DEV_SMPL** outputs); +static UINT8 ym2414_write(void* chip, UINT8 offset, UINT8 data); +static UINT8 ym2414_read(void* chip, UINT8 offset); +static void ym2414_set_mute_mask(void* chip, UINT32 muteMask); + + +//********************************************************* +// DEVICE DEFINITION +//********************************************************* + +static DEVDEF_RWFUNC devFunc[] = +{ + {RWF_REGISTER | RWF_WRITE, DEVRW_A8D8, 0, (void*)ym2414_write}, + {RWF_REGISTER | RWF_READ, DEVRW_A8D8, 0, (void*)ym2414_read}, + {RWF_CHN_MUTE | RWF_WRITE, DEVRW_ALL, 0, (void*)ym2414_set_mute_mask}, + {0x00, 0x00, 0, NULL} +}; + +static DEV_DEF devDef_YM2414 = +{ + "YM2414", "ymfm", FCC_YMFM, + + device_start_ym2414, + device_stop_ym2414, + device_reset_ym2414, + ym2414_update, + + NULL, // SetOptionBits + ym2414_set_mute_mask, + NULL, // SetPanning (deprecated) + NULL, // SetSampleRateChangeCallback + NULL, // SetLoggingCallback + NULL, // LinkDevice + + devFunc, +}; + + +//********************************************************* +// DEVICE FUNCTIONS +//********************************************************* + +static UINT8 device_start_ym2414(const DEV_GEN_CFG* cfg, DEV_INFO* retDevInf) +{ + ymfm_ym2414_chip* info; + DEV_DATA* devData; + UINT32 rate; + + info = (ymfm_ym2414_chip*)calloc(1, sizeof(ymfm_ym2414_chip)); + if (info == NULL) + return 0xFF; + + devData = &info->_devData; + info->clock = cfg->clock; + + // Create interface and chip + info->intf = new libvgm_ymfm_interface(); + info->chip = new ymfm::ym2414(*info->intf); + + // Calculate sample rate + rate = info->chip->sample_rate(cfg->clock); + + // Initialize device info + INIT_DEVINF(retDevInf, devData, rate, &devDef_YM2414); + + return 0x00; +} + +static void device_stop_ym2414(void* chip) +{ + ymfm_ym2414_chip* info = CHP_GET_PTR(chip); + + delete info->chip; + delete info->intf; + free(info); + + return; +} + +static void device_reset_ym2414(void* chip) +{ + ymfm_ym2414_chip* info = CHP_GET_PTR(chip); + + info->chip->reset(); + info->address = 0x00; + + return; +} + +static void ym2414_update(void* chip, UINT32 samples, DEV_SMPL** outputs) +{ + ymfm_ym2414_chip* info = CHP_GET_PTR(chip); + DEV_SMPL* outL = outputs[0]; + DEV_SMPL* outR = outputs[1]; + UINT32 i; + + ymfm::ym2414::output_data output; + + for (i = 0; i < samples; i++) + { + info->chip->generate(&output, 1); + + // ymfm outputs 32-bit signed samples + // libvgm expects DEV_SMPL (also 32-bit signed) + outL[i] = output.data[0]; + outR[i] = output.data[1]; + } + + return; +} + +static UINT8 ym2414_write(void* chip, UINT8 offset, UINT8 data) +{ + ymfm_ym2414_chip* info = CHP_GET_PTR(chip); + + switch (offset & 0x01) + { + case 0x00: // Address + info->address = data; + info->chip->write_address(data); + break; + case 0x01: // Data + info->chip->write_data(data); + break; + } + + return 0x00; +} + +static UINT8 ym2414_read(void* chip, UINT8 offset) +{ + ymfm_ym2414_chip* info = CHP_GET_PTR(chip); + + switch (offset & 0x01) + { + case 0x00: // Status + return info->chip->read_status(); + case 0x01: // Data (not supported on YM2414) + return 0xFF; + } + + return 0x00; +} + +static void ym2414_set_mute_mask(void* chip, UINT32 muteMask) +{ + // YM2414 has 8 channels + // TODO: Implement channel muting if needed + // ymfm doesn't provide a direct mute interface, would need to be + // implemented at the output stage or by modifying registers + return; +} + + +//********************************************************* +// DEVICE METADATA +//********************************************************* + +static const char* DeviceName_YM2414(const DEV_GEN_CFG* devCfg) +{ + return "YM2414"; +} + +static UINT16 DeviceChannels_YM2414(const DEV_GEN_CFG* devCfg) +{ + return 8; // 8 FM channels +} + +static const char** DeviceChannelNames_YM2414(const DEV_GEN_CFG* devCfg) +{ + return NULL; // No special channel names +} + +static const DEVLINK_IDS* DeviceLinkIDs_YM2414(const DEV_GEN_CFG* devCfg) +{ + return NULL; // No linked devices +} + + +//********************************************************* +// DEVICE DECLARATION +//********************************************************* + +extern "C" const DEV_DECL sndDev_YM2414 = +{ + DEVID_YM2414, + DeviceName_YM2414, + DeviceChannels_YM2414, + DeviceChannelNames_YM2414, + DeviceLinkIDs_YM2414, + { +#ifdef EC_YM2414_YMFM + &devDef_YM2414, +#endif + NULL + } +}; diff --git a/emu/cores/ymfmintf.h b/emu/cores/ymfmintf.h new file mode 100644 index 00000000..5a5f1725 --- /dev/null +++ b/emu/cores/ymfmintf.h @@ -0,0 +1,14 @@ +#ifndef __YMFMINTF_H__ +#define __YMFMINTF_H__ + +#include "../EmuStructs.h" + +// Core selection defines +#ifndef EC_YM2414_YMFM +#define EC_YM2414_YMFM 0x01 +#endif + +// Device declaration +extern const DEV_DECL sndDev_YM2414; + +#endif // __YMFMINTF_H__ From ddb743bc0c515129568d55a9316f4683e3fa6ffc Mon Sep 17 00:00:00 2001 From: vampi Date: Wed, 31 Dec 2025 02:14:48 +0200 Subject: [PATCH 2/3] Add ymfm OPZ implementation file and test program MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ymfm_opz.cpp to build (vendored from ymfm library) - Include ymfm_fm.h and ymfm_fm.ipp for template instantiation - Add opz_test.cpp to generate test WAV file with FM synthesis - Fix key on/off control (bit 6 of register 0x20) - Adjust audio output scaling for proper volume 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- emu/CMakeLists.txt | 2 +- emu/cores/ymfm/ymfm_opz.cpp | 808 ++++++++++++++++++++++++++++++++++++ emu/cores/ymfmintf.cpp | 6 + opz_test.cpp | 232 +++++++++++ 4 files changed, 1047 insertions(+), 1 deletion(-) create mode 100644 emu/cores/ymfm/ymfm_opz.cpp create mode 100644 opz_test.cpp diff --git a/emu/CMakeLists.txt b/emu/CMakeLists.txt index e2505b65..674c05e3 100644 --- a/emu/CMakeLists.txt +++ b/emu/CMakeLists.txt @@ -652,7 +652,7 @@ if(SNDEMU_ICS2115_ALL) endif() if(SNDEMU_YM2414_YMFM) set(EMU_DEFS ${EMU_DEFS} " SNDDEV_YM2414" " EC_YM2414_YMFM") - set(EMU_FILES ${EMU_FILES} cores/ymfmintf.cpp) + set(EMU_FILES ${EMU_FILES} cores/ymfmintf.cpp cores/ymfm/ymfm_opz.cpp) set(EMU_CORE_HEADERS ${EMU_CORE_HEADERS} cores/ymfmintf.h) set(EMU_CORE_HEADERS ${EMU_CORE_HEADERS} cores/ymfm/ymfm.h diff --git a/emu/cores/ymfm/ymfm_opz.cpp b/emu/cores/ymfm/ymfm_opz.cpp new file mode 100644 index 00000000..1178417b --- /dev/null +++ b/emu/cores/ymfm/ymfm_opz.cpp @@ -0,0 +1,808 @@ +// BSD 3-Clause License +// +// Copyright (c) 2021, Aaron Giles +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "ymfm_opz.h" +#include "ymfm_fm.ipp" + +#define TEMPORARY_DEBUG_PRINTS (0) + +// +// OPZ (aka YM2414) +// +// This chip is not officially documented as far as I know. What I have +// comes from this site: +// +// http://sr4.sakura.ne.jp/fmsound/opz.html +// +// and from reading the TX81Z operator manual, which describes how a number +// of these new features work. +// +// OPZ appears be bsaically OPM with a bunch of extra features. +// +// For starters, there are two LFO generators. I have presumed that they +// operate identically since identical parameters are offered for each. I +// have also presumed the effects are additive between them. The LFOs on +// the OPZ have an extra "sync" option which apparently causes the LFO to +// reset whenever a key on is received. +// +// At the channel level, there is an additional 8-bit volume control. This +// might work as an addition to total level, or some other way. Completely +// unknown, and unimplemented. +// +// At the operator level, there are a number of extra features. First, there +// are 8 different waveforms to choose from. These are different than the +// waveforms introduced in the OPL2 and later chips. +// +// Second, there is an additional "reverb" stage added to the envelope +// generator, which kicks in when the envelope reaches -18dB. It specifies +// a slower decay rate to produce a sort of faux reverb effect. +// +// The envelope generator also supports a 2-bit shift value, which can be +// used to reduce the effect of the envelope attenuation. +// +// OPZ supports a "fixed frequency" mode for each operator, with a 3-bit +// range and 4-bit frequency value, plus a 1-bit enable. Not sure how that +// works at all, so it's not implemented. +// +// There are also several mystery fields in the operators which I have no +// clue about: "fine" (4 bits), "eg_shift" (2 bits), and "rev" (3 bits). +// eg_shift is some kind of envelope generator effect, but how it works is +// unknown. +// +// Also, according to the site above, the panning controls are changed from +// OPM, with a "mono" bit and only one control bit for the right channel. +// Current implementation is just a guess. +// + +namespace ymfm +{ + +//********************************************************* +// OPZ REGISTERS +//********************************************************* + +//------------------------------------------------- +// opz_registers - constructor +//------------------------------------------------- + +opz_registers::opz_registers() : + m_lfo_counter{ 0, 0 }, + m_noise_lfsr(1), + m_noise_counter(0), + m_noise_state(0), + m_noise_lfo(0), + m_lfo_am{ 0, 0 } +{ + // create the waveforms + for (uint32_t index = 0; index < WAVEFORM_LENGTH; index++) + m_waveform[0][index] = abs_sin_attenuation(index) | (bitfield(index, 9) << 15); + + // we only have the diagrams to judge from, but suspecting waveform 1 (and + // derived waveforms) are sin^2, based on OPX description of similar wave- + // forms; since our sin table is logarithmic, this ends up just being + // 2*existing value + uint16_t zeroval = m_waveform[0][0]; + for (uint32_t index = 0; index < WAVEFORM_LENGTH; index++) + m_waveform[1][index] = std::min(2 * (m_waveform[0][index] & 0x7fff), zeroval) | (bitfield(index, 9) << 15); + + // remaining waveforms are just derivations of the 2 main ones + for (uint32_t index = 0; index < WAVEFORM_LENGTH; index++) + { + m_waveform[2][index] = bitfield(index, 9) ? zeroval : m_waveform[0][index]; + m_waveform[3][index] = bitfield(index, 9) ? zeroval : m_waveform[1][index]; + m_waveform[4][index] = bitfield(index, 9) ? zeroval : m_waveform[0][index * 2]; + m_waveform[5][index] = bitfield(index, 9) ? zeroval : m_waveform[1][index * 2]; + m_waveform[6][index] = bitfield(index, 9) ? zeroval : m_waveform[0][(index * 2) & 0x1ff]; + m_waveform[7][index] = bitfield(index, 9) ? zeroval : m_waveform[1][(index * 2) & 0x1ff]; + } + + // create the LFO waveforms; AM in the low 8 bits, PM in the upper 8 + // waveforms are adjusted to match the pictures in the application manual + for (uint32_t index = 0; index < LFO_WAVEFORM_LENGTH; index++) + { + // waveform 0 is a sawtooth + uint8_t am = index ^ 0xff; + uint8_t pm = index; + m_lfo_waveform[0][index] = am | (pm << 8); + + // waveform 1 is a square wave + am = bitfield(index, 7) ? 0 : 0xff; + pm = am ^ 0x80; + m_lfo_waveform[1][index] = am | (pm << 8); + + // waveform 2 is a triangle wave + am = bitfield(index, 7) ? (index << 1) : ((index ^ 0xff) << 1); + pm = bitfield(index, 6) ? am : ~am; + m_lfo_waveform[2][index] = am | (pm << 8); + + // waveform 3 is noise; it is filled in dynamically + } +} + + +//------------------------------------------------- +// reset - reset to initial state +//------------------------------------------------- + +void opz_registers::reset() +{ + std::fill_n(&m_regdata[0], REGISTERS, 0); + + // enable output on both channels by default + m_regdata[0x30] = m_regdata[0x31] = m_regdata[0x32] = m_regdata[0x33] = 0x01; + m_regdata[0x34] = m_regdata[0x35] = m_regdata[0x36] = m_regdata[0x37] = 0x01; +} + + +//------------------------------------------------- +// save_restore - save or restore the data +//------------------------------------------------- + +void opz_registers::save_restore(ymfm_saved_state &state) +{ + state.save_restore(m_lfo_counter); + state.save_restore(m_lfo_am); + state.save_restore(m_noise_lfsr); + state.save_restore(m_noise_counter); + state.save_restore(m_noise_state); + state.save_restore(m_noise_lfo); + state.save_restore(m_regdata); + state.save_restore(m_phase_substep); +} + + +//------------------------------------------------- +// operator_map - return an array of operator +// indices for each channel; for OPZ this is fixed +//------------------------------------------------- + +void opz_registers::operator_map(operator_mapping &dest) const +{ + // Note that the channel index order is 0,2,1,3, so we bitswap the index. + // + // This is because the order in the map is: + // carrier 1, carrier 2, modulator 1, modulator 2 + // + // But when wiring up the connections, the more natural order is: + // carrier 1, modulator 1, carrier 2, modulator 2 + static const operator_mapping s_fixed_map = + { { + operator_list( 0, 16, 8, 24 ), // Channel 0 operators + operator_list( 1, 17, 9, 25 ), // Channel 1 operators + operator_list( 2, 18, 10, 26 ), // Channel 2 operators + operator_list( 3, 19, 11, 27 ), // Channel 3 operators + operator_list( 4, 20, 12, 28 ), // Channel 4 operators + operator_list( 5, 21, 13, 29 ), // Channel 5 operators + operator_list( 6, 22, 14, 30 ), // Channel 6 operators + operator_list( 7, 23, 15, 31 ), // Channel 7 operators + } }; + dest = s_fixed_map; +} + + +//------------------------------------------------- +// write - handle writes to the register array +//------------------------------------------------- + +bool opz_registers::write(uint16_t index, uint8_t data, uint32_t &channel, uint32_t &opmask) +{ + assert(index < REGISTERS); + + // special mappings: + // 0x16 -> 0x188 if bit 7 is set + // 0x19 -> 0x189 if bit 7 is set + // 0x38..0x3F -> 0x180..0x187 if bit 7 is set + // 0x40..0x5F -> 0x100..0x11F if bit 7 is set + // 0xC0..0xDF -> 0x120..0x13F if bit 5 is set + if (index == 0x17 && bitfield(data, 7) != 0) + m_regdata[0x188] = data; + else if (index == 0x19 && bitfield(data, 7) != 0) + m_regdata[0x189] = data; + else if ((index & 0xf8) == 0x38 && bitfield(data, 7) != 0) + m_regdata[0x180 + (index & 7)] = data; + else if ((index & 0xe0) == 0x40 && bitfield(data, 7) != 0) + m_regdata[0x100 + (index & 0x1f)] = data; + else if ((index & 0xe0) == 0xc0 && bitfield(data, 5) != 0) + m_regdata[0x120 + (index & 0x1f)] = data; + else if (index < 0x100) + m_regdata[index] = data; + + // preset writes restore some values from a preset memory; not sure + // how this really works but the TX81Z will overwrite the sustain level/ + // release rate register and the envelope shift/reverb rate register to + // dampen sound, then write the preset number to register 8 to restore them + if (index == 0x08) + { + int chan = bitfield(data, 0, 3); + if (TEMPORARY_DEBUG_PRINTS) + printf("Loading preset %d\n", chan); + m_regdata[0xe0 + chan + 0] = m_regdata[0x140 + chan + 0]; + m_regdata[0xe0 + chan + 8] = m_regdata[0x140 + chan + 8]; + m_regdata[0xe0 + chan + 16] = m_regdata[0x140 + chan + 16]; + m_regdata[0xe0 + chan + 24] = m_regdata[0x140 + chan + 24]; + m_regdata[0x120 + chan + 0] = m_regdata[0x160 + chan + 0]; + m_regdata[0x120 + chan + 8] = m_regdata[0x160 + chan + 8]; + m_regdata[0x120 + chan + 16] = m_regdata[0x160 + chan + 16]; + m_regdata[0x120 + chan + 24] = m_regdata[0x160 + chan + 24]; + } + + // store the presets under some unknown condition; the pattern of writes + // when setting a new preset is: + // + // 08 (0-7), 80-9F, A0-BF, C0-DF, C0-DF (alt), 20-27, 40-5F, 40-5F (alt), + // C0-DF (alt -- again?), 38-3F, 1B, 18, E0-FF + // + // So it writes 0-7 to 08 to either reset all presets or to indicate + // that we're going to be loading them. Immediately after all the writes + // above, the very next write will be temporary values to blow away the + // values loaded into E0-FF, so somehow it also knows that anything after + // that point is not part of the preset. + // + // For now, try using the 40-5F (alt) writes as flags that presets are + // being loaded until the E0-FF writes happen. + bool is_setting_preset = (bitfield(m_regdata[0x100 + (index & 0x1f)], 7) != 0); + if (is_setting_preset) + { + if ((index & 0xe0) == 0xe0) + { + m_regdata[0x140 + (index & 0x1f)] = data; + m_regdata[0x100 + (index & 0x1f)] &= 0x7f; + } + else if ((index & 0xe0) == 0xc0 && bitfield(data, 5) != 0) + m_regdata[0x160 + (index & 0x1f)] = data; + } + + // handle writes to the key on index + if ((index & 0xf8) == 0x20 && bitfield(index, 0, 3) == bitfield(m_regdata[0x08], 0, 3)) + { + channel = bitfield(index, 0, 3); + opmask = ch_key_on(channel) ? 0xf : 0; + + // according to the TX81Z manual, the sync option causes the LFOs + // to reset at each note on + if (opmask != 0) + { + if (lfo_sync()) + m_lfo_counter[0] = 0; + if (lfo2_sync()) + m_lfo_counter[1] = 0; + } + return true; + } + return false; +} + + +//------------------------------------------------- +// clock_noise_and_lfo - clock the noise and LFO, +// handling clock division, depth, and waveform +// computations +//------------------------------------------------- + +int32_t opz_registers::clock_noise_and_lfo() +{ + // base noise frequency is measured at 2x 1/2 FM frequency; this + // means each tick counts as two steps against the noise counter + uint32_t freq = noise_frequency(); + for (int rep = 0; rep < 2; rep++) + { + // evidence seems to suggest the LFSR is clocked continually and just + // sampled at the noise frequency for output purposes; note that the + // low 8 bits are the most recent 8 bits of history while bits 8-24 + // contain the 17 bit LFSR state + m_noise_lfsr <<= 1; + m_noise_lfsr |= bitfield(m_noise_lfsr, 17) ^ bitfield(m_noise_lfsr, 14) ^ 1; + + // compare against the frequency and latch when we exceed it + if (m_noise_counter++ >= freq) + { + m_noise_counter = 0; + m_noise_state = bitfield(m_noise_lfsr, 17); + } + } + + // treat the rate as a 4.4 floating-point step value with implied + // leading 1; this matches exactly the frequencies in the application + // manual, though it might not be implemented exactly this way on chip + uint32_t rate0 = lfo_rate(); + uint32_t rate1 = lfo2_rate(); + m_lfo_counter[0] += (0x10 | bitfield(rate0, 0, 4)) << bitfield(rate0, 4, 4); + m_lfo_counter[1] += (0x10 | bitfield(rate1, 0, 4)) << bitfield(rate1, 4, 4); + uint32_t lfo0 = bitfield(m_lfo_counter[0], 22, 8); + uint32_t lfo1 = bitfield(m_lfo_counter[1], 22, 8); + + // fill in the noise entry 1 ahead of our current position; this + // ensures the current value remains stable for a full LFO clock + // and effectively latches the running value when the LFO advances + uint32_t lfo_noise = bitfield(m_noise_lfsr, 17, 8); + m_lfo_waveform[3][(lfo0 + 1) & 0xff] = lfo_noise | (lfo_noise << 8); + m_lfo_waveform[3][(lfo1 + 1) & 0xff] = lfo_noise | (lfo_noise << 8); + + // fetch the AM/PM values based on the waveform; AM is unsigned and + // encoded in the low 8 bits, while PM signed and encoded in the upper + // 8 bits + int32_t ampm0 = m_lfo_waveform[lfo_waveform()][lfo0]; + int32_t ampm1 = m_lfo_waveform[lfo2_waveform()][lfo1]; + + // apply depth to the AM values and store for later + m_lfo_am[0] = ((ampm0 & 0xff) * lfo_am_depth()) >> 7; + m_lfo_am[1] = ((ampm1 & 0xff) * lfo2_am_depth()) >> 7; + + // apply depth to the PM values and return them combined into two + int32_t pm0 = ((ampm0 >> 8) * int32_t(lfo_pm_depth())) >> 7; + int32_t pm1 = ((ampm1 >> 8) * int32_t(lfo2_pm_depth())) >> 7; + return (pm0 & 0xff) | (pm1 << 8); +} + + +//------------------------------------------------- +// lfo_am_offset - return the AM offset from LFO +// for the given channel +//------------------------------------------------- + +uint32_t opz_registers::lfo_am_offset(uint32_t choffs) const +{ + // not sure how this works for real, but just adding the two + // AM LFOs together + uint32_t result = 0; + + // shift value for AM sensitivity is [*, 0, 1, 2], + // mapping to values of [0, 23.9, 47.8, and 95.6dB] + uint32_t am_sensitivity = ch_lfo_am_sens(choffs); + if (am_sensitivity != 0) + result = m_lfo_am[0] << (am_sensitivity - 1); + + // QUESTION: see OPN note below for the dB range mapping; it applies + // here as well + + // raw LFO AM value on OPZ is 0-FF, which is already a factor of 2 + // larger than the OPN below, putting our staring point at 2x theirs; + // this works out since our minimum is 2x their maximum + uint32_t am_sensitivity2 = ch_lfo2_am_sens(choffs); + if (am_sensitivity2 != 0) + result += m_lfo_am[1] << (am_sensitivity2 - 1); + + return result; +} + + +//------------------------------------------------- +// cache_operator_data - fill the operator cache +// with prefetched data +//------------------------------------------------- + +void opz_registers::cache_operator_data(uint32_t choffs, uint32_t opoffs, opdata_cache &cache) +{ + // TODO: how does fixed frequency mode work? appears to be enabled by + // op_fix_mode(), and controlled by op_fix_range(), op_fix_frequency() + + // TODO: what is op_rev()? + + // set up the easy stuff + cache.waveform = &m_waveform[op_waveform(opoffs)][0]; + + // get frequency from the channel + uint32_t block_freq = cache.block_freq = ch_block_freq(choffs); + + // compute the keycode: block_freq is: + // + // BBBCCCCFFFFFF + // ^^^^^ + // + // the 5-bit keycode is just the top 5 bits (block + top 2 bits + // of the key code) + uint32_t keycode = bitfield(block_freq, 8, 5); + + // detune adjustment + cache.detune = detune_adjustment(op_detune(opoffs), keycode); + + // multiple value, as an x.4 value (0 means 0.5) + // the "fine" control provides the fractional bits + cache.multiple = op_multiple(opoffs) << 4; + if (cache.multiple == 0) + cache.multiple = 0x08; + cache.multiple |= op_fine(opoffs); + + // phase step, or PHASE_STEP_DYNAMIC if PM is active; this depends on + // block_freq, detune, and multiple, so compute it after we've done those; + // note that fix frequency mode is also treated as dynamic + if (!op_fix_mode(opoffs) && (lfo_pm_depth() == 0 || ch_lfo_pm_sens(choffs) == 0) && (lfo2_pm_depth() == 0 || ch_lfo2_pm_sens(choffs) == 0)) + cache.phase_step = compute_phase_step(choffs, opoffs, cache, 0); + else + cache.phase_step = opdata_cache::PHASE_STEP_DYNAMIC; + + // total level, scaled by 8 + // TODO: how does ch_volume() fit into this? + cache.total_level = op_total_level(opoffs) << 3; + + // 4-bit sustain level, but 15 means 31 so effectively 5 bits + cache.eg_sustain = op_sustain_level(opoffs); + cache.eg_sustain |= (cache.eg_sustain + 1) & 0x10; + cache.eg_sustain <<= 5; + + // determine KSR adjustment for enevlope rates + uint32_t ksrval = keycode >> (op_ksr(opoffs) ^ 3); + cache.eg_rate[EG_ATTACK] = effective_rate(op_attack_rate(opoffs) * 2, ksrval); + cache.eg_rate[EG_DECAY] = effective_rate(op_decay_rate(opoffs) * 2, ksrval); + cache.eg_rate[EG_SUSTAIN] = effective_rate(op_sustain_rate(opoffs) * 2, ksrval); + cache.eg_rate[EG_RELEASE] = effective_rate(op_release_rate(opoffs) * 4 + 2, ksrval); + cache.eg_rate[EG_REVERB] = cache.eg_rate[EG_RELEASE]; + uint32_t reverb = op_reverb_rate(opoffs); + if (reverb != 0) + cache.eg_rate[EG_REVERB] = std::min(effective_rate(reverb * 4 + 2, ksrval), cache.eg_rate[EG_REVERB]); + + // set the envelope shift; TX81Z manual says operator 1 shift is fixed at "off" + cache.eg_shift = ((opoffs & 0x18) == 0) ? 0 : op_eg_shift(opoffs); +} + + +//------------------------------------------------- +// compute_phase_step - compute the phase step +//------------------------------------------------- + +uint32_t opz_registers::compute_phase_step(uint32_t choffs, uint32_t opoffs, opdata_cache const &cache, int32_t lfo_raw_pm) +{ + // OPZ has a fixed frequency mode; it is unclear whether the + // detune and multiple parameters affect things + + uint32_t phase_step; + if (op_fix_mode(opoffs)) + { + // the baseline frequency in hz comes from the fix frequency and fine + // registers, which can specify values 8-255Hz in 1Hz increments; that + // value is then shifted up by the 3-bit range + uint32_t freq = op_fix_frequency(opoffs) << 4; + if (freq == 0) + freq = 8; + freq |= op_fine(opoffs); + freq <<= op_fix_range(opoffs); + + // there is not enough resolution in the plain phase step to track the + // full range of frequencies, so we keep a per-operator sub step with an + // additional 12 bits of resolution; this calculation gives us, for + // example, a frequency of 8.0009Hz when 8Hz is requested + uint32_t substep = m_phase_substep[opoffs]; + substep += 75 * freq; + phase_step = substep >> 12; + m_phase_substep[opoffs] = substep & 0xfff; + + // detune/multiple occupy the same space as fix_range/fix_frequency so + // don't apply them in addition + return phase_step; + } + else + { + // start with coarse detune delta; table uses cents value from + // manual, converted into 1/64ths + static const int16_t s_detune2_delta[4] = { 0, (600*64+50)/100, (781*64+50)/100, (950*64+50)/100 }; + int32_t delta = s_detune2_delta[op_detune2(opoffs)]; + + // add in the PM deltas + uint32_t pm_sensitivity = ch_lfo_pm_sens(choffs); + if (pm_sensitivity != 0) + { + // raw PM value is -127..128 which is +/- 200 cents + // manual gives these magnitudes in cents: + // 0, +/-5, +/-10, +/-20, +/-50, +/-100, +/-400, +/-700 + // this roughly corresponds to shifting the 200-cent value: + // 0 >> 5, >> 4, >> 3, >> 2, >> 1, << 1, << 2 + if (pm_sensitivity < 6) + delta += int8_t(lfo_raw_pm) >> (6 - pm_sensitivity); + else + delta += int8_t(lfo_raw_pm) << (pm_sensitivity - 5); + } + uint32_t pm_sensitivity2 = ch_lfo2_pm_sens(choffs); + if (pm_sensitivity2 != 0) + { + // raw PM value is -127..128 which is +/- 200 cents + // manual gives these magnitudes in cents: + // 0, +/-5, +/-10, +/-20, +/-50, +/-100, +/-400, +/-700 + // this roughly corresponds to shifting the 200-cent value: + // 0 >> 5, >> 4, >> 3, >> 2, >> 1, << 1, << 2 + if (pm_sensitivity2 < 6) + delta += int8_t(lfo_raw_pm >> 8) >> (6 - pm_sensitivity2); + else + delta += int8_t(lfo_raw_pm >> 8) << (pm_sensitivity2 - 5); + } + + // apply delta and convert to a frequency number; this translation is + // the same as OPM so just re-use that helper + phase_step = opm_key_code_to_phase_step(cache.block_freq, delta); + + // apply detune based on the keycode + phase_step += cache.detune; + + // apply frequency multiplier (which is cached as an x.4 value) + return (phase_step * cache.multiple) >> 4; + } +} + + +//------------------------------------------------- +// log_keyon - log a key-on event +//------------------------------------------------- + +std::string opz_registers::log_keyon(uint32_t choffs, uint32_t opoffs) +{ + uint32_t chnum = choffs; + uint32_t opnum = opoffs; + + char buffer[256]; + int end = 0; + + end += snprintf(&buffer[end], sizeof(buffer) - end, "%u.%02u", chnum, opnum); + + if (op_fix_mode(opoffs)) + end += snprintf(&buffer[end], sizeof(buffer) - end, " fixfreq=%X fine=%X shift=%X", op_fix_frequency(opoffs), op_fine(opoffs), op_fix_range(opoffs)); + else + end += snprintf(&buffer[end], sizeof(buffer) - end, " freq=%04X dt2=%u fine=%X", ch_block_freq(choffs), op_detune2(opoffs), op_fine(opoffs)); + + end += snprintf(&buffer[end], sizeof(buffer) - end, " dt=%u fb=%u alg=%X mul=%X tl=%02X ksr=%u adsr=%02X/%02X/%02X/%X sl=%X out=%c%c", + op_detune(opoffs), + ch_feedback(choffs), + ch_algorithm(choffs), + op_multiple(opoffs), + op_total_level(opoffs), + op_ksr(opoffs), + op_attack_rate(opoffs), + op_decay_rate(opoffs), + op_sustain_rate(opoffs), + op_release_rate(opoffs), + op_sustain_level(opoffs), + ch_output_0(choffs) ? 'L' : '-', + ch_output_1(choffs) ? 'R' : '-'); + + if (op_eg_shift(opoffs) != 0) + end += snprintf(&buffer[end], sizeof(buffer) - end, " egshift=%u", op_eg_shift(opoffs)); + + bool am = (lfo_am_depth() != 0 && ch_lfo_am_sens(choffs) != 0 && op_lfo_am_enable(opoffs) != 0); + if (am) + end += snprintf(&buffer[end], sizeof(buffer) - end, " am=%u/%02X", ch_lfo_am_sens(choffs), lfo_am_depth()); + bool pm = (lfo_pm_depth() != 0 && ch_lfo_pm_sens(choffs) != 0); + if (pm) + end += snprintf(&buffer[end], sizeof(buffer) - end, " pm=%u/%02X", ch_lfo_pm_sens(choffs), lfo_pm_depth()); + if (am || pm) + end += snprintf(&buffer[end], sizeof(buffer) - end, " lfo=%02X/%c", lfo_rate(), "WQTN"[lfo_waveform()]); + + bool am2 = (lfo2_am_depth() != 0 && ch_lfo2_am_sens(choffs) != 0 && op_lfo_am_enable(opoffs) != 0); + if (am2) + end += snprintf(&buffer[end], sizeof(buffer) - end, " am2=%u/%02X", ch_lfo2_am_sens(choffs), lfo2_am_depth()); + bool pm2 = (lfo2_pm_depth() != 0 && ch_lfo2_pm_sens(choffs) != 0); + if (pm2) + end += snprintf(&buffer[end], sizeof(buffer) - end, " pm2=%u/%02X", ch_lfo2_pm_sens(choffs), lfo2_pm_depth()); + if (am2 || pm2) + end += snprintf(&buffer[end], sizeof(buffer) - end, " lfo2=%02X/%c", lfo2_rate(), "WQTN"[lfo2_waveform()]); + + if (op_reverb_rate(opoffs) != 0) + end += snprintf(&buffer[end], sizeof(buffer) - end, " rev=%u", op_reverb_rate(opoffs)); + if (op_waveform(opoffs) != 0) + end += snprintf(&buffer[end], sizeof(buffer) - end, " wf=%u", op_waveform(opoffs)); + if (noise_enable() && opoffs == 31) + end += snprintf(&buffer[end], sizeof(buffer) - end, " noise=1"); + + return buffer; +} + + + +//********************************************************* +// YM2414 +//********************************************************* + +//------------------------------------------------- +// ym2414 - constructor +//------------------------------------------------- + +ym2414::ym2414(ymfm_interface &intf) : + m_address(0), + m_fm(intf) +{ +} + + +//------------------------------------------------- +// reset - reset the system +//------------------------------------------------- + +void ym2414::reset() +{ + // reset the engines + m_fm.reset(); +} + + +//------------------------------------------------- +// save_restore - save or restore the data +//------------------------------------------------- + +void ym2414::save_restore(ymfm_saved_state &state) +{ + m_fm.save_restore(state); + state.save_restore(m_address); +} + + +//------------------------------------------------- +// read_status - read the status register +//------------------------------------------------- + +uint8_t ym2414::read_status() +{ + uint8_t result = m_fm.status(); + if (m_fm.intf().ymfm_is_busy()) + result |= fm_engine::STATUS_BUSY; + return result; +} + + +//------------------------------------------------- +// read - handle a read from the device +//------------------------------------------------- + +uint8_t ym2414::read(uint32_t offset) +{ + uint8_t result = 0xff; + switch (offset & 1) + { + case 0: // data port (unused) + debug::log_unexpected_read_write("Unexpected read from YM2414 offset %d\n", offset & 3); + break; + + case 1: // status port, YM2203 compatible + result = read_status(); + break; + } + return result; +} + + +//------------------------------------------------- +// write_address - handle a write to the address +// register +//------------------------------------------------- + +void ym2414::write_address(uint8_t data) +{ + // just set the address + m_address = data; +} + + +//------------------------------------------------- +// write - handle a write to the register +// interface +//------------------------------------------------- + +void ym2414::write_data(uint8_t data) +{ + // write the FM register + m_fm.write(m_address, data); + if (TEMPORARY_DEBUG_PRINTS) + { + switch (m_address & 0xe0) + { + case 0x00: + printf("CTL %02X = %02X\n", m_address, data); + break; + + case 0x20: + switch (m_address & 0xf8) + { + case 0x20: printf("R/FBL/ALG %d = %02X\n", m_address & 7, data); break; + case 0x28: printf("KC %d = %02X\n", m_address & 7, data); break; + case 0x30: printf("KF/M %d = %02X\n", m_address & 7, data); break; + case 0x38: printf("PMS/AMS %d = %02X\n", m_address & 7, data); break; + } + break; + + case 0x40: + if (bitfield(data, 7) == 0) + printf("DT1/MUL %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + else + printf("OW/FINE %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + break; + + case 0x60: + printf("TL %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + break; + + case 0x80: + printf("KRS/FIX/AR %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + break; + + case 0xa0: + printf("A/D1R %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + break; + + case 0xc0: + if (bitfield(data, 5) == 0) + printf("DT2/D2R %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + else + printf("EGS/REV %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + break; + + case 0xe0: + printf("D1L/RR %d.%d = %02X\n", m_address & 7, (m_address >> 3) & 3, data); + break; + } + } + + // special cases + if (m_address == 0x1b) + { + // writes to register 0x1B send the upper 2 bits to the output lines + m_fm.intf().ymfm_external_write(ACCESS_IO, 0, data >> 6); + } + + // mark busy for a bit + m_fm.intf().ymfm_set_busy_end(32 * m_fm.clock_prescale()); +} + + +//------------------------------------------------- +// write - handle a write to the register +// interface +//------------------------------------------------- + +void ym2414::write(uint32_t offset, uint8_t data) +{ + switch (offset & 1) + { + case 0: // address port + write_address(data); + break; + + case 1: // data port + write_data(data); + break; + } +} + + +//------------------------------------------------- +// generate - generate one sample of sound +//------------------------------------------------- + +void ym2414::generate(output_data *output, uint32_t numsamples) +{ + for (uint32_t samp = 0; samp < numsamples; samp++, output++) + { + // clock the system + m_fm.clock(fm_engine::ALL_CHANNELS); + + // update the FM content; YM2414 is full 14-bit with no intermediate clipping + m_fm.output(output->clear(), 0, 32767, fm_engine::ALL_CHANNELS); + + // unsure about YM2414 outputs; assume it is like YM2151 + output->roundtrip_fp(); + } +} + +} diff --git a/emu/cores/ymfmintf.cpp b/emu/cores/ymfmintf.cpp index 3dab5dc2..b48e540f 100644 --- a/emu/cores/ymfmintf.cpp +++ b/emu/cores/ymfmintf.cpp @@ -18,7 +18,9 @@ extern "C" } #include "ymfm/ymfm.h" +#include "ymfm/ymfm_fm.h" #include "ymfm/ymfm_opz.h" +#include "ymfm/ymfm_fm.ipp" // FourCC for ymfm cores @@ -192,9 +194,13 @@ static UINT8 ym2414_write(void* chip, UINT8 offset, UINT8 data) case 0x00: // Address info->address = data; info->chip->write_address(data); + // Debug + //printf("YM2414: Write address 0x%02X\n", data); break; case 0x01: // Data info->chip->write_data(data); + // Debug + //printf("YM2414: Write data 0x%02X to address 0x%02X\n", data, info->address); break; } diff --git a/opz_test.cpp b/opz_test.cpp new file mode 100644 index 00000000..307cce6d --- /dev/null +++ b/opz_test.cpp @@ -0,0 +1,232 @@ +// YM2414 (OPZ) Test Program +// Generates some test tones and saves to WAV file + +#include +#include +#include +#include +#include + +extern "C" { +#include "emu/EmuStructs.h" +#include "emu/SoundDevs.h" +#include "emu/cores/ymfmintf.h" +} + +// WAV file header structure +#pragma pack(push, 1) +struct WAVHeader { + char riff[4]; // "RIFF" + uint32_t fileSize; // File size - 8 + char wave[4]; // "WAVE" + char fmt[4]; // "fmt " + uint32_t fmtSize; // 16 for PCM + uint16_t audioFormat; // 1 for PCM + uint16_t numChannels; // 2 for stereo + uint32_t sampleRate; // Sample rate + uint32_t byteRate; // SampleRate * NumChannels * BitsPerSample/8 + uint16_t blockAlign; // NumChannels * BitsPerSample/8 + uint16_t bitsPerSample; // 16 + char data[4]; // "data" + uint32_t dataSize; // Size of audio data +}; +#pragma pack(pop) + +void WriteWAVHeader(FILE* f, uint32_t sampleRate, uint32_t numSamples) { + WAVHeader header; + memcpy(header.riff, "RIFF", 4); + memcpy(header.wave, "WAVE", 4); + memcpy(header.fmt, "fmt ", 4); + memcpy(header.data, "data", 4); + + header.fmtSize = 16; + header.audioFormat = 1; // PCM + header.numChannels = 2; // Stereo + header.sampleRate = sampleRate; + header.bitsPerSample = 16; + header.blockAlign = header.numChannels * header.bitsPerSample / 8; + header.byteRate = sampleRate * header.blockAlign; + header.dataSize = numSamples * header.blockAlign; + header.fileSize = 36 + header.dataSize; + + fwrite(&header, sizeof(header), 1, f); +} + +void YM2414_Write(const DEV_DEF* devDef, DEV_INFO* devInf, uint8_t reg, uint8_t data) { + typedef uint8_t (*WriteFn)(void*, uint8_t, uint8_t); + WriteFn writeFunc = (WriteFn)devDef->rwFuncs[0].funcPtr; + + // Write address + writeFunc(devInf->dataPtr, 0, reg); + // Write data + writeFunc(devInf->dataPtr, 1, data); +} + +int main(int argc, char* argv[]) { + printf("YM2414 (OPZ) Test - Generating test tones...\n"); + + // Initialize YM2414 + DEV_GEN_CFG devCfg; + // Clock calculation: sample_rate = clock / (prescale * operators) + // For 44100 Hz: clock = 44100 * 16 * 4 = 2,822,400 Hz + devCfg.clock = 2822400; // ~2.82 MHz for 44100 Hz output + devCfg.flags = 0; + devCfg.emuCore = 0; + + DEV_INFO devInf; + const DEV_DEF* devDef = sndDev_YM2414.cores[0]; // Get first core + uint8_t result = devDef->Start(&devCfg, &devInf); + if (result != 0) { + printf("Failed to initialize YM2414: error %d\n", result); + return 1; + } + + printf("YM2414 initialized at %d Hz\n", devInf.sampleRate); + + // Reset chip + devDef->Reset(devInf.dataPtr); + + // Program a basic FM voice on channel 0 + // Algorithm 0 (operators in series): Op1->Op2->Op3->Op4->Out + + // Channel 0 setup + YM2414_Write(devDef, &devInf, 0x20, 0x80); // CH0: Pan right ON, Key OFF, Feedback=0, Algorithm=0 + YM2414_Write(devDef, &devInf, 0x28, 0x40); // CH0: Key code (note) + YM2414_Write(devDef, &devInf, 0x30, 0x01); // CH0: Key fraction + Mono/Left OUT + YM2414_Write(devDef, &devInf, 0x38, 0x00); // CH0: LFO sensitivity + + // Operator 1 (Modulator) - offset 0x00 + YM2414_Write(devDef, &devInf, 0x40, 0x31); // OP1: DT=3, MUL=1 + YM2414_Write(devDef, &devInf, 0x60, 0x1F); // OP1: TL=31 (moderate level) + YM2414_Write(devDef, &devInf, 0x80, 0x9F); // OP1: KSR=2, AR=31 (fast attack) + YM2414_Write(devDef, &devInf, 0xA0, 0x85); // OP1: AM=1, DR=5 + YM2414_Write(devDef, &devInf, 0xC0, 0x05); // OP1: DT2=0, SR=5 + YM2414_Write(devDef, &devInf, 0xE0, 0xA3); // OP1: SL=10, RR=3 + + // Operator 2 (Modulator) - offset 0x08 + YM2414_Write(devDef, &devInf, 0x48, 0x31); // OP2: DT=3, MUL=1 + YM2414_Write(devDef, &devInf, 0x68, 0x20); // OP2: TL=32 + YM2414_Write(devDef, &devInf, 0x88, 0x9F); // OP2: KSR=2, AR=31 + YM2414_Write(devDef, &devInf, 0xA8, 0x85); // OP2: AM=1, DR=5 + YM2414_Write(devDef, &devInf, 0xC8, 0x05); // OP2: DT2=0, SR=5 + YM2414_Write(devDef, &devInf, 0xE8, 0xA3); // OP2: SL=10, RR=3 + + // Operator 3 (Modulator) - offset 0x10 + YM2414_Write(devDef, &devInf, 0x50, 0x31); // OP3: DT=3, MUL=1 + YM2414_Write(devDef, &devInf, 0x70, 0x20); // OP3: TL=32 + YM2414_Write(devDef, &devInf, 0x90, 0x9F); // OP3: KSR=2, AR=31 + YM2414_Write(devDef, &devInf, 0xB0, 0x85); // OP3: AM=1, DR=5 + YM2414_Write(devDef, &devInf, 0xD0, 0x05); // OP3: DT2=0, SR=5 + YM2414_Write(devDef, &devInf, 0xF0, 0xA3); // OP3: SL=10, RR=3 + + // Operator 4 (Carrier) - offset 0x18 + YM2414_Write(devDef, &devInf, 0x58, 0x31); // OP4: DT=3, MUL=1 + YM2414_Write(devDef, &devInf, 0x78, 0x00); // OP4: TL=0 (full volume) + YM2414_Write(devDef, &devInf, 0x98, 0x9F); // OP4: KSR=2, AR=31 + YM2414_Write(devDef, &devInf, 0xB8, 0x85); // OP4: AM=1, DR=5 + YM2414_Write(devDef, &devInf, 0xD8, 0x05); // OP4: DT2=0, SR=5 + YM2414_Write(devDef, &devInf, 0xF8, 0xA3); // OP4: SL=10, RR=3 + + // Set volume (channel output level) + YM2414_Write(devDef, &devInf, 0x00, 0x7F); // CH0: Volume=127 (max) + + // Prepare output buffer + const uint32_t duration = 3; // 3 seconds + const uint32_t totalSamples = devInf.sampleRate * duration; + int16_t* wavBuffer = (int16_t*)malloc(totalSamples * 2 * sizeof(int16_t)); + + const uint32_t chunkSize = 1024; + int32_t* tempL = (int32_t*)malloc(chunkSize * sizeof(int32_t)); + int32_t* tempR = (int32_t*)malloc(chunkSize * sizeof(int32_t)); + int32_t* outputs[2] = { tempL, tempR }; + + printf("Generating audio...\n"); + + // Note sequence: C, E, G, C (major arpeggio) + uint8_t notes[] = { 0x40, 0x44, 0x47, 0x4C }; // MIDI-like note codes + uint32_t noteLength = devInf.sampleRate / 2; // 0.5 seconds per note + + uint32_t samplePos = 0; + for (int note = 0; note < 4; note++) { + printf("Playing note %d...\n", note); + + // Set note frequency + YM2414_Write(devDef, &devInf, 0x28, notes[note]); + + // Key ON (bit 6 = 1 for key on) + YM2414_Write(devDef, &devInf, 0x20, 0xC0); // Key ON, Both pans, Feedback=0, Alg=0 + + // Generate note + uint32_t remaining = noteLength; + while (remaining > 0 && samplePos < totalSamples) { + uint32_t toRender = (remaining < chunkSize) ? remaining : chunkSize; + + devDef->Update(devInf.dataPtr, toRender, outputs); + + // Convert to 16-bit and interleave + for (uint32_t i = 0; i < toRender && samplePos < totalSamples; i++) { + wavBuffer[samplePos * 2 + 0] = (int16_t)tempL[i]; // Left + wavBuffer[samplePos * 2 + 1] = (int16_t)tempR[i]; // Right + samplePos++; + } + + remaining -= toRender; + } + + // Key OFF (bit 6 = 0 for key off) + YM2414_Write(devDef, &devInf, 0x20, 0x80); // Key OFF, Both pans + + // Brief silence between notes + uint32_t silence = devInf.sampleRate / 10; // 0.1 seconds + remaining = silence; + while (remaining > 0 && samplePos < totalSamples) { + uint32_t toRender = (remaining < chunkSize) ? remaining : chunkSize; + + devDef->Update(devInf.dataPtr, toRender, outputs); + + for (uint32_t i = 0; i < toRender && samplePos < totalSamples; i++) { + wavBuffer[samplePos * 2 + 0] = (int16_t)tempL[i]; + wavBuffer[samplePos * 2 + 1] = (int16_t)tempR[i]; + samplePos++; + } + + remaining -= toRender; + } + } + + // Fill remaining with silence + while (samplePos < totalSamples) { + uint32_t toRender = ((totalSamples - samplePos) < chunkSize) ? (totalSamples - samplePos) : chunkSize; + + devDef->Update(devInf.dataPtr, toRender, outputs); + + for (uint32_t i = 0; i < toRender; i++) { + wavBuffer[samplePos * 2 + 0] = (int16_t)(tempL[i] >> 8); + wavBuffer[samplePos * 2 + 1] = (int16_t)(tempR[i] >> 8); + samplePos++; + } + } + + // Write WAV file + printf("Writing opz_test.wav...\n"); + FILE* f = fopen("opz_test.wav", "wb"); + if (!f) { + printf("Failed to create output file\n"); + return 1; + } + + WriteWAVHeader(f, devInf.sampleRate, totalSamples); + fwrite(wavBuffer, sizeof(int16_t) * 2, totalSamples, f); + fclose(f); + + printf("Done! Created opz_test.wav (%d Hz, %d samples, %d seconds)\n", + devInf.sampleRate, totalSamples, duration); + + // Cleanup + free(wavBuffer); + free(tempL); + free(tempR); + devDef->Stop(devInf.dataPtr); + + return 0; +} From 84302dccd497f70c029fe23cbe6f3c4def78cfd3 Mon Sep 17 00:00:00 2001 From: vampi Date: Wed, 31 Dec 2025 16:57:16 +0200 Subject: [PATCH 3/3] remove opz test --- opz_test.cpp | 232 --------------------------------------------------- 1 file changed, 232 deletions(-) delete mode 100644 opz_test.cpp diff --git a/opz_test.cpp b/opz_test.cpp deleted file mode 100644 index 307cce6d..00000000 --- a/opz_test.cpp +++ /dev/null @@ -1,232 +0,0 @@ -// YM2414 (OPZ) Test Program -// Generates some test tones and saves to WAV file - -#include -#include -#include -#include -#include - -extern "C" { -#include "emu/EmuStructs.h" -#include "emu/SoundDevs.h" -#include "emu/cores/ymfmintf.h" -} - -// WAV file header structure -#pragma pack(push, 1) -struct WAVHeader { - char riff[4]; // "RIFF" - uint32_t fileSize; // File size - 8 - char wave[4]; // "WAVE" - char fmt[4]; // "fmt " - uint32_t fmtSize; // 16 for PCM - uint16_t audioFormat; // 1 for PCM - uint16_t numChannels; // 2 for stereo - uint32_t sampleRate; // Sample rate - uint32_t byteRate; // SampleRate * NumChannels * BitsPerSample/8 - uint16_t blockAlign; // NumChannels * BitsPerSample/8 - uint16_t bitsPerSample; // 16 - char data[4]; // "data" - uint32_t dataSize; // Size of audio data -}; -#pragma pack(pop) - -void WriteWAVHeader(FILE* f, uint32_t sampleRate, uint32_t numSamples) { - WAVHeader header; - memcpy(header.riff, "RIFF", 4); - memcpy(header.wave, "WAVE", 4); - memcpy(header.fmt, "fmt ", 4); - memcpy(header.data, "data", 4); - - header.fmtSize = 16; - header.audioFormat = 1; // PCM - header.numChannels = 2; // Stereo - header.sampleRate = sampleRate; - header.bitsPerSample = 16; - header.blockAlign = header.numChannels * header.bitsPerSample / 8; - header.byteRate = sampleRate * header.blockAlign; - header.dataSize = numSamples * header.blockAlign; - header.fileSize = 36 + header.dataSize; - - fwrite(&header, sizeof(header), 1, f); -} - -void YM2414_Write(const DEV_DEF* devDef, DEV_INFO* devInf, uint8_t reg, uint8_t data) { - typedef uint8_t (*WriteFn)(void*, uint8_t, uint8_t); - WriteFn writeFunc = (WriteFn)devDef->rwFuncs[0].funcPtr; - - // Write address - writeFunc(devInf->dataPtr, 0, reg); - // Write data - writeFunc(devInf->dataPtr, 1, data); -} - -int main(int argc, char* argv[]) { - printf("YM2414 (OPZ) Test - Generating test tones...\n"); - - // Initialize YM2414 - DEV_GEN_CFG devCfg; - // Clock calculation: sample_rate = clock / (prescale * operators) - // For 44100 Hz: clock = 44100 * 16 * 4 = 2,822,400 Hz - devCfg.clock = 2822400; // ~2.82 MHz for 44100 Hz output - devCfg.flags = 0; - devCfg.emuCore = 0; - - DEV_INFO devInf; - const DEV_DEF* devDef = sndDev_YM2414.cores[0]; // Get first core - uint8_t result = devDef->Start(&devCfg, &devInf); - if (result != 0) { - printf("Failed to initialize YM2414: error %d\n", result); - return 1; - } - - printf("YM2414 initialized at %d Hz\n", devInf.sampleRate); - - // Reset chip - devDef->Reset(devInf.dataPtr); - - // Program a basic FM voice on channel 0 - // Algorithm 0 (operators in series): Op1->Op2->Op3->Op4->Out - - // Channel 0 setup - YM2414_Write(devDef, &devInf, 0x20, 0x80); // CH0: Pan right ON, Key OFF, Feedback=0, Algorithm=0 - YM2414_Write(devDef, &devInf, 0x28, 0x40); // CH0: Key code (note) - YM2414_Write(devDef, &devInf, 0x30, 0x01); // CH0: Key fraction + Mono/Left OUT - YM2414_Write(devDef, &devInf, 0x38, 0x00); // CH0: LFO sensitivity - - // Operator 1 (Modulator) - offset 0x00 - YM2414_Write(devDef, &devInf, 0x40, 0x31); // OP1: DT=3, MUL=1 - YM2414_Write(devDef, &devInf, 0x60, 0x1F); // OP1: TL=31 (moderate level) - YM2414_Write(devDef, &devInf, 0x80, 0x9F); // OP1: KSR=2, AR=31 (fast attack) - YM2414_Write(devDef, &devInf, 0xA0, 0x85); // OP1: AM=1, DR=5 - YM2414_Write(devDef, &devInf, 0xC0, 0x05); // OP1: DT2=0, SR=5 - YM2414_Write(devDef, &devInf, 0xE0, 0xA3); // OP1: SL=10, RR=3 - - // Operator 2 (Modulator) - offset 0x08 - YM2414_Write(devDef, &devInf, 0x48, 0x31); // OP2: DT=3, MUL=1 - YM2414_Write(devDef, &devInf, 0x68, 0x20); // OP2: TL=32 - YM2414_Write(devDef, &devInf, 0x88, 0x9F); // OP2: KSR=2, AR=31 - YM2414_Write(devDef, &devInf, 0xA8, 0x85); // OP2: AM=1, DR=5 - YM2414_Write(devDef, &devInf, 0xC8, 0x05); // OP2: DT2=0, SR=5 - YM2414_Write(devDef, &devInf, 0xE8, 0xA3); // OP2: SL=10, RR=3 - - // Operator 3 (Modulator) - offset 0x10 - YM2414_Write(devDef, &devInf, 0x50, 0x31); // OP3: DT=3, MUL=1 - YM2414_Write(devDef, &devInf, 0x70, 0x20); // OP3: TL=32 - YM2414_Write(devDef, &devInf, 0x90, 0x9F); // OP3: KSR=2, AR=31 - YM2414_Write(devDef, &devInf, 0xB0, 0x85); // OP3: AM=1, DR=5 - YM2414_Write(devDef, &devInf, 0xD0, 0x05); // OP3: DT2=0, SR=5 - YM2414_Write(devDef, &devInf, 0xF0, 0xA3); // OP3: SL=10, RR=3 - - // Operator 4 (Carrier) - offset 0x18 - YM2414_Write(devDef, &devInf, 0x58, 0x31); // OP4: DT=3, MUL=1 - YM2414_Write(devDef, &devInf, 0x78, 0x00); // OP4: TL=0 (full volume) - YM2414_Write(devDef, &devInf, 0x98, 0x9F); // OP4: KSR=2, AR=31 - YM2414_Write(devDef, &devInf, 0xB8, 0x85); // OP4: AM=1, DR=5 - YM2414_Write(devDef, &devInf, 0xD8, 0x05); // OP4: DT2=0, SR=5 - YM2414_Write(devDef, &devInf, 0xF8, 0xA3); // OP4: SL=10, RR=3 - - // Set volume (channel output level) - YM2414_Write(devDef, &devInf, 0x00, 0x7F); // CH0: Volume=127 (max) - - // Prepare output buffer - const uint32_t duration = 3; // 3 seconds - const uint32_t totalSamples = devInf.sampleRate * duration; - int16_t* wavBuffer = (int16_t*)malloc(totalSamples * 2 * sizeof(int16_t)); - - const uint32_t chunkSize = 1024; - int32_t* tempL = (int32_t*)malloc(chunkSize * sizeof(int32_t)); - int32_t* tempR = (int32_t*)malloc(chunkSize * sizeof(int32_t)); - int32_t* outputs[2] = { tempL, tempR }; - - printf("Generating audio...\n"); - - // Note sequence: C, E, G, C (major arpeggio) - uint8_t notes[] = { 0x40, 0x44, 0x47, 0x4C }; // MIDI-like note codes - uint32_t noteLength = devInf.sampleRate / 2; // 0.5 seconds per note - - uint32_t samplePos = 0; - for (int note = 0; note < 4; note++) { - printf("Playing note %d...\n", note); - - // Set note frequency - YM2414_Write(devDef, &devInf, 0x28, notes[note]); - - // Key ON (bit 6 = 1 for key on) - YM2414_Write(devDef, &devInf, 0x20, 0xC0); // Key ON, Both pans, Feedback=0, Alg=0 - - // Generate note - uint32_t remaining = noteLength; - while (remaining > 0 && samplePos < totalSamples) { - uint32_t toRender = (remaining < chunkSize) ? remaining : chunkSize; - - devDef->Update(devInf.dataPtr, toRender, outputs); - - // Convert to 16-bit and interleave - for (uint32_t i = 0; i < toRender && samplePos < totalSamples; i++) { - wavBuffer[samplePos * 2 + 0] = (int16_t)tempL[i]; // Left - wavBuffer[samplePos * 2 + 1] = (int16_t)tempR[i]; // Right - samplePos++; - } - - remaining -= toRender; - } - - // Key OFF (bit 6 = 0 for key off) - YM2414_Write(devDef, &devInf, 0x20, 0x80); // Key OFF, Both pans - - // Brief silence between notes - uint32_t silence = devInf.sampleRate / 10; // 0.1 seconds - remaining = silence; - while (remaining > 0 && samplePos < totalSamples) { - uint32_t toRender = (remaining < chunkSize) ? remaining : chunkSize; - - devDef->Update(devInf.dataPtr, toRender, outputs); - - for (uint32_t i = 0; i < toRender && samplePos < totalSamples; i++) { - wavBuffer[samplePos * 2 + 0] = (int16_t)tempL[i]; - wavBuffer[samplePos * 2 + 1] = (int16_t)tempR[i]; - samplePos++; - } - - remaining -= toRender; - } - } - - // Fill remaining with silence - while (samplePos < totalSamples) { - uint32_t toRender = ((totalSamples - samplePos) < chunkSize) ? (totalSamples - samplePos) : chunkSize; - - devDef->Update(devInf.dataPtr, toRender, outputs); - - for (uint32_t i = 0; i < toRender; i++) { - wavBuffer[samplePos * 2 + 0] = (int16_t)(tempL[i] >> 8); - wavBuffer[samplePos * 2 + 1] = (int16_t)(tempR[i] >> 8); - samplePos++; - } - } - - // Write WAV file - printf("Writing opz_test.wav...\n"); - FILE* f = fopen("opz_test.wav", "wb"); - if (!f) { - printf("Failed to create output file\n"); - return 1; - } - - WriteWAVHeader(f, devInf.sampleRate, totalSamples); - fwrite(wavBuffer, sizeof(int16_t) * 2, totalSamples, f); - fclose(f); - - printf("Done! Created opz_test.wav (%d Hz, %d samples, %d seconds)\n", - devInf.sampleRate, totalSamples, duration); - - // Cleanup - free(wavBuffer); - free(tempL); - free(tempR); - devDef->Stop(devInf.dataPtr); - - return 0; -}