diff --git a/CMakeLists.txt b/CMakeLists.txt index 682475ba..80120b85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -277,6 +277,8 @@ src/engine/platform/sound/n163/n163.cpp src/engine/platform/sound/vic20sound.c +src/engine/platform/sound/vrcvi/vrcvi.cpp + src/engine/platform/ym2610Interface.cpp src/engine/blip_buf.c @@ -327,6 +329,7 @@ src/engine/platform/bubsyswsg.cpp src/engine/platform/n163.cpp src/engine/platform/pet.cpp src/engine/platform/vic20.cpp +src/engine/platform/vrc6.cpp src/engine/platform/dummy.cpp ) diff --git a/papers/doc/4-instrument/README.md b/papers/doc/4-instrument/README.md index 2cf4d25a..1ffff7e5 100644 --- a/papers/doc/4-instrument/README.md +++ b/papers/doc/4-instrument/README.md @@ -28,6 +28,7 @@ depending on the instrument type, there are currently 13 different types of an i - [Seta/Allumer X1-010](x1_010.md) - for use with Wavetable portion in Seta/Allumer X1-010. - [Konami SCC/Bubble System WSG](scc.md) - for use with Konami SCC and Wavetable portion in Bubble System's sound hardware. - [Namco 163](n163.md) - for use with Namco 163. +- [Konami VRC6](vrc6.md) - for use with VRC6's PSG sound source. # macros diff --git a/papers/doc/4-instrument/vrc6.md b/papers/doc/4-instrument/vrc6.md new file mode 100644 index 00000000..a4ea64a1 --- /dev/null +++ b/papers/doc/4-instrument/vrc6.md @@ -0,0 +1,7 @@ +# VRC6 instrument editor + +VRC6 instrument editor consists of only three macros: + +- [Volume] - volume sequence +- [Arpeggio] - pitch sequence +- [Duty cycle] - specifies duty cycle for pulse wave channels diff --git a/papers/doc/7-systems/README.md b/papers/doc/7-systems/README.md index 71ffb9f3..f759956f 100644 --- a/papers/doc/7-systems/README.md +++ b/papers/doc/7-systems/README.md @@ -29,5 +29,6 @@ this is a list of systems that Furnace supports, including each system's effects - [PC Speaker](pcspkr.md) - [Commodore PET](pet.md) - [Commodore VIC-20](vic20.md) +- [Konami VRC6](vrc6.md) Furnace also reads .dmf files with the [Yamaha YMU759](ymu759.md) system, but... diff --git a/papers/doc/7-systems/vrc6.md b/papers/doc/7-systems/vrc6.md new file mode 100644 index 00000000..63bda1ab --- /dev/null +++ b/papers/doc/7-systems/vrc6.md @@ -0,0 +1,16 @@ +# Konami VRC6 + +the most popular expansion chip to the NES' sound system. + +the chip has 2 pulse wave channels and one sawtooth channel. +volume register is 4 bit for pulse wave and 6 bit for sawtooth, but sawtooth output is corrupted when volume register value is too high. because this register is actually an 8 bit accumulator, its output may wrap around. + +pulse wave duty cycle is 8-level. it can be ignored and it has potential for DAC at this case: volume register in this mode is DAC output and it can be PCM playback through this mode. +Furnace supports this routine for PCM playback, but it consumes a lot of CPU time in real hardware (even if conjunction with VRC6's integrated IRQ timer). + +# effects + +these effects only are effective in the pulse channels. + +- `12xx`: set duty cycle (0 to 7). +- `17xx`: toggle PCM mode. \ No newline at end of file diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index ec7aa9a6..498630cc 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -49,6 +49,7 @@ #include "platform/n163.h" #include "platform/pet.h" #include "platform/vic20.h" +#include "platform/vrc6.h" #include "platform/dummy.h" #include "../ta-log.h" #include "song.h" @@ -287,6 +288,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_VIC20: dispatch=new DivPlatformVIC20; break; + case DIV_SYSTEM_VRC6: + dispatch=new DivPlatformVRC6; + break; default: logW("this system is not supported yet! using dummy platform.\n"); dispatch=new DivPlatformDummy; diff --git a/src/engine/platform/sound/vrcvi/vrcvi.cpp b/src/engine/platform/sound/vrcvi/vrcvi.cpp new file mode 100644 index 00000000..a8a289da --- /dev/null +++ b/src/engine/platform/sound/vrcvi/vrcvi.cpp @@ -0,0 +1,313 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Konami VRC VI sound emulation core + + It's one of NES mapper with built-in sound chip, and also one of 2 Konami VRCs with this feature. (rest one has OPLL derivatives.) + + It's also DACless like other sound chip and mapper-with-sound manufactured by konami, + the Chips 6 bit digital sound output is needs converted to analog sound output when you it want to make some sounds, or send to sound mixer. + + Its are used for Akumajou Densetsu (Japan release of Castlevania III), Madara, Esper Dream 2. + + The chip is installed in 351951 PCB and 351949A PCB. + + 351951 PCB is used exclusivly for Akumajou Densetsu, Small board has VRC VI, PRG and CHR ROM. + - It's configuration also calls VRC6a, iNES mapper 024. + + 351949A PCB is for Last 2 titles with VRC VI, Bigger board has VRC VI, PRG and CHR ROM, and Battery Backed 8K x 8 bit SRAM. + - Additionally, It's PRG A0 and A1 bit to VRC VI input is swapped, compare to above. + - It's configuration also calls VRC6b, iNES mapper 026. + + The chip itself has 053328, 053329, 053330 Revision, but Its difference between revision is unknown. + + Like other mappers for NES, It has internal timer - Its timer can be sync with scanline like other Konami mapper in this era. + + Register layout (Sound and Timer only; 351951 PCB case, 351949A swaps xxx1 and xxx2): + + Address Bits Description + 7654 3210 + + 9000-9002 Pulse 1 + + 9000 x--- ---- Pulse 1 Duty ignore + -xxx ---- Pulse 1 Duty cycle + ---- xxxx Pulse 1 Volume + 9001 xxxx xxxx Pulse 1 Pitch bit 0-7 + 9002 x--- ---- Pulse 1 Enable + ---- xxxx Pulse 1 Pitch bit 8-11 + + 9003 Sound control + + 9003 ---- -x-- 4 bit Frequency mode + ---- -0x- 8 bit Frequency mode + ---- ---x Halt + + a000-a002 Pulse 2 + + a000 x--- ---- Pulse 2 Duty ignore + -xxx ---- Pulse 2 Duty cycle + ---- xxxx Pulse 2 Volume + a001 xxxx xxxx Pulse 2 Pitch bit 0-7 + a002 x--- ---- Pulse 2 Enable + ---- xxxx Pulse 2 Pitch bit 8-11 + + b000-b002 Sawtooth + + b000 --xx xxxx Sawtooth Accumulate Rate + b001 xxxx xxxx Sawtooth Pitch bit 0-7 + b002 x--- ---- Sawtooth Enable + ---- xxxx Sawtooth Pitch bit 8-11 + + f000-f002 IRQ Timer + + f000 xxxx xxxx IRQ Timer latch + f001 ---- -0-- Sync with scanline + ---- --x- Enable timer + ---- ---x Enable timer after IRQ Acknowledge + f002 ---- ---- IRQ Acknowledge + + Frequency calculations: + + if 4 bit Frequency Mode then + Frequency: Input clock / (bit 8 to 11 of Pitch + 1) + end else if 8 bit Frequency Mode then + Frequency: Input clock / (bit 4 to 11 of Pitch + 1) + end else then + Frequency: Input clock / (Pitch + 1) + end +*/ + +#include "vrcvi.hpp" + +void vrcvi_core::tick() +{ + m_out = 0; + if (!m_control.m_halt) // Halt flag + { + // tick per each clock + for (auto & elem : m_pulse) + { + if (elem.tick()) + m_out += elem.m_control.m_volume; // add 4 bit pulse output + } + if (m_sawtooth.tick()) + m_out += bitfield(m_sawtooth.m_accum, 3, 5); // add 5 bit sawtooth output + } + if (m_timer.tick()) + m_timer.counter_tick(); +} + +void vrcvi_core::reset() +{ + for (auto & elem : m_pulse) + elem.reset(); + + m_sawtooth.reset(); + m_timer.reset(); + m_control.reset(); + m_out = 0; +} + +bool vrcvi_core::alu_t::tick() +{ + if (m_divider.m_enable) + { + const u16 temp = m_counter; + // post decrement + if (bitfield(m_host.m_control.m_shift, 1)) + { + m_counter = (m_counter & 0x0ff) | (bitfield(bitfield(m_counter, 8, 4) - 1, 0, 4) << 8); + m_counter = (m_counter & 0xf00) | (bitfield(bitfield(m_counter, 0, 8) - 1, 0, 8) << 0); + } + else if (bitfield(m_host.m_control.m_shift, 0)) + { + m_counter = (m_counter & 0x00f) | (bitfield(bitfield(m_counter, 4, 8) - 1, 0, 8) << 4); + m_counter = (m_counter & 0xff0) | (bitfield(bitfield(m_counter, 0, 4) - 1, 0, 4) << 0); + } + else + m_counter = bitfield(bitfield(m_counter, 0, 12) - 1, 0, 12); + + // carry handling + bool carry = bitfield(m_host.m_control.m_shift, 1) ? (bitfield(temp, 8, 4) == 0) : + (bitfield(m_host.m_control.m_shift, 0) ? (bitfield(temp, 4, 8) == 0) : + (bitfield(temp, 0, 12) == 0)); + if (carry) + m_counter = m_divider.m_divider; + + return carry; + } + return false; +} + +bool vrcvi_core::pulse_t::tick() +{ + if (!m_divider.m_enable) + { + m_cycle = 0; + return false; + } + + if (vrcvi_core::alu_t::tick()) + m_cycle = bitfield(m_cycle + 1, 0, 4); + + return m_control.m_mode ? true : ((m_cycle > m_control.m_duty) ? true : false); +} + +bool vrcvi_core::sawtooth_t::tick() +{ + if (!m_divider.m_enable) + { + m_accum = 0; + return false; + } + + if (vrcvi_core::alu_t::tick()) + { + if (bitfield(m_cycle++, 0)) // Even step only + m_accum += m_rate; + if (m_cycle >= 14) // Reset accumulator at every 14 cycles + { + m_accum = 0; + m_cycle = 0; + } + } + return (m_accum == 0) ? false : true; +} + +void vrcvi_core::alu_t::reset() +{ + m_divider.reset(); + m_counter = 0; + m_cycle = 0; +} + +void vrcvi_core::pulse_t::reset() +{ + vrcvi_core::alu_t::reset(); + m_control.reset(); +} + +void vrcvi_core::sawtooth_t::reset() +{ + vrcvi_core::alu_t::reset(); + m_rate = 0; + m_accum = 0; +} + +bool vrcvi_core::timer_t::tick() +{ + if (m_timer_control.m_enable) + { + if (!m_timer_control.m_sync) // scanline sync mode + { + m_prescaler -= 3; + if (m_prescaler <= 0) + { + m_prescaler += 341; + return true; + } + } + } + return (m_timer_control.m_enable && m_timer_control.m_sync) ? true : false; +} + +void vrcvi_core::timer_t::counter_tick() +{ + if (bitfield(++m_counter, 0, 8) == 0) + { + m_counter = m_counter_latch; + irq_set(); + } +} + +void vrcvi_core::timer_t::reset() +{ + m_timer_control.reset(); + m_prescaler = 341; + m_counter = m_counter_latch = 0; + irq_clear(); +} + +// Accessors + +void vrcvi_core::alu_t::divider_t::write(bool msb, u8 data) +{ + if (msb) + { + m_divider = (m_divider & ~0xf00) | (bitfield(data, 0, 4) << 8); + m_enable = bitfield(data, 7); + } + else + m_divider = (m_divider & ~0x0ff) | data; +} + + +void vrcvi_core::pulse_w(u8 voice, u8 address, u8 data) +{ + pulse_t &v = m_pulse[voice]; + switch (address) + { + case 0x00: // Control - 0x9000 (Pulse 1), 0xa000 (Pulse 2) + v.m_control.m_mode = bitfield(data, 7); + v.m_control.m_duty = bitfield(data, 4, 3); + v.m_control.m_volume = bitfield(data, 0, 4); + break; + case 0x01: // Pitch LSB - 0x9001/0x9002 (Pulse 1), 0xa001/0xa002 (Pulse 2) + v.m_divider.write(false, data); + break; + case 0x02: // Pitch MSB, Enable/Disable - 0x9002/0x9001 (Pulse 1), 0xa002/0xa001 (Pulse 2) + v.m_divider.write(true, data); + break; + } +} + +void vrcvi_core::saw_w(u8 address, u8 data) +{ + switch (address) + { + case 0x00: // Sawtooth Accumulate - 0xb000 + m_sawtooth.m_rate = bitfield(data, 0, 6); + break; + case 0x01: // Pitch LSB - 0xb001/0xb002 (Sawtooth) + m_sawtooth.m_divider.write(false, data); + break; + case 0x02: // Pitch MSB, Enable/Disable - 0xb002/0xb001 (Sawtooth) + m_sawtooth.m_divider.write(true, data); + break; + } +} + +void vrcvi_core::timer_w(u8 address, u8 data) +{ + switch (address) + { + case 0x00: // Timer latch - 0xf000 + m_timer.m_counter_latch = data; + break; + case 0x01: // Timer control - 0xf001/0xf002 + m_timer.m_timer_control.m_sync = bitfield(data, 2); + m_timer.m_timer_control.m_enable = bitfield(data, 1); + m_timer.m_timer_control.m_enable_ack = bitfield(data, 0); + if (m_timer.m_timer_control.m_enable) + { + m_timer.m_counter = m_timer.m_counter_latch; + m_timer.m_prescaler = 341; + } + m_timer.irq_clear(); + break; + case 0x02: // IRQ Acknowledge - 0xf002/0xf001 + m_timer.irq_clear(); + m_timer.m_timer_control.m_enable = m_timer.m_timer_control.m_enable_ack; + break; + } +} + +void vrcvi_core::control_w(u8 data) +{ + // Global control - 0x9003 + m_control.m_halt = bitfield(data, 0); + m_control.m_shift = bitfield(data, 1, 2); +} diff --git a/src/engine/platform/sound/vrcvi/vrcvi.hpp b/src/engine/platform/sound/vrcvi/vrcvi.hpp new file mode 100644 index 00000000..40b4245e --- /dev/null +++ b/src/engine/platform/sound/vrcvi/vrcvi.hpp @@ -0,0 +1,238 @@ +/* + License: BSD-3-Clause + see https://github.com/cam900/vgsound_emu/LICENSE for more details + + Copyright holder(s): cam900 + Konami VRC VI sound emulation core + + See vrcvi.cpp to more infos. +*/ + +#include +#include + +#ifndef _VGSOUND_EMU_VRCVI_HPP +#define _VGSOUND_EMU_VRCVI_HPP + +#pragma once + +namespace vrcvi +{ + typedef unsigned char u8; + typedef unsigned short u16; + typedef unsigned int u32; + typedef signed char s8; + typedef signed short s16; + + // get bitfield, bitfield(input, position, len) + template T bitfield(T in, u8 pos, u8 len = 1) + { + return (in >> pos) & (len ? (T(1 << len) - 1) : 1); + } +}; + +class vrcvi_intf +{ +public: + virtual void irq_w(bool irq) { } +}; + +using namespace vrcvi; +class vrcvi_core +{ +public: + friend class vrcvi_intf; + // constructor + vrcvi_core(vrcvi_intf &intf) + : m_pulse{*this,*this} + , m_sawtooth(*this) + , m_timer(*this) + , m_intf(intf) + { + } + // accessors, getters, setters + void pulse_w(u8 voice, u8 address, u8 data); + void saw_w(u8 address, u8 data); + void timer_w(u8 address, u8 data); + void control_w(u8 data); + + // internal state + void reset(); + void tick(); + + // 6 bit output + s8 out() { return m_out; } +private: + // Common ALU for sound channels + struct alu_t + { + alu_t(vrcvi_core &host) + : m_host(host) + { }; + + + virtual void reset(); + virtual bool tick(); + + struct divider_t + { + divider_t() + : m_divider(0) + , m_enable(0) + { }; + + void reset() + { + m_divider = 0; + m_enable = 0; + } + + void write(bool msb, u8 data); + + u16 m_divider : 12; // divider (pitch) + u16 m_enable : 1; // channel enable flag + }; + + vrcvi_core &m_host; + divider_t m_divider; + u16 m_counter = 0; // clock counter + u8 m_cycle = 0; // clock cycle + }; + + // 2 Pulse channels + struct pulse_t : alu_t + { + pulse_t(vrcvi_core &host) + : alu_t(host) + { }; + + virtual void reset() override; + virtual bool tick() override; + + // Control bits + struct pulse_control_t + { + pulse_control_t() + : m_mode(0) + , m_duty(0) + , m_volume(0) + { }; + + void reset() + { + m_mode = 0; + m_duty = 0; + m_volume = 0; + } + + u8 m_mode : 1; // duty toggle flag + u8 m_duty : 3; // 3 bit duty cycle + u8 m_volume : 4; // 4 bit volume + }; + + pulse_control_t m_control; + }; + + // 1 Sawtooth channel + struct sawtooth_t : alu_t + { + sawtooth_t(vrcvi_core &host) + : alu_t(host) + { }; + + virtual void reset() override; + virtual bool tick() override; + + u8 m_rate = 0; // sawtooth accumulate rate + u8 m_accum = 0; // sawtooth accumulator, high 5 bit is accumulated to output + }; + + // Internal timer + struct timer_t + { + timer_t(vrcvi_core &host) + : m_host(host) + { }; + + void reset(); + bool tick(); + void counter_tick(); + + // IRQ update + void update() { m_host.m_intf.irq_w(m_timer_control.m_irq_trigger); } + void irq_set() + { + if (!m_timer_control.m_irq_trigger) + { + m_timer_control.m_irq_trigger = 1; + update(); + } + } + void irq_clear() + { + if (m_timer_control.m_irq_trigger) + { + m_timer_control.m_irq_trigger = 0; + update(); + } + } + + // Control bits + struct timer_control_t + { + timer_control_t() + : m_irq_trigger(0) + , m_enable_ack(0) + , m_enable(0) + , m_sync(0) + { }; + + void reset() + { + m_irq_trigger = 0; + m_enable_ack = 0; + m_enable = 0; + m_sync = 0; + } + + u8 m_irq_trigger : 1; + u8 m_enable_ack : 1; + u8 m_enable : 1; + u8 m_sync : 1; + }; + + vrcvi_core &m_host; // host core + timer_control_t m_timer_control; // timer control bits + s16 m_prescaler = 341; // prescaler + u8 m_counter = 0; // clock counter + u8 m_counter_latch = 0; // clock counter latch + }; + + struct global_control_t + { + global_control_t() + : m_halt(0) + , m_shift(0) + { }; + + void reset() + { + m_halt = 0; + m_shift = 0; + } + + u8 m_halt : 1; // halt sound + u8 m_shift : 2; // 4/8 bit right shift + }; + + pulse_t m_pulse[2]; // 2 pulse channels + sawtooth_t m_sawtooth; // sawtooth channel + timer_t m_timer; // internal timer + global_control_t m_control; // control + + vrcvi_intf &m_intf; + + s8 m_out = 0; // 6 bit output +}; + +#endif diff --git a/src/engine/platform/vrc6.cpp b/src/engine/platform/vrc6.cpp new file mode 100644 index 00000000..630d5972 --- /dev/null +++ b/src/engine/platform/vrc6.cpp @@ -0,0 +1,496 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "vrc6.h" +#include "../engine.h" +#include +#include + +#define CHIP_DIVIDER 1 // 16 for pulse, 14 for sawtooth + +#define rWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } +#define chWrite(c,a,v) rWrite(0x9000+(c<<12)+(a&3),v) + +const char* regCheatSheetVRC6[]={ + "S0DutyVol", "9000", + "S0PeriodL", "9001", + "S0PeriodH", "9002", + "GlobalCtl", "9003", + "S1DutyVol", "A000", + "S1PeriodL", "A001", + "S1PeriodH", "A002", + "SawVolume", "B000", + "SawPeriodL", "B001", + "SawPeriodH", "B002", + "TimerLatch", "F000", + "TimerCtl", "F001", + "IRQAck", "F002", + NULL +}; + +const char** DivPlatformVRC6::getRegisterSheet() { + return regCheatSheetVRC6; +} + +const char* DivPlatformVRC6::getEffectName(unsigned char effect) { + switch (effect) { + case 0x12: + return "12xx: Set duty cycle (pulse: 0 to 7)"; + break; + case 0x17: + return "17xx: Toggle PCM mode (pulse channel)"; + break; + } + return NULL; +} + +void DivPlatformVRC6::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t i=start; irate) { + DivSample* s=parent->getSample(chan[i].dacSample); + if (s->samples<=0) { + chan[i].dacSample=-1; + chWrite(i,0,0); + continue; + } + unsigned char dacData=(((unsigned char)s->data8[chan[i].dacPos]^0x80)>>4); + chan[i].dacOut=MAX(0,MIN(15,(dacData*chan[i].outVol)>>4)); + if (!isMuted[i]) { + chWrite(i,0,0x80|chan[i].dacOut); + } + chan[i].dacPos++; + if (chan[i].dacPos>=s->samples) { + if (s->loopStart>=0 && s->loopStart<(int)s->samples) { + chan[i].dacPos=s->loopStart; + } else { + chan[i].dacSample=-1; + chWrite(i,0,0); + } + } + chan[i].dacPeriod-=rate; + } + } + } + + // VRC6 part + vrc6.tick(); + int sample=vrc6.out()<<9; // scale to 16 bit + if (sample>32767) sample=32767; + if (sample<-32768) sample=-32768; + bufL[i]=bufR[i]=sample; + + // Command part + while (!writes.empty()) { + QueuedWrite w=writes.front(); + switch (w.addr&0xf000) { + case 0x9000: // Pulse 1 + if (w.addr<=0x9003) { + if (w.addr==0x9003) { + vrc6.control_w(w.val); + } else if (w.addr<=0x9002) { + vrc6.pulse_w(0,w.addr&3,w.val); + } + regPool[w.addr-0x9000]=w.val; + } + break; + case 0xa000: // Pulse 2 + if (w.addr<=0xa002) { + vrc6.pulse_w(1,w.addr&3,w.val); + regPool[(w.addr-0xa000)+4]=w.val; + } + break; + case 0xb000: // Sawtooth + if (w.addr<=0xb002) { + vrc6.saw_w(w.addr&3,w.val); + regPool[(w.addr-0xb000)+7]=w.val; + } + break; + case 0xf000: // IRQ/Timer + if (w.addr<=0xf002) { + vrc6.timer_w(w.addr&3,w.val); + regPool[(w.addr-0xf000)+10]=w.val; + } + break; + } + writes.pop(); + } + } +} + +void DivPlatformVRC6::tick() { + for (int i=0; i<3; i++) { + chan[i].std.next(); + if (chan[i].std.hadVol) { + if (i==2) { // sawtooth + chan[i].outVol=((chan[i].vol&63)*MIN(63,chan[i].std.vol))/63; + if (chan[i].outVol<0) chan[i].outVol=0; + if (chan[i].outVol>63) chan[i].outVol=63; + if (!isMuted[i]) { + chWrite(i,0,chan[i].outVol); + } + } else { // pulse + chan[i].outVol=((chan[i].vol&15)*MIN(63,chan[i].std.vol))/63; + if (chan[i].outVol<0) chan[i].outVol=0; + if (chan[i].outVol>15) chan[i].outVol=15; + if ((!isMuted[i]) && (!chan[i].pcm)) { + chWrite(i,0,(chan[i].outVol&0xf)|((chan[i].duty&7)<<4)); + } + } + } + if (chan[i].std.hadArp) { + if (!chan[i].inPorta) { + if (chan[i].std.arpMode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); + } else { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arpMode && chan[i].std.finishedArp) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.hadDuty) { + chan[i].duty=chan[i].std.duty; + if ((!isMuted[i]) && (i!=2) && (!chan[i].pcm)) { // pulse + chWrite(i,0,(chan[i].outVol&0xf)|((chan[i].duty&7)<<4)); + } + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + if (i==2) { // sawtooth + chan[i].freq=parent->calcFreq(chan[i].baseFreq/14,chan[i].pitch,true)-1; + } else { // pulse + chan[i].freq=parent->calcFreq(chan[i].baseFreq/16,chan[i].pitch,true)-1; + if (chan[i].furnaceDac) { + double off=1.0; + if (chan[i].dacSample>=0 && chan[i].dacSamplesong.sampleLen) { + DivSample* s=parent->getSample(chan[i].dacSample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + chan[i].dacRate=((double)chipClock)/MAX(1,off*chan[i].freq); + if (dumpWrites) addWrite(0xffff0001+(i<<8),chan[i].dacRate); + } + } + if (chan[i].freq>4095) chan[i].freq=4095; + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].keyOn) { + //rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(ins->gb.soundLen&63))); + //rWrite(16+i*5+2,((chan[i].vol<<4))|(ins->gb.envLen&7)|((ins->gb.envDir&1)<<3)); + } + if (chan[i].keyOff) { + chWrite(i,2,0); + } else { + chWrite(i,1,chan[i].freq&0xff); + chWrite(i,2,0x80|((chan[i].freq>>8)&0xf)); + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformVRC6::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + if (c.chan!=2) { // pulse wave + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].pcm=true; + } else if (chan[c.chan].furnaceDac) { + chan[c.chan].pcm=false; + } + if (chan[c.chan].pcm) { + if (skipRegisterWrites) break; + if (ins->type==DIV_INS_AMIGA) { + chan[c.chan].dacSample=ins->amiga.initSample; + if (chan[c.chan].dacSample<0 || chan[c.chan].dacSample>=parent->song.sampleLen) { + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + break; + } else { + if (dumpWrites) { + chWrite(c.chan,2,0x80); + chWrite(c.chan,0,isMuted[c.chan]?0:0x80); + addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample); + } + } + chan[c.chan].dacPos=0; + chan[c.chan].dacPeriod=0; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].std.init(ins); + //chan[c.chan].keyOn=true; + chan[c.chan].furnaceDac=true; + } else { + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].note=c.value; + } + chan[c.chan].dacSample=12*sampleBank+chan[c.chan].note%12; + if (chan[c.chan].dacSample>=parent->song.sampleLen) { + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + break; + } else { + if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample); + } + chan[c.chan].dacPos=0; + chan[c.chan].dacPeriod=0; + chan[c.chan].dacRate=parent->getSample(chan[c.chan].dacSample)->rate; + if (dumpWrites) { + chWrite(c.chan,2,0x80); + chWrite(c.chan,0,isMuted[c.chan]?0:0x80); + addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dacRate); + } + chan[c.chan].furnaceDac=false; + } + break; + } + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + if (!isMuted[c.chan]) { + if (c.chan==2) { // sawtooth + chWrite(c.chan,0,chan[c.chan].vol); + } else if (!chan[c.chan].pcm) { + chWrite(c.chan,0,(chan[c.chan].vol&0xf)|((chan[c.chan].duty&7)<<4)); + } + } + break; + case DIV_CMD_NOTE_OFF: + chan[c.chan].dacSample=-1; + if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); + chan[c.chan].pcm=false; + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].std.init(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.hasVol) { + chan[c.chan].outVol=c.value; + } + if (!isMuted[c.chan]) { + if (chan[c.chan].active) { + if (c.chan==2) { // sawtooth + chWrite(c.chan,0,chan[c.chan].vol); + } else if (!chan[c.chan].pcm) { + chWrite(c.chan,0,(chan[c.chan].vol&0xf)|((chan[c.chan].duty&7)<<4)); + } + } + } + } + break; + case DIV_CMD_GET_VOLUME: + return chan[c.chan].vol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_STD_NOISE_MODE: + if ((c.chan!=2) && (!chan[c.chan].pcm)) { // pulse + chan[c.chan].duty=c.value; + } + break; + case DIV_CMD_SAMPLE_MODE: + if (c.chan!=2) { // pulse + chan[c.chan].pcm=c.value; + } + break; + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + break; + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + if (c.chan==2) return 63; // sawtooth has 6 bit volume + return 15; // pulse has 4 bit volume + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformVRC6::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + if (isMuted[ch]) { + chWrite(ch,0,0); + } else if (chan[ch].active) { + if (ch==2) { // sawtooth + chWrite(ch,0,chan[ch].outVol); + } else { + chWrite(ch,0,chan[ch].pcm?chan[ch].dacOut:((chan[ch].outVol&0xf)|((chan[ch].duty&7)<<4))); + } + } +} + +void DivPlatformVRC6::forceIns() { + for (int i=0; i<3; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + } +} + +void* DivPlatformVRC6::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformVRC6::getRegisterPool() { + return regPool; +} + +int DivPlatformVRC6::getRegisterPoolSize() { + return 13; +} + +void DivPlatformVRC6::reset() { + for (int i=0; i<3; i++) { + chan[i]=DivPlatformVRC6::Channel(); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + + sampleBank=0; + + vrc6.reset(); + // Initialize control register + rWrite(0x9003,0); + // Clear internal IRQ + rWrite(0xf000,0); + rWrite(0xf001,0); + rWrite(0xf002,0); +} + +bool DivPlatformVRC6::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformVRC6::setFlags(unsigned int flags) { + if (flags==2) { // Dendy + rate=COLOR_PAL*2.0/5.0; + } else if (flags==1) { // PAL + rate=COLOR_PAL*3.0/8.0; + } else { // NTSC + rate=COLOR_NTSC/2.0; + } + chipClock=rate; +} + +void DivPlatformVRC6::notifyInsDeletion(void* ins) { + for (int i=0; i<3; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformVRC6::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformVRC6::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformVRC6::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<3; i++) { + isMuted[i]=false; + } + setFlags(flags); + reset(); + return 3; +} + +void DivPlatformVRC6::quit() { +} + +DivPlatformVRC6::~DivPlatformVRC6() { +} diff --git a/src/engine/platform/vrc6.h b/src/engine/platform/vrc6.h new file mode 100644 index 00000000..05cd8941 --- /dev/null +++ b/src/engine/platform/vrc6.h @@ -0,0 +1,100 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _VRC6_H +#define _VRC6_H + +#include +#include "../dispatch.h" +#include "../macroInt.h" +#include "sound/vrcvi/vrcvi.hpp" + + +class DivPlatformVRC6: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch, note; + int dacPeriod, dacRate, dacOut; + unsigned int dacPos; + int dacSample; + unsigned char ins, duty; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, pcm, furnaceDac; + signed char vol, outVol; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + note(0), + dacPeriod(0), + dacRate(0), + dacOut(0), + dacPos(0), + dacSample(-1), + ins(-1), + duty(0), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + pcm(false), + furnaceDac(false), + vol(15), + outVol(15) {} + }; + Channel chan[3]; + bool isMuted[3]; + struct QueuedWrite { + unsigned short addr; + unsigned char val; + QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v) {} + }; + std::queue writes; + unsigned char sampleBank; + vrcvi_intf intf; + vrcvi_core vrc6; + unsigned char regPool[13]; + + friend void putDispatchChan(void*,int,int); + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool keyOffAffectsArp(int ch); + void setFlags(unsigned int flags); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + DivPlatformVRC6() : vrc6(intf) {}; + ~DivPlatformVRC6(); +}; + +#endif diff --git a/src/engine/platform/x1_010.h b/src/engine/platform/x1_010.h index 73cbaf6b..389fa7bf 100644 --- a/src/engine/platform/x1_010.h +++ b/src/engine/platform/x1_010.h @@ -20,7 +20,6 @@ #ifndef _X1_010_H #define _X1_010_H -#include #include "../dispatch.h" #include "../engine.h" #include "../macroInt.h" diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 10ab489d..3d990bb2 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -387,6 +387,18 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe return false; } break; + case DIV_SYSTEM_VRC6: + switch (effect) { + case 0x12: // duty or noise mode + dispatchCmd(DivCommand(DIV_CMD_STD_NOISE_MODE,ch,effectVal)); + break; + case 0x17: // PCM enable + dispatchCmd(DivCommand(DIV_CMD_SAMPLE_MODE,ch,(effectVal>0))); + break; + default: + return false; + } + break; default: return false; } diff --git a/src/gui/debug.cpp b/src/gui/debug.cpp index f5adb754..f1c974e8 100644 --- a/src/gui/debug.cpp +++ b/src/gui/debug.cpp @@ -38,6 +38,7 @@ #include "../engine/platform/amiga.h" #include "../engine/platform/x1_010.h" #include "../engine/platform/n163.h" +#include "../engine/platform/vrc6.h" #include "../engine/platform/dummy.h" #define GENESIS_DEBUG \ @@ -305,6 +306,33 @@ void putDispatchChan(void* data, int chanNum, int type) { ImGui::TextColored(ch->inPorta?colorOn:colorOff,">> InPorta"); break; } + case DIV_SYSTEM_VRC6: { + DivPlatformVRC6::Channel* ch=(DivPlatformVRC6::Channel*)data; + ImGui::Text("> VRC6"); + ImGui::Text("* freq: %d",ch->freq); + ImGui::Text(" - base: %d",ch->baseFreq); + ImGui::Text(" - pitch: %d",ch->pitch); + ImGui::Text("- note: %d",ch->note); + ImGui::Text("* DAC:"); + ImGui::Text(" - period: %d",ch->dacPeriod); + ImGui::Text(" - rate: %d",ch->dacRate); + ImGui::Text(" - out: %d",ch->dacOut); + ImGui::Text(" - pos: %d",ch->dacPos); + ImGui::Text(" - sample: %d",ch->dacSample); + ImGui::Text("- ins: %d",ch->ins); + ImGui::Text("- duty: %d",ch->duty); + ImGui::Text("- vol: %.2x",ch->vol); + ImGui::Text("- outVol: %.2x",ch->outVol); + ImGui::TextColored(ch->active?colorOn:colorOff,">> Active"); + ImGui::TextColored(ch->insChanged?colorOn:colorOff,">> InsChanged"); + ImGui::TextColored(ch->freqChanged?colorOn:colorOff,">> FreqChanged"); + ImGui::TextColored(ch->keyOn?colorOn:colorOff,">> KeyOn"); + ImGui::TextColored(ch->keyOff?colorOn:colorOff,">> KeyOff"); + ImGui::TextColored(ch->inPorta?colorOn:colorOff,">> InPorta"); + ImGui::TextColored(ch->pcm?colorOn:colorOff,">> DAC"); + ImGui::TextColored(ch->furnaceDac?colorOn:colorOff,">> FurnaceDAC"); + break; + } default: ImGui::Text("Unknown system! Help!"); break; diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 4dbf8c82..4aa0f29e 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -166,5 +166,6 @@ const int availableSystems[]={ DIV_SYSTEM_N163, DIV_SYSTEM_PET, DIV_SYSTEM_VIC20, + DIV_SYSTEM_VRC6, 0 // don't remove this last one! }; \ No newline at end of file diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 4d707f81..fadf3637 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -2091,7 +2091,7 @@ void FurnaceGUI::drawInsEdit() { if ((ins->type==DIV_INS_PCE || ins->type==DIV_INS_AY8930)) { volMax=31; } - if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_VERA) { + if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_VERA || ins->type==DIV_INS_VRC6) { volMax=63; } if (ins->type==DIV_INS_AMIGA) { @@ -2156,6 +2156,10 @@ void FurnaceGUI::drawInsEdit() { dutyLabel="Waveform pos."; dutyMax=255; } + if (ins->type==DIV_INS_VRC6) { + dutyLabel="Duty"; + dutyMax=7; + } bool dutyIsRel=(ins->type==DIV_INS_C64 && !ins->c64.dutyIsAbs); const char* waveLabel="Waveform"; @@ -2164,7 +2168,7 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_C64 || ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_SAA1099) { bitMode=true; } - if (ins->type==DIV_INS_STD) waveMax=0; + if (ins->type==DIV_INS_STD || ins->type==DIV_INS_VRC6) waveMax=0; if (ins->type==DIV_INS_TIA || ins->type==DIV_INS_VIC || ins->type==DIV_INS_OPLL) waveMax=15; if (ins->type==DIV_INS_C64) waveMax=4; if (ins->type==DIV_INS_SAA1099) waveMax=2; diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index 16d69cf7..cf4e3aae 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -191,6 +191,13 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "NES with Konami VRC6", { + DIV_SYSTEM_NES, 64, 0, 0, + DIV_SYSTEM_VRC6, 64, 0, 0, + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "NES with Konami VRC7", { DIV_SYSTEM_NES, 64, 0, 0, diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index 419decc4..8260b0a4 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -147,6 +147,7 @@ void FurnaceGUI::drawSysConf(int i) { } break; case DIV_SYSTEM_NES: + case DIV_SYSTEM_VRC6: if (ImGui::RadioButton("NTSC (1.79MHz)",flags==0)) { e->setSysFlags(i,0,restart); updateWindowTitle();