Merge branch 'cam900-vrc6'

This commit is contained in:
tildearrow 2022-03-27 22:19:08 -05:00
commit 56786d96d1
17 changed files with 1234 additions and 3 deletions

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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...

View file

@ -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.

View file

@ -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;

View file

@ -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<u32>(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);
}

View file

@ -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 <algorithm>
#include <memory>
#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<typename T> 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

View file

@ -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 <cstddef>
#include <math.h>
#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; i<start+len; i++) {
// PCM part
for (int i=0; i<2; i++) {
if (chan[i].pcm && chan[i].dacSample!=-1) {
chan[i].dacPeriod+=chan[i].dacRate;
if (chan[i].dacPeriod>rate) {
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].dacSample<parent->song.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<DivRegWrite>& 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() {
}

100
src/engine/platform/vrc6.h Normal file
View file

@ -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 <queue>
#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<QueuedWrite> 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<DivRegWrite>& 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

View file

@ -20,7 +20,6 @@
#ifndef _X1_010_H
#define _X1_010_H
#include <queue>
#include "../dispatch.h"
#include "../engine.h"
#include "../macroInt.h"

View file

@ -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;
}

View file

@ -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;

View file

@ -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!
};

View file

@ -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;

View file

@ -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,

View file

@ -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();