From f3a90d554c28013ab2b14b66939079dfbc745ded Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Mon, 21 Mar 2022 14:38:12 +0700 Subject: [PATCH 1/5] WIP SNES support, part 1 --- CMakeLists.txt | 3 + papers/doc/7-systems/README.md | 1 + papers/doc/7-systems/snes.md | 18 + src/engine/dispatchContainer.cpp | 4 + src/engine/instrument.h | 1 + src/engine/platform/snes.cpp | 351 ++++++ src/engine/platform/snes.h | 98 ++ src/engine/platform/sound/snes/SPC_DSP.cpp | 1027 +++++++++++++++++ src/engine/platform/sound/snes/SPC_DSP.h | 307 +++++ .../platform/sound/snes/blargg_common.h | 186 +++ .../platform/sound/snes/blargg_config.h | 26 + .../platform/sound/snes/blargg_endian.h | 185 +++ .../platform/sound/snes/blargg_source.h | 100 ++ src/gui/gui.h | 1 + src/gui/guiConst.cpp | 3 +- 15 files changed, 2310 insertions(+), 1 deletion(-) create mode 100644 papers/doc/7-systems/snes.md create mode 100644 src/engine/platform/snes.cpp create mode 100644 src/engine/platform/snes.h create mode 100644 src/engine/platform/sound/snes/SPC_DSP.cpp create mode 100644 src/engine/platform/sound/snes/SPC_DSP.h create mode 100644 src/engine/platform/sound/snes/blargg_common.h create mode 100644 src/engine/platform/sound/snes/blargg_config.h create mode 100644 src/engine/platform/sound/snes/blargg_endian.h create mode 100644 src/engine/platform/sound/snes/blargg_source.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d8491b455..bc720fc24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -377,6 +377,8 @@ src/engine/platform/sound/rf5c68.cpp src/engine/platform/sound/oki/okim6258.cpp src/engine/platform/sound/oki/msm6295.cpp +src/engine/platform/sound/snes/SPC_DSP.cpp + src/engine/platform/oplAInterface.cpp src/engine/platform/ym2608Interface.cpp src/engine/platform/ym2610Interface.cpp @@ -447,6 +449,7 @@ src/engine/platform/scc.cpp src/engine/platform/ymz280b.cpp src/engine/platform/namcowsg.cpp src/engine/platform/rf5c68.cpp +src/engine/platform/snes.cpp src/engine/platform/dummy.cpp ) diff --git a/papers/doc/7-systems/README.md b/papers/doc/7-systems/README.md index fcf2e8860..d8551e1c3 100644 --- a/papers/doc/7-systems/README.md +++ b/papers/doc/7-systems/README.md @@ -34,5 +34,6 @@ this is a list of systems that Furnace supports, including each system's effects - [Konami VRC6](vrc6.md) - [Famicom Disk System](fds.md) - [Nintendo MMC5](mmc5.md) +- [SNES](snes.md) Furnace also reads .dmf files with the [Yamaha YMU759](ymu759.md) system, but... diff --git a/papers/doc/7-systems/snes.md b/papers/doc/7-systems/snes.md new file mode 100644 index 000000000..5bb1ae5f1 --- /dev/null +++ b/papers/doc/7-systems/snes.md @@ -0,0 +1,18 @@ +# Super NES + +The successor to NES to compete with Genesis. Now packing with superior graphics and sample-based audio. Also known as Super Famicom. + +Its audio subsystem, developed by Sony, features the DSP chip, SPC700 microcontroller and 64KB of dedicated SRAM used by both. This whole system itself is pretty much a separate computer that the main CPU needs to upload its program and samples to. + +The DSP chip can + +Furnace communicates with the DSP directly and provide a full 64KB memory. This memory might be reduced excessively on ROM export to make up for playback engine and pattern data. + +# effects + +Note: this chip has a signed left/right level. Which can be used for inverted (surround) stereo. A signed 8-bit value means 80 - FF = -128 - -1. Other values work normally. A value of -128 is not recommended as it could cause overflows. + +- `10xx`: Set echo feedback level. This effect will apply to all channels. +- `11xx`: Set echo left level (signed 8-bit). This effect will apply to all channels. +- `12xx`: Set echo right level (signed 8-bit). This effect will apply to all channels. +- `13xx`: Set the length of the echo delay buffer. This will also affect the size of the sample RAM! diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 227f60679..5437d6fc8 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -64,6 +64,7 @@ #include "platform/scc.h" #include "platform/ymz280b.h" #include "platform/rf5c68.h" +#include "platform/snes.h" #include "platform/dummy.h" #include "../ta-log.h" #include "platform/zxbeeper.h" @@ -395,6 +396,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do dispatch=new DivPlatformNamcoWSG; ((DivPlatformNamcoWSG*)dispatch)->setDeviceType(30); break; + case DIV_SYSTEM_SNES: + dispatch=new DivPlatformSNES; + break; case DIV_SYSTEM_DUMMY: dispatch=new DivPlatformDummy; break; diff --git a/src/engine/instrument.h b/src/engine/instrument.h index c4af6f042..66197a0fe 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -60,6 +60,7 @@ enum DivInstrumentType: unsigned short { DIV_INS_SU=30, DIV_INS_NAMCO=31, DIV_INS_OPL_DRUMS=32, + DIV_INS_SNES=33, DIV_INS_MAX, DIV_INS_NULL }; diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp new file mode 100644 index 000000000..de96ee968 --- /dev/null +++ b/src/engine/platform/snes.cpp @@ -0,0 +1,351 @@ +/** + * 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 "snes.h" +#include "../engine.h" +#include + +#define CHIP_DIVIDER 16 + +#define rWrite(a,v) {dsp->write(a,v); regPool[(a)&0x7f]=v; } + +const char* regCheatSheetSNESDSP[]={ + "VxVOLL", "x0", + "VxVOLR", "x1", + "VxPITCHL", "x2", + "VxPITCHH", "x3", + "VxSRCN", "x4", + "VxADSR1", "x5", + "VxADSR2", "x6", + "VxGAIN", "x7", + "VxENVX", "x8", + "VxOUTX", "x9", + "FIRx", "xF", + + "MVOLL", "0C", + "MVOLR", "1C", + "EVOLL", "2C", + "EVOLR", "3C", + "KON", "4C", + "KOFF", "5C", + "FLG", "6C", + "ENDX", "7C", + + "EFB", "0D", + "PMON", "2D", + "NON", "3D", + "EON", "4D", + "DIR", "5D", + "ESA", "6D", + "EDL", "7D", + NULL +}; + +const char** DivPlatformSNES::getRegisterSheet() { + return regCheatSheetSNESDSP; +} + +void DivPlatformSNES::acquire(short* bufL, short* bufR, size_t start, size_t len) { + for (size_t h=start; hset_output(out,1); + dsp->run(32); + bufL[h]=out[0]; + bufR[h]=out[1]; + } +} + +void DivPlatformSNES::tick() { + for (int i=0; i<8; i++) { + chan[i].std.next(); + if (chan[i].std.hadVol) { + chan[i].outVol=((chan[i].vol%65)*MIN(64,chan[i].std.vol))>>6; + } + double off=1.0; + if (chan[i].sample>=0 && chan[i].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[i].sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + if (chan[i].std.hadArp) { + if (!chan[i].inPorta) { + if (chan[i].std.arpMode) { + chan[i].baseFreq=off*NOTE_PERIODIC(chan[i].std.arp); + } else { + chan[i].baseFreq=off*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=off*NOTE_PERIODIC(chan[i].note); + chan[i].freqChanged=true; + } + } + if (chan[i].std.hadWave) { + if (chan[i].wave!=chan[i].std.wave) { + chan[i].wave=chan[i].std.wave; + if (!chan[i].keyOff) chan[i].keyOn=true; + } + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + //DivInstrument* ins=parent->getIns(chan[i].ins); + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); + if (chan[i].freq>4095) chan[i].freq=4095; + if (chan[i].note>0x5d) chan[i].freq=0x01; + if (chan[i].keyOn) { + if (chan[i].wave<0) { + chan[i].wave=0; + } + } + if (chan[i].keyOff) { + } + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } +} + +int DivPlatformSNES::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + chan[c.chan].sample=ins->amiga.initSample; + double off=1.0; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=off*NOTE_PERIODIC(c.value); + } + if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { + chan[c.chan].sample=-1; + } + if (chan[c.chan].setPos) { + chan[c.chan].setPos=false; + } else { + chan[c.chan].audPos=0; + } + chan[c.chan].audSub=0; + if (c.value!=DIV_NOTE_NULL) { + 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(ins); + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].sample=-1; + 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; + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.hasVol) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + chan[c.chan].wave=c.value; + chan[c.chan].keyOn=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_LEGATO: { + double off=1.0; + if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[c.chan].sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + chan[c.chan].baseFreq=off*NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp-12):(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_SAMPLE_POS: + chan[c.chan].audPos=c.value; + chan[c.chan].setPos=true; + break; + case DIV_CMD_GET_VOLMAX: + return 127; + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformSNES::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; +} + +void DivPlatformSNES::forceIns() { + for (int i=0; i<8; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + chan[i].audPos=131072; + chan[i].audDat=0; + chan[i].sample=-1; + } +} + +void* DivPlatformSNES::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformSNES::getRegisterPool() { + // get states from emulator + for (int i=0; i<0x80; i+=0x10) { + regPool[i+8]=dsp->read(i+8); + regPool[i+9]=dsp->read(i+9); + } + return regPool; +} + +int DivPlatformSNES::getRegisterPoolSize() { + return 128; +} + +void DivPlatformSNES::reset() { + for (int i=0; i<8; i++) { + chan[i]=Channel(); + } + dsp->init(&aram); + dsp->set_output(NULL, 0); + memset(regPool,0,128); +} + +bool DivPlatformSNES::isStereo() { + return true; +} + +bool DivPlatformSNES::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformSNES::notifyInsChange(int ins) { + for (int i=0; i<8; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } +} + +void DivPlatformSNES::notifyWaveChange(int wave) { + // TODO when wavetables are added +} + +void DivPlatformSNES::notifyInsDeletion(void* ins) { + for (int i=0; i<8; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformSNES::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformSNES::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformSNES::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dsp=new SPC_DSP; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<8; i++) { + isMuted[i]=false; + } + chipClock=1024000; + rate=chipClock/32; + reset(); + return 8; +} + +void DivPlatformSNES::quit() { + delete dsp; +} diff --git a/src/engine/platform/snes.h b/src/engine/platform/snes.h new file mode 100644 index 000000000..93564ee58 --- /dev/null +++ b/src/engine/platform/snes.h @@ -0,0 +1,98 @@ +/** + * 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 _SNES_H +#define _SNES_H + +#include "../dispatch.h" +#include "../macroInt.h" +#include +#include "sound/snes/SPC_DSP.h" + +class DivPlatformSNES: public DivDispatch { + struct Channel { + int freq, baseFreq, pitch; + unsigned int audLoc; + unsigned short audLen; + unsigned int audPos; + int audSub; + signed char audDat; + int sample, wave; + unsigned char ins; + int busClock; + int note; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave, setPos; + signed char vol, outVol; + DivMacroInt std; + Channel(): + freq(0), + baseFreq(0), + pitch(0), + audLoc(0), + audLen(0), + audPos(0), + audSub(0), + audDat(0), + sample(-1), + wave(0), + ins(-1), + busClock(0), + note(0), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + useWave(false), + setPos(false), + vol(64), + outVol(64) {} + }; + Channel chan[8]; + bool isMuted[8]; + + unsigned char regPool[0x80]; + unsigned char aram[0x10000]; + SPC_DSP* dsp; + 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 isStereo(); + bool keyOffAffectsArp(int ch); + void notifyInsChange(int ins); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); +}; + +#endif diff --git a/src/engine/platform/sound/snes/SPC_DSP.cpp b/src/engine/platform/sound/snes/SPC_DSP.cpp new file mode 100644 index 000000000..abbf827db --- /dev/null +++ b/src/engine/platform/sound/snes/SPC_DSP.cpp @@ -0,0 +1,1027 @@ +// snes_spc 0.9.0. http://www.slack.net/~ant/ + +#include "SPC_DSP.h" + +#include "blargg_endian.h" +#include + +/* Copyright (C) 2007 Shay Green. This module is free software; you +can redistribute it and/or modify it under the terms of the GNU Lesser +General Public License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. This +module 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 Lesser General Public License for more +details. You should have received a copy of the GNU Lesser General Public +License along with this module; if not, write to the Free Software Foundation, +Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + +#include "blargg_source.h" + +#ifdef BLARGG_ENABLE_OPTIMIZER + #include BLARGG_ENABLE_OPTIMIZER +#endif + +#if INT_MAX < 0x7FFFFFFF + #error "Requires that int type have at least 32 bits" +#endif + +// TODO: add to blargg_endian.h +#define GET_LE16SA( addr ) ((BOOST::int16_t) GET_LE16( addr )) +#define GET_LE16A( addr ) GET_LE16( addr ) +#define SET_LE16A( addr, data ) SET_LE16( addr, data ) + +static BOOST::uint8_t const initial_regs [SPC_DSP::register_count] = +{ + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + +// 0x45,0x8B,0x5A,0x9A,0xE4,0x82,0x1B,0x78,0x00,0x00,0xAA,0x96,0x89,0x0E,0xE0,0x80, +// 0x2A,0x49,0x3D,0xBA,0x14,0xA0,0xAC,0xC5,0x00,0x00,0x51,0xBB,0x9C,0x4E,0x7B,0xFF, +// 0xF4,0xFD,0x57,0x32,0x37,0xD9,0x42,0x22,0x00,0x00,0x5B,0x3C,0x9F,0x1B,0x87,0x9A, +// 0x6F,0x27,0xAF,0x7B,0xE5,0x68,0x0A,0xD9,0x00,0x00,0x9A,0xC5,0x9C,0x4E,0x7B,0xFF, +// 0xEA,0x21,0x78,0x4F,0xDD,0xED,0x24,0x14,0x00,0x00,0x77,0xB1,0xD1,0x36,0xC1,0x67, +// 0x52,0x57,0x46,0x3D,0x59,0xF4,0x87,0xA4,0x00,0x00,0x7E,0x44,0x9C,0x4E,0x7B,0xFF, +// 0x75,0xF5,0x06,0x97,0x10,0xC3,0x24,0xBB,0x00,0x00,0x7B,0x7A,0xE0,0x60,0x12,0x0F, +// 0xF7,0x74,0x1C,0xE5,0x39,0x3D,0x73,0xC1,0x00,0x00,0x7A,0xB3,0xFF,0x4E,0x7B,0xFF +}; + +// if ( io < -32768 ) io = -32768; +// if ( io > 32767 ) io = 32767; +#define CLAMP16( io )\ +{\ + if ( (int16_t) io != io )\ + io = (io >> 31) ^ 0x7FFF;\ +} + +// Access global DSP register +#define REG(n) m.regs [r_##n] + +// Access voice DSP register +#define VREG(r,n) r [v_##n] + +#define WRITE_SAMPLES( l, r, out ) \ +{\ + out [0] = l;\ + out [1] = r;\ + out += 2;\ + if ( out >= m.out_end )\ + {\ + check( out == m.out_end );\ + check( m.out_end != &m.extra [extra_size] || \ + (m.extra <= m.out_begin && m.extra < &m.extra [extra_size]) );\ + out = m.extra;\ + m.out_end = &m.extra [extra_size];\ + }\ +}\ + +void SPC_DSP::set_output( sample_t* out, int size ) +{ + require( (size & 1) == 0 ); // must be even + if ( !out ) + { + out = m.extra; + size = extra_size; + } + m.out_begin = out; + m.out = out; + m.out_end = out + size; +} + +// Volume registers and efb are signed! Easy to forget int8_t cast. +// Prefixes are to avoid accidental use of locals with same names. + +// Gaussian interpolation + +static short const gauss [512] = +{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, + 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, + 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, + 11, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 15, 16, 16, 17, 17, + 18, 19, 19, 20, 20, 21, 21, 22, 23, 23, 24, 24, 25, 26, 27, 27, + 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 36, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 58, 59, 60, 61, 62, 64, 65, 66, 67, 69, 70, 71, 73, 74, 76, 77, + 78, 80, 81, 83, 84, 86, 87, 89, 90, 92, 94, 95, 97, 99, 100, 102, + 104, 106, 107, 109, 111, 113, 115, 117, 118, 120, 122, 124, 126, 128, 130, 132, + 134, 137, 139, 141, 143, 145, 147, 150, 152, 154, 156, 159, 161, 163, 166, 168, + 171, 173, 175, 178, 180, 183, 186, 188, 191, 193, 196, 199, 201, 204, 207, 210, + 212, 215, 218, 221, 224, 227, 230, 233, 236, 239, 242, 245, 248, 251, 254, 257, + 260, 263, 267, 270, 273, 276, 280, 283, 286, 290, 293, 297, 300, 304, 307, 311, + 314, 318, 321, 325, 328, 332, 336, 339, 343, 347, 351, 354, 358, 362, 366, 370, + 374, 378, 381, 385, 389, 393, 397, 401, 405, 410, 414, 418, 422, 426, 430, 434, + 439, 443, 447, 451, 456, 460, 464, 469, 473, 477, 482, 486, 491, 495, 499, 504, + 508, 513, 517, 522, 527, 531, 536, 540, 545, 550, 554, 559, 563, 568, 573, 577, + 582, 587, 592, 596, 601, 606, 611, 615, 620, 625, 630, 635, 640, 644, 649, 654, + 659, 664, 669, 674, 678, 683, 688, 693, 698, 703, 708, 713, 718, 723, 728, 732, + 737, 742, 747, 752, 757, 762, 767, 772, 777, 782, 787, 792, 797, 802, 806, 811, + 816, 821, 826, 831, 836, 841, 846, 851, 855, 860, 865, 870, 875, 880, 884, 889, + 894, 899, 904, 908, 913, 918, 923, 927, 932, 937, 941, 946, 951, 955, 960, 965, + 969, 974, 978, 983, 988, 992, 997,1001,1005,1010,1014,1019,1023,1027,1032,1036, +1040,1045,1049,1053,1057,1061,1066,1070,1074,1078,1082,1086,1090,1094,1098,1102, +1106,1109,1113,1117,1121,1125,1128,1132,1136,1139,1143,1146,1150,1153,1157,1160, +1164,1167,1170,1174,1177,1180,1183,1186,1190,1193,1196,1199,1202,1205,1207,1210, +1213,1216,1219,1221,1224,1227,1229,1232,1234,1237,1239,1241,1244,1246,1248,1251, +1253,1255,1257,1259,1261,1263,1265,1267,1269,1270,1272,1274,1275,1277,1279,1280, +1282,1283,1284,1286,1287,1288,1290,1291,1292,1293,1294,1295,1296,1297,1297,1298, +1299,1300,1300,1301,1302,1302,1303,1303,1303,1304,1304,1304,1304,1304,1305,1305, +}; + +inline int SPC_DSP::interpolate( voice_t const* v ) +{ + // Make pointers into gaussian based on fractional position between samples + int offset = v->interp_pos >> 4 & 0xFF; + short const* fwd = gauss + 255 - offset; + short const* rev = gauss + offset; // mirror left half of gaussian + + int const* in = &v->buf [(v->interp_pos >> 12) + v->buf_pos]; + int out; + out = (fwd [ 0] * in [0]) >> 11; + out += (fwd [256] * in [1]) >> 11; + out += (rev [256] * in [2]) >> 11; + out = (int16_t) out; + out += (rev [ 0] * in [3]) >> 11; + + CLAMP16( out ); + out &= ~1; + return out; +} + + +//// Counters + +int const simple_counter_range = 2048 * 5 * 3; // 30720 + +static unsigned const counter_rates [32] = +{ + simple_counter_range + 1, // never fires + 2048, 1536, + 1280, 1024, 768, + 640, 512, 384, + 320, 256, 192, + 160, 128, 96, + 80, 64, 48, + 40, 32, 24, + 20, 16, 12, + 10, 8, 6, + 5, 4, 3, + 2, + 1 +}; + +static unsigned const counter_offsets [32] = +{ + 1, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 536, 0, 1040, + 0, + 0 +}; + +inline void SPC_DSP::init_counter() +{ + m.counter = 0; +} + +inline void SPC_DSP::run_counters() +{ + if ( --m.counter < 0 ) + m.counter = simple_counter_range - 1; +} + +inline unsigned SPC_DSP::read_counter( int rate ) +{ + return ((unsigned) m.counter + counter_offsets [rate]) % counter_rates [rate]; +} + + +//// Envelope + +inline void SPC_DSP::run_envelope( voice_t* const v ) +{ + int env = v->env; + if ( v->env_mode == env_release ) // 60% + { + if ( (env -= 0x8) < 0 ) + env = 0; + v->env = env; + } + else + { + int rate; + int env_data = VREG(v->regs,adsr1); + if ( m.t_adsr0 & 0x80 ) // 99% ADSR + { + if ( v->env_mode >= env_decay ) // 99% + { + env--; + env -= env >> 8; + rate = env_data & 0x1F; + if ( v->env_mode == env_decay ) // 1% + rate = (m.t_adsr0 >> 3 & 0x0E) + 0x10; + } + else // env_attack + { + rate = (m.t_adsr0 & 0x0F) * 2 + 1; + env += rate < 31 ? 0x20 : 0x400; + } + } + else // GAIN + { + int mode; + env_data = VREG(v->regs,gain); + mode = env_data >> 5; + if ( mode < 4 ) // direct + { + env = env_data * 0x10; + rate = 31; + } + else + { + rate = env_data & 0x1F; + if ( mode == 4 ) // 4: linear decrease + { + env -= 0x20; + } + else if ( mode < 6 ) // 5: exponential decrease + { + env--; + env -= env >> 8; + } + else // 6,7: linear increase + { + env += 0x20; + if ( mode > 6 && (unsigned) v->hidden_env >= 0x600 ) + env += 0x8 - 0x20; // 7: two-slope linear increase + } + } + } + + // Sustain level + if ( (env >> 8) == (env_data >> 5) && v->env_mode == env_decay ) + v->env_mode = env_sustain; + + v->hidden_env = env; + + // unsigned cast because linear decrease going negative also triggers this + if ( (unsigned) env > 0x7FF ) + { + env = (env < 0 ? 0 : 0x7FF); + if ( v->env_mode == env_attack ) + v->env_mode = env_decay; + } + + if ( !read_counter( rate ) ) + v->env = env; // nothing else is controlled by the counter + } +} + + +//// BRR Decoding + +inline void SPC_DSP::decode_brr( voice_t* v ) +{ + // Arrange the four input nybbles in 0xABCD order for easy decoding + int nybbles = m.t_brr_byte * 0x100 + m.ram [(v->brr_addr + v->brr_offset + 1) & 0xFFFF]; + + int const header = m.t_brr_header; + + // Write to next four samples in circular buffer + int* pos = &v->buf [v->buf_pos]; + int* end; + if ( (v->buf_pos += 4) >= brr_buf_size ) + v->buf_pos = 0; + + // Decode four samples + for ( end = pos + 4; pos < end; pos++, nybbles <<= 4 ) + { + // Extract nybble and sign-extend + int s = (int16_t) nybbles >> 12; + + // Shift sample based on header + int const shift = header >> 4; + s = (s << shift) >> 1; + if ( shift >= 0xD ) // handle invalid range + s = (s >> 25) << 11; // same as: s = (s < 0 ? -0x800 : 0) + + // Apply IIR filter (8 is the most commonly used) + int const filter = header & 0x0C; + int const p1 = pos [brr_buf_size - 1]; + int const p2 = pos [brr_buf_size - 2] >> 1; + if ( filter >= 8 ) + { + s += p1; + s -= p2; + if ( filter == 8 ) // s += p1 * 0.953125 - p2 * 0.46875 + { + s += p2 >> 4; + s += (p1 * -3) >> 6; + } + else // s += p1 * 0.8984375 - p2 * 0.40625 + { + s += (p1 * -13) >> 7; + s += (p2 * 3) >> 4; + } + } + else if ( filter ) // s += p1 * 0.46875 + { + s += p1 >> 1; + s += (-p1) >> 5; + } + + // Adjust and write sample + CLAMP16( s ); + s = (int16_t) (s * 2); + pos [brr_buf_size] = pos [0] = s; // second copy simplifies wrap-around + } +} + + +//// Misc + +#define MISC_CLOCK( n ) inline void SPC_DSP::misc_##n() + +MISC_CLOCK( 27 ) +{ + m.t_pmon = REG(pmon) & 0xFE; // voice 0 doesn't support PMON +} +MISC_CLOCK( 28 ) +{ + m.t_non = REG(non); + m.t_eon = REG(eon); + m.t_dir = REG(dir); +} +MISC_CLOCK( 29 ) +{ + if ( (m.every_other_sample ^= 1) != 0 ) + m.new_kon &= ~m.kon; // clears KON 63 clocks after it was last read +} +MISC_CLOCK( 30 ) +{ + if ( m.every_other_sample ) + { + m.kon = m.new_kon; + m.t_koff = REG(koff) | m.mute_mask; + } + + run_counters(); + + // Noise + if ( !read_counter( REG(flg) & 0x1F ) ) + { + int feedback = (m.noise << 13) ^ (m.noise << 14); + m.noise = (feedback & 0x4000) ^ (m.noise >> 1); + } +} + + +//// Voices + +#define VOICE_CLOCK( n ) void SPC_DSP::voice_##n( voice_t* const v ) + +inline VOICE_CLOCK( V1 ) +{ + m.t_dir_addr = (m.t_dir * 0x100 + m.t_srcn * 4) & 0xffff; + m.t_srcn = VREG(v->regs,srcn); +} +inline VOICE_CLOCK( V2 ) +{ + // Read sample pointer (ignored if not needed) + uint8_t const* entry = &m.ram [m.t_dir_addr]; + if ( !v->kon_delay ) + entry += 2; + m.t_brr_next_addr = GET_LE16A( entry ); + + m.t_adsr0 = VREG(v->regs,adsr0); + + // Read pitch, spread over two clocks + m.t_pitch = VREG(v->regs,pitchl); +} +inline VOICE_CLOCK( V3a ) +{ + m.t_pitch += (VREG(v->regs,pitchh) & 0x3F) << 8; +} +inline VOICE_CLOCK( V3b ) +{ + // Read BRR header and byte + m.t_brr_byte = m.ram [(v->brr_addr + v->brr_offset) & 0xFFFF]; + m.t_brr_header = m.ram [v->brr_addr]; // brr_addr doesn't need masking +} +VOICE_CLOCK( V3c ) +{ + // Pitch modulation using previous voice's output + if ( m.t_pmon & v->vbit ) + m.t_pitch += ((m.t_output >> 5) * m.t_pitch) >> 10; + + if ( v->kon_delay ) + { + // Get ready to start BRR decoding on next sample + if ( v->kon_delay == 5 ) + { + v->brr_addr = m.t_brr_next_addr; + v->brr_offset = 1; + v->buf_pos = 0; + m.t_brr_header = 0; // header is ignored on this sample + m.kon_check = true; + } + + // Envelope is never run during KON + v->env = 0; + v->hidden_env = 0; + + // Disable BRR decoding until last three samples + v->interp_pos = 0; + if ( --v->kon_delay & 3 ) + v->interp_pos = 0x4000; + + // Pitch is never added during KON + m.t_pitch = 0; + } + + // Gaussian interpolation + { + int output = interpolate( v ); + + // Noise + if ( m.t_non & v->vbit ) + output = (int16_t) (m.noise * 2); + + // Apply envelope + m.t_output = (output * v->env) >> 11 & ~1; + v->t_envx_out = (uint8_t) (v->env >> 4); + } + + // Immediate silence due to end of sample or soft reset + if ( REG(flg) & 0x80 || (m.t_brr_header & 3) == 1 ) + { + v->env_mode = env_release; + v->env = 0; + } + + if ( m.every_other_sample ) + { + // KOFF + if ( m.t_koff & v->vbit ) + v->env_mode = env_release; + + // KON + if ( m.kon & v->vbit ) + { + v->kon_delay = 5; + v->env_mode = env_attack; + } + } + + // Run envelope for next sample + if ( !v->kon_delay ) + run_envelope( v ); +} +inline void SPC_DSP::voice_output( voice_t const* v, int ch ) +{ + // Apply left/right volume + int amp = (m.t_output * (int8_t) VREG(v->regs,voll + ch)) >> 7; + + // Add to output total + m.t_main_out [ch] += amp; + CLAMP16( m.t_main_out [ch] ); + + // Optionally add to echo total + if ( m.t_eon & v->vbit ) + { + m.t_echo_out [ch] += amp; + CLAMP16( m.t_echo_out [ch] ); + } +} +VOICE_CLOCK( V4 ) +{ + // Decode BRR + m.t_looped = 0; + if ( v->interp_pos >= 0x4000 ) + { + decode_brr( v ); + + if ( (v->brr_offset += 2) >= brr_block_size ) + { + // Start decoding next BRR block + assert( v->brr_offset == brr_block_size ); + v->brr_addr = (v->brr_addr + brr_block_size) & 0xFFFF; + if ( m.t_brr_header & 1 ) + { + v->brr_addr = m.t_brr_next_addr; + m.t_looped = v->vbit; + } + v->brr_offset = 1; + } + } + + // Apply pitch + v->interp_pos = (v->interp_pos & 0x3FFF) + m.t_pitch; + + // Keep from getting too far ahead (when using pitch modulation) + if ( v->interp_pos > 0x7FFF ) + v->interp_pos = 0x7FFF; + + // Output left + voice_output( v, 0 ); +} +inline VOICE_CLOCK( V5 ) +{ + // Output right + voice_output( v, 1 ); + + // ENDX, OUTX, and ENVX won't update if you wrote to them 1-2 clocks earlier + int endx_buf = REG(endx) | m.t_looped; + + // Clear bit in ENDX if KON just began + if ( v->kon_delay == 5 ) + endx_buf &= ~v->vbit; + m.endx_buf = (uint8_t) endx_buf; +} +inline VOICE_CLOCK( V6 ) +{ + (void) v; // avoid compiler warning about unused v + m.outx_buf = (uint8_t) (m.t_output >> 8); +} +inline VOICE_CLOCK( V7 ) +{ + // Update ENDX + REG(endx) = m.endx_buf; + + m.envx_buf = v->t_envx_out; +} +inline VOICE_CLOCK( V8 ) +{ + // Update OUTX + VREG(v->regs,outx) = m.outx_buf; +} +inline VOICE_CLOCK( V9 ) +{ + // Update ENVX + VREG(v->regs,envx) = m.envx_buf; +} + +// Most voices do all these in one clock, so make a handy composite +inline VOICE_CLOCK( V3 ) +{ + voice_V3a( v ); + voice_V3b( v ); + voice_V3c( v ); +} + +// Common combinations of voice steps on different voices. This greatly reduces +// code size and allows everything to be inlined in these functions. +VOICE_CLOCK(V7_V4_V1) { voice_V7(v); voice_V1(v+3); voice_V4(v+1); } +VOICE_CLOCK(V8_V5_V2) { voice_V8(v); voice_V5(v+1); voice_V2(v+2); } +VOICE_CLOCK(V9_V6_V3) { voice_V9(v); voice_V6(v+1); voice_V3(v+2); } + + +//// Echo + +// Current echo buffer pointer for left/right channel +#define ECHO_PTR( ch ) (&m.ram [m.t_echo_ptr + ch * 2]) + +// Sample in echo history buffer, where 0 is the oldest +#define ECHO_FIR( i ) (m.echo_hist_pos [i]) + +// Calculate FIR point for left/right channel +#define CALC_FIR( i, ch ) ((ECHO_FIR( i + 1 ) [ch] * (int8_t) REG(fir + i * 0x10)) >> 6) + +#define ECHO_CLOCK( n ) inline void SPC_DSP::echo_##n() + +inline void SPC_DSP::echo_read( int ch ) +{ + int s = GET_LE16SA( ECHO_PTR( ch ) ); + // second copy simplifies wrap-around handling + ECHO_FIR( 0 ) [ch] = ECHO_FIR( 8 ) [ch] = s >> 1; +} + +ECHO_CLOCK( 22 ) +{ + // History + if ( ++m.echo_hist_pos >= &m.echo_hist [echo_hist_size] ) + m.echo_hist_pos = m.echo_hist; + + m.t_echo_ptr = (m.t_esa * 0x100 + m.echo_offset) & 0xFFFF; + echo_read( 0 ); + + // FIR (using l and r temporaries below helps compiler optimize) + int l = CALC_FIR( 0, 0 ); + int r = CALC_FIR( 0, 1 ); + + m.t_echo_in [0] = l; + m.t_echo_in [1] = r; +} +ECHO_CLOCK( 23 ) +{ + int l = CALC_FIR( 1, 0 ) + CALC_FIR( 2, 0 ); + int r = CALC_FIR( 1, 1 ) + CALC_FIR( 2, 1 ); + + m.t_echo_in [0] += l; + m.t_echo_in [1] += r; + + echo_read( 1 ); +} +ECHO_CLOCK( 24 ) +{ + int l = CALC_FIR( 3, 0 ) + CALC_FIR( 4, 0 ) + CALC_FIR( 5, 0 ); + int r = CALC_FIR( 3, 1 ) + CALC_FIR( 4, 1 ) + CALC_FIR( 5, 1 ); + + m.t_echo_in [0] += l; + m.t_echo_in [1] += r; +} +ECHO_CLOCK( 25 ) +{ + int l = m.t_echo_in [0] + CALC_FIR( 6, 0 ); + int r = m.t_echo_in [1] + CALC_FIR( 6, 1 ); + + l = (int16_t) l; + r = (int16_t) r; + + l += (int16_t) CALC_FIR( 7, 0 ); + r += (int16_t) CALC_FIR( 7, 1 ); + + CLAMP16( l ); + CLAMP16( r ); + + m.t_echo_in [0] = l & ~1; + m.t_echo_in [1] = r & ~1; +} +inline int SPC_DSP::echo_output( int ch ) +{ + int out = (int16_t) ((m.t_main_out [ch] * (int8_t) REG(mvoll + ch * 0x10)) >> 7) + + (int16_t) ((m.t_echo_in [ch] * (int8_t) REG(evoll + ch * 0x10)) >> 7); + CLAMP16( out ); + return out; +} +ECHO_CLOCK( 26 ) +{ + // Left output volumes + // (save sample for next clock so we can output both together) + m.t_main_out [0] = echo_output( 0 ); + + // Echo feedback + int l = m.t_echo_out [0] + (int16_t) ((m.t_echo_in [0] * (int8_t) REG(efb)) >> 7); + int r = m.t_echo_out [1] + (int16_t) ((m.t_echo_in [1] * (int8_t) REG(efb)) >> 7); + + CLAMP16( l ); + CLAMP16( r ); + + m.t_echo_out [0] = l & ~1; + m.t_echo_out [1] = r & ~1; +} +ECHO_CLOCK( 27 ) +{ + // Output + int l = m.t_main_out [0]; + int r = echo_output( 1 ); + m.t_main_out [0] = 0; + m.t_main_out [1] = 0; + + // TODO: global muting isn't this simple (turns DAC on and off + // or something, causing small ~37-sample pulse when first muted) + if ( REG(flg) & 0x40 ) + { + l = 0; + r = 0; + } + + // Output sample to DAC + #ifdef SPC_DSP_OUT_HOOK + SPC_DSP_OUT_HOOK( l, r ); + #else + sample_t* out = m.out; + WRITE_SAMPLES( l, r, out ); + m.out = out; + #endif +} +ECHO_CLOCK( 28 ) +{ + m.t_echo_enabled = REG(flg); +} +inline void SPC_DSP::echo_write( int ch ) +{ + if ( !(m.t_echo_enabled & 0x20) ) + SET_LE16A( ECHO_PTR( ch ), m.t_echo_out [ch] ); + m.t_echo_out [ch] = 0; +} +ECHO_CLOCK( 29 ) +{ + m.t_esa = REG(esa); + + if ( !m.echo_offset ) + m.echo_length = (REG(edl) & 0x0F) * 0x800; + + m.echo_offset += 4; + if ( m.echo_offset >= m.echo_length ) + m.echo_offset = 0; + + // Write left echo + echo_write( 0 ); + + m.t_echo_enabled = REG(flg); +} +ECHO_CLOCK( 30 ) +{ + // Write right echo + echo_write( 1 ); +} + + +//// Timing + +// Execute clock for a particular voice +#define V( clock, voice ) voice_##clock( &m.voices [voice] ); + +/* The most common sequence of clocks uses composite operations +for efficiency. For example, the following are equivalent to the +individual steps on the right: + +V(V7_V4_V1,2) -> V(V7,2) V(V4,3) V(V1,5) +V(V8_V5_V2,2) -> V(V8,2) V(V5,3) V(V2,4) +V(V9_V6_V3,2) -> V(V9,2) V(V6,3) V(V3,4) */ + +// Voice 0 1 2 3 4 5 6 7 +#define GEN_DSP_TIMING \ +PHASE( 0) V(V5,0)V(V2,1)\ +PHASE( 1) V(V6,0)V(V3,1)\ +PHASE( 2) V(V7_V4_V1,0)\ +PHASE( 3) V(V8_V5_V2,0)\ +PHASE( 4) V(V9_V6_V3,0)\ +PHASE( 5) V(V7_V4_V1,1)\ +PHASE( 6) V(V8_V5_V2,1)\ +PHASE( 7) V(V9_V6_V3,1)\ +PHASE( 8) V(V7_V4_V1,2)\ +PHASE( 9) V(V8_V5_V2,2)\ +PHASE(10) V(V9_V6_V3,2)\ +PHASE(11) V(V7_V4_V1,3)\ +PHASE(12) V(V8_V5_V2,3)\ +PHASE(13) V(V9_V6_V3,3)\ +PHASE(14) V(V7_V4_V1,4)\ +PHASE(15) V(V8_V5_V2,4)\ +PHASE(16) V(V9_V6_V3,4)\ +PHASE(17) V(V1,0) V(V7,5)V(V4,6)\ +PHASE(18) V(V8_V5_V2,5)\ +PHASE(19) V(V9_V6_V3,5)\ +PHASE(20) V(V1,1) V(V7,6)V(V4,7)\ +PHASE(21) V(V8,6)V(V5,7) V(V2,0) /* t_brr_next_addr order dependency */\ +PHASE(22) V(V3a,0) V(V9,6)V(V6,7) echo_22();\ +PHASE(23) V(V7,7) echo_23();\ +PHASE(24) V(V8,7) echo_24();\ +PHASE(25) V(V3b,0) V(V9,7) echo_25();\ +PHASE(26) echo_26();\ +PHASE(27) misc_27(); echo_27();\ +PHASE(28) misc_28(); echo_28();\ +PHASE(29) misc_29(); echo_29();\ +PHASE(30) misc_30();V(V3c,0) echo_30();\ +PHASE(31) V(V4,0) V(V1,2)\ + +#if !SPC_DSP_CUSTOM_RUN + +void SPC_DSP::run( int clocks_remain ) +{ + require( clocks_remain > 0 ); + + int const phase = m.phase; + m.phase = (phase + clocks_remain) & 31; + switch ( phase ) + { + loop: + + #define PHASE( n ) if ( n && !--clocks_remain ) break; case n: + GEN_DSP_TIMING + #undef PHASE + + if ( --clocks_remain ) + goto loop; + } +} + +#endif + + +//// Setup + +void SPC_DSP::init( void* ram_64k ) +{ + m.ram = (uint8_t*) ram_64k; + mute_voices( 0 ); + disable_surround( false ); + set_output( 0, 0 ); + reset(); + + #ifndef NDEBUG + // be sure this sign-extends + assert( (int16_t) 0x8000 == -0x8000 ); + + // be sure right shift preserves sign + assert( (-1 >> 1) == -1 ); + + // check clamp macro + int i; + i = +0x8000; CLAMP16( i ); assert( i == +0x7FFF ); + i = -0x8001; CLAMP16( i ); assert( i == -0x8000 ); + + blargg_verify_byte_order(); + #endif +} + +void SPC_DSP::soft_reset_common() +{ + require( m.ram ); // init() must have been called already + + m.noise = 0x4000; + m.echo_hist_pos = m.echo_hist; + m.every_other_sample = 1; + m.echo_offset = 0; + m.phase = 0; + + init_counter(); +} + +void SPC_DSP::soft_reset() +{ + REG(flg) = 0xE0; + soft_reset_common(); +} + +void SPC_DSP::load( uint8_t const regs [register_count] ) +{ + memcpy( m.regs, regs, sizeof m.regs ); + memset( &m.regs [register_count], 0, offsetof (state_t,ram) - register_count ); + + // Internal state + for ( int i = voice_count; --i >= 0; ) + { + voice_t* v = &m.voices [i]; + v->brr_offset = 1; + v->vbit = 1 << i; + v->regs = &m.regs [i * 0x10]; + } + m.new_kon = REG(kon); + m.t_dir = REG(dir); + m.t_esa = REG(esa); + + soft_reset_common(); +} + +void SPC_DSP::reset() { load( initial_regs ); } + + +//// State save/load + +#if !SPC_NO_COPY_STATE_FUNCS + +void SPC_State_Copier::copy( void* state, size_t size ) +{ + func( buf, state, size ); +} + +int SPC_State_Copier::copy_int( int state, int size ) +{ + BOOST::uint8_t s [2]; + SET_LE16( s, state ); + func( buf, &s, size ); + return GET_LE16( s ); +} + +void SPC_State_Copier::skip( int count ) +{ + if ( count > 0 ) + { + char temp [64]; + memset( temp, 0, sizeof temp ); + do + { + int n = sizeof temp; + if ( n > count ) + n = count; + count -= n; + func( buf, temp, n ); + } + while ( count ); + } +} + +void SPC_State_Copier::extra() +{ + int n = 0; + SPC_State_Copier& copier = *this; + SPC_COPY( uint8_t, n ); + skip( n ); +} + +void SPC_DSP::copy_state( unsigned char** io, copy_func_t copy ) +{ + SPC_State_Copier copier( io, copy ); + + // DSP registers + copier.copy( m.regs, register_count ); + + // Internal state + + // Voices + int i; + for ( i = 0; i < voice_count; i++ ) + { + voice_t* v = &m.voices [i]; + + // BRR buffer + int i; + for ( i = 0; i < brr_buf_size; i++ ) + { + int s = v->buf [i]; + SPC_COPY( int16_t, s ); + v->buf [i] = v->buf [i + brr_buf_size] = s; + } + + SPC_COPY( uint16_t, v->interp_pos ); + SPC_COPY( uint16_t, v->brr_addr ); + SPC_COPY( uint16_t, v->env ); + SPC_COPY( int16_t, v->hidden_env ); + SPC_COPY( uint8_t, v->buf_pos ); + SPC_COPY( uint8_t, v->brr_offset ); + SPC_COPY( uint8_t, v->kon_delay ); + { + int m = v->env_mode; + SPC_COPY( uint8_t, m ); + v->env_mode = (enum env_mode_t) m; + } + SPC_COPY( uint8_t, v->t_envx_out ); + + copier.extra(); + } + + // Echo history + for ( i = 0; i < echo_hist_size; i++ ) + { + int j; + for ( j = 0; j < 2; j++ ) + { + int s = m.echo_hist_pos [i] [j]; + SPC_COPY( int16_t, s ); + m.echo_hist [i] [j] = s; // write back at offset 0 + } + } + m.echo_hist_pos = m.echo_hist; + memcpy( &m.echo_hist [echo_hist_size], m.echo_hist, echo_hist_size * sizeof m.echo_hist [0] ); + + // Misc + SPC_COPY( uint8_t, m.every_other_sample ); + SPC_COPY( uint8_t, m.kon ); + + SPC_COPY( uint16_t, m.noise ); + SPC_COPY( uint16_t, m.counter ); + SPC_COPY( uint16_t, m.echo_offset ); + SPC_COPY( uint16_t, m.echo_length ); + SPC_COPY( uint8_t, m.phase ); + + SPC_COPY( uint8_t, m.new_kon ); + SPC_COPY( uint8_t, m.endx_buf ); + SPC_COPY( uint8_t, m.envx_buf ); + SPC_COPY( uint8_t, m.outx_buf ); + + SPC_COPY( uint8_t, m.t_pmon ); + SPC_COPY( uint8_t, m.t_non ); + SPC_COPY( uint8_t, m.t_eon ); + SPC_COPY( uint8_t, m.t_dir ); + SPC_COPY( uint8_t, m.t_koff ); + + SPC_COPY( uint16_t, m.t_brr_next_addr ); + SPC_COPY( uint8_t, m.t_adsr0 ); + SPC_COPY( uint8_t, m.t_brr_header ); + SPC_COPY( uint8_t, m.t_brr_byte ); + SPC_COPY( uint8_t, m.t_srcn ); + SPC_COPY( uint8_t, m.t_esa ); + SPC_COPY( uint8_t, m.t_echo_enabled ); + + SPC_COPY( int16_t, m.t_main_out [0] ); + SPC_COPY( int16_t, m.t_main_out [1] ); + SPC_COPY( int16_t, m.t_echo_out [0] ); + SPC_COPY( int16_t, m.t_echo_out [1] ); + SPC_COPY( int16_t, m.t_echo_in [0] ); + SPC_COPY( int16_t, m.t_echo_in [1] ); + + SPC_COPY( uint16_t, m.t_dir_addr ); + SPC_COPY( uint16_t, m.t_pitch ); + SPC_COPY( int16_t, m.t_output ); + SPC_COPY( uint16_t, m.t_echo_ptr ); + SPC_COPY( uint8_t, m.t_looped ); + + copier.extra(); +} +#endif diff --git a/src/engine/platform/sound/snes/SPC_DSP.h b/src/engine/platform/sound/snes/SPC_DSP.h new file mode 100644 index 000000000..d59284797 --- /dev/null +++ b/src/engine/platform/sound/snes/SPC_DSP.h @@ -0,0 +1,307 @@ +// Highly accurate SNES SPC-700 DSP emulator + +// snes_spc 0.9.0 +#ifndef SPC_DSP_H +#define SPC_DSP_H + +#include "blargg_common.h" + +extern "C" { typedef void (*dsp_copy_func_t)( unsigned char** io, void* state, size_t ); } + +class SPC_DSP { +public: + typedef BOOST::uint8_t uint8_t; + +// Setup + + // Initializes DSP and has it use the 64K RAM provided + void init( void* ram_64k ); + + // Sets destination for output samples. If out is NULL or out_size is 0, + // doesn't generate any. + typedef short sample_t; + void set_output( sample_t* out, int out_size ); + + // Number of samples written to output since it was last set, always + // a multiple of 2. Undefined if more samples were generated than + // output buffer could hold. + int sample_count() const; + +// Emulation + + // Resets DSP to power-on state + void reset(); + + // Emulates pressing reset switch on SNES + void soft_reset(); + + // Reads/writes DSP registers. For accuracy, you must first call run() + // to catch the DSP up to present. + int read ( int addr ) const; + void write( int addr, int data ); + + // Runs DSP for specified number of clocks (~1024000 per second). Every 32 clocks + // a pair of samples is be generated. + void run( int clock_count ); + +// Sound control + + // Mutes voices corresponding to non-zero bits in mask (issues repeated KOFF events). + // Reduces emulation accuracy. + enum { voice_count = 8 }; + void mute_voices( int mask ); + +// State + + // Resets DSP and uses supplied values to initialize registers + enum { register_count = 128 }; + void load( uint8_t const regs [register_count] ); + + // Saves/loads exact emulator state + enum { state_size = 640 }; // maximum space needed when saving + typedef dsp_copy_func_t copy_func_t; + void copy_state( unsigned char** io, copy_func_t ); + + // Returns non-zero if new key-on events occurred since last call + bool check_kon(); + +// DSP register addresses + + // Global registers + enum { + r_mvoll = 0x0C, r_mvolr = 0x1C, + r_evoll = 0x2C, r_evolr = 0x3C, + r_kon = 0x4C, r_koff = 0x5C, + r_flg = 0x6C, r_endx = 0x7C, + r_efb = 0x0D, r_pmon = 0x2D, + r_non = 0x3D, r_eon = 0x4D, + r_dir = 0x5D, r_esa = 0x6D, + r_edl = 0x7D, + r_fir = 0x0F // 8 coefficients at 0x0F, 0x1F ... 0x7F + }; + + // Voice registers + enum { + v_voll = 0x00, v_volr = 0x01, + v_pitchl = 0x02, v_pitchh = 0x03, + v_srcn = 0x04, v_adsr0 = 0x05, + v_adsr1 = 0x06, v_gain = 0x07, + v_envx = 0x08, v_outx = 0x09 + }; + +public: + enum { extra_size = 16 }; + sample_t* extra() { return m.extra; } + sample_t const* out_pos() const { return m.out; } + void disable_surround( bool ) { } // not supported +public: + BLARGG_DISABLE_NOTHROW + + typedef BOOST::int8_t int8_t; + typedef BOOST::int16_t int16_t; + + enum { echo_hist_size = 8 }; + + enum env_mode_t { env_release, env_attack, env_decay, env_sustain }; + enum { brr_buf_size = 12 }; + struct voice_t + { + int buf [brr_buf_size*2];// decoded samples (twice the size to simplify wrap handling) + int buf_pos; // place in buffer where next samples will be decoded + int interp_pos; // relative fractional position in sample (0x1000 = 1.0) + int brr_addr; // address of current BRR block + int brr_offset; // current decoding offset in BRR block + uint8_t* regs; // pointer to voice's DSP registers + int vbit; // bitmask for voice: 0x01 for voice 0, 0x02 for voice 1, etc. + int kon_delay; // KON delay/current setup phase + env_mode_t env_mode; + int env; // current envelope level + int hidden_env; // used by GAIN mode 7, very obscure quirk + uint8_t t_envx_out; + }; +private: + enum { brr_block_size = 9 }; + + struct state_t + { + uint8_t regs [register_count]; + + // Echo history keeps most recent 8 samples (twice the size to simplify wrap handling) + int echo_hist [echo_hist_size * 2] [2]; + int (*echo_hist_pos) [2]; // &echo_hist [0 to 7] + + int every_other_sample; // toggles every sample + int kon; // KON value when last checked + int noise; + int counter; + int echo_offset; // offset from ESA in echo buffer + int echo_length; // number of bytes that echo_offset will stop at + int phase; // next clock cycle to run (0-31) + bool kon_check; // set when a new KON occurs + + // Hidden registers also written to when main register is written to + int new_kon; + uint8_t endx_buf; + uint8_t envx_buf; + uint8_t outx_buf; + + // Temporary state between clocks + + // read once per sample + int t_pmon; + int t_non; + int t_eon; + int t_dir; + int t_koff; + + // read a few clocks ahead then used + int t_brr_next_addr; + int t_adsr0; + int t_brr_header; + int t_brr_byte; + int t_srcn; + int t_esa; + int t_echo_enabled; + + // internal state that is recalculated every sample + int t_dir_addr; + int t_pitch; + int t_output; + int t_looped; + int t_echo_ptr; + + // left/right sums + int t_main_out [2]; + int t_echo_out [2]; + int t_echo_in [2]; + + voice_t voices [voice_count]; + + // non-emulation state + uint8_t* ram; // 64K shared RAM between DSP and SMP + int mute_mask; + sample_t* out; + sample_t* out_end; + sample_t* out_begin; + sample_t extra [extra_size]; + }; + state_t m; + + void init_counter(); + void run_counters(); + unsigned read_counter( int rate ); + + int interpolate( voice_t const* v ); + void run_envelope( voice_t* const v ); + void decode_brr( voice_t* v ); + + void misc_27(); + void misc_28(); + void misc_29(); + void misc_30(); + + void voice_output( voice_t const* v, int ch ); + void voice_V1( voice_t* const ); + void voice_V2( voice_t* const ); + void voice_V3( voice_t* const ); + void voice_V3a( voice_t* const ); + void voice_V3b( voice_t* const ); + void voice_V3c( voice_t* const ); + void voice_V4( voice_t* const ); + void voice_V5( voice_t* const ); + void voice_V6( voice_t* const ); + void voice_V7( voice_t* const ); + void voice_V8( voice_t* const ); + void voice_V9( voice_t* const ); + void voice_V7_V4_V1( voice_t* const ); + void voice_V8_V5_V2( voice_t* const ); + void voice_V9_V6_V3( voice_t* const ); + + void echo_read( int ch ); + int echo_output( int ch ); + void echo_write( int ch ); + void echo_22(); + void echo_23(); + void echo_24(); + void echo_25(); + void echo_26(); + void echo_27(); + void echo_28(); + void echo_29(); + void echo_30(); + + void soft_reset_common(); + +public: + bool mute() { return m.regs[r_flg] & 0x40; } +}; + +#include + +inline int SPC_DSP::sample_count() const { return m.out - m.out_begin; } + +inline int SPC_DSP::read( int addr ) const +{ + assert( (unsigned) addr < register_count ); + return m.regs [addr]; +} + +inline void SPC_DSP::write( int addr, int data ) +{ + assert( (unsigned) addr < register_count ); + + m.regs [addr] = (uint8_t) data; + switch ( addr & 0x0F ) + { + case v_envx: + m.envx_buf = (uint8_t) data; + break; + + case v_outx: + m.outx_buf = (uint8_t) data; + break; + + case 0x0C: + if ( addr == r_kon ) + m.new_kon = (uint8_t) data; + + if ( addr == r_endx ) // always cleared, regardless of data written + { + m.endx_buf = 0; + m.regs [r_endx] = 0; + } + break; + } +} + +inline void SPC_DSP::mute_voices( int mask ) { m.mute_mask = mask; } + +inline bool SPC_DSP::check_kon() +{ + bool old = m.kon_check; + m.kon_check = 0; + return old; +} + +#if !SPC_NO_COPY_STATE_FUNCS + +class SPC_State_Copier { + SPC_DSP::copy_func_t func; + unsigned char** buf; +public: + SPC_State_Copier( unsigned char** p, SPC_DSP::copy_func_t f ) { func = f; buf = p; } + void copy( void* state, size_t size ); + int copy_int( int state, int size ); + void skip( int count ); + void extra(); +}; + +#define SPC_COPY( type, state )\ +{\ + state = (BOOST::type) copier.copy_int( state, sizeof (BOOST::type) );\ + assert( (BOOST::type) state == state );\ +} + +#endif + +#endif diff --git a/src/engine/platform/sound/snes/blargg_common.h b/src/engine/platform/sound/snes/blargg_common.h new file mode 100644 index 000000000..75edff391 --- /dev/null +++ b/src/engine/platform/sound/snes/blargg_common.h @@ -0,0 +1,186 @@ +// Sets up common environment for Shay Green's libraries. +// To change configuration options, modify blargg_config.h, not this file. + +// snes_spc 0.9.0 +#ifndef BLARGG_COMMON_H +#define BLARGG_COMMON_H + +#include +#include +#include +#include + +#undef BLARGG_COMMON_H +// allow blargg_config.h to #include blargg_common.h +#include "blargg_config.h" +#ifndef BLARGG_COMMON_H +#define BLARGG_COMMON_H + +// BLARGG_RESTRICT: equivalent to restrict, where supported +#if defined (__GNUC__) || _MSC_VER >= 1100 + #define BLARGG_RESTRICT __restrict +#else + #define BLARGG_RESTRICT +#endif + +// STATIC_CAST(T,expr): Used in place of static_cast (expr) +#ifndef STATIC_CAST + #define STATIC_CAST(T,expr) ((T) (expr)) +#endif + +// blargg_err_t (0 on success, otherwise error string) +#ifndef blargg_err_t + typedef const char* blargg_err_t; +#endif + +// blargg_vector - very lightweight vector of POD types (no constructor/destructor) +template +class blargg_vector { + T* begin_; + size_t size_; +public: + blargg_vector() : begin_( 0 ), size_( 0 ) { } + ~blargg_vector() { free( begin_ ); } + size_t size() const { return size_; } + T* begin() const { return begin_; } + T* end() const { return begin_ + size_; } + blargg_err_t resize( size_t n ) + { + // TODO: blargg_common.cpp to hold this as an outline function, ugh + void* p = realloc( begin_, n * sizeof (T) ); + if ( p ) + begin_ = (T*) p; + else if ( n > size_ ) // realloc failure only a problem if expanding + return "Out of memory"; + size_ = n; + return 0; + } + void clear() { void* p = begin_; begin_ = 0; size_ = 0; free( p ); } + T& operator [] ( size_t n ) const + { + assert( n <= size_ ); // <= to allow past-the-end value + return begin_ [n]; + } +}; + +#ifndef BLARGG_DISABLE_NOTHROW + // throw spec mandatory in ISO C++ if operator new can return NULL + #if __cplusplus >= 199711 || defined (__GNUC__) + #define BLARGG_THROWS( spec ) throw spec + #else + #define BLARGG_THROWS( spec ) + #endif + #define BLARGG_DISABLE_NOTHROW \ + void* operator new ( size_t s ) BLARGG_THROWS(()) { return malloc( s ); }\ + void operator delete ( void* p ) { free( p ); } + #define BLARGG_NEW new +#else + #include + #define BLARGG_NEW new (std::nothrow) +#endif + +// BLARGG_4CHAR('a','b','c','d') = 'abcd' (four character integer constant) +#define BLARGG_4CHAR( a, b, c, d ) \ + ((a&0xFF)*0x1000000L + (b&0xFF)*0x10000L + (c&0xFF)*0x100L + (d&0xFF)) + +// BOOST_STATIC_ASSERT( expr ): Generates compile error if expr is 0. +#ifndef BOOST_STATIC_ASSERT + #ifdef _MSC_VER + // MSVC6 (_MSC_VER < 1300) fails for use of __LINE__ when /Zl is specified + #define BOOST_STATIC_ASSERT( expr ) \ + void blargg_failed_( int (*arg) [2 / (int) !!(expr) - 1] ) + #else + // Some other compilers fail when declaring same function multiple times in class, + // so differentiate them by line + #define BOOST_STATIC_ASSERT( expr ) \ + void blargg_failed_( int (*arg) [2 / !!(expr) - 1] [__LINE__] ) + #endif +#endif + +// BLARGG_COMPILER_HAS_BOOL: If 0, provides bool support for old compiler. If 1, +// compiler is assumed to support bool. If undefined, availability is determined. +#ifndef BLARGG_COMPILER_HAS_BOOL + #if defined (__MWERKS__) + #if !__option(bool) + #define BLARGG_COMPILER_HAS_BOOL 0 + #endif + #elif defined (_MSC_VER) + #if _MSC_VER < 1100 + #define BLARGG_COMPILER_HAS_BOOL 0 + #endif + #elif defined (__GNUC__) + // supports bool + #elif __cplusplus < 199711 + #define BLARGG_COMPILER_HAS_BOOL 0 + #endif +#endif +#if defined (BLARGG_COMPILER_HAS_BOOL) && !BLARGG_COMPILER_HAS_BOOL + // If you get errors here, modify your blargg_config.h file + typedef int bool; + const bool true = 1; + const bool false = 0; +#endif + +// blargg_long/blargg_ulong = at least 32 bits, int if it's big enough + +#if INT_MAX < 0x7FFFFFFF || LONG_MAX == 0x7FFFFFFF + typedef long blargg_long; +#else + typedef int blargg_long; +#endif + +#if UINT_MAX < 0xFFFFFFFF || ULONG_MAX == 0xFFFFFFFF + typedef unsigned long blargg_ulong; +#else + typedef unsigned blargg_ulong; +#endif + +// BOOST::int8_t etc. + +// HAVE_STDINT_H: If defined, use for int8_t etc. +#if defined (HAVE_STDINT_H) + #include + #define BOOST + +// HAVE_INTTYPES_H: If defined, use for int8_t etc. +#elif defined (HAVE_INTTYPES_H) + #include + #define BOOST + +#else + struct BOOST + { + #if UCHAR_MAX == 0xFF && SCHAR_MAX == 0x7F + typedef signed char int8_t; + typedef unsigned char uint8_t; + #else + // No suitable 8-bit type available + typedef struct see_blargg_common_h int8_t; + typedef struct see_blargg_common_h uint8_t; + #endif + + #if USHRT_MAX == 0xFFFF + typedef short int16_t; + typedef unsigned short uint16_t; + #else + // No suitable 16-bit type available + typedef struct see_blargg_common_h int16_t; + typedef struct see_blargg_common_h uint16_t; + #endif + + #if ULONG_MAX == 0xFFFFFFFF + typedef long int32_t; + typedef unsigned long uint32_t; + #elif UINT_MAX == 0xFFFFFFFF + typedef int int32_t; + typedef unsigned int uint32_t; + #else + // No suitable 32-bit type available + typedef struct see_blargg_common_h int32_t; + typedef struct see_blargg_common_h uint32_t; + #endif + }; +#endif + +#endif +#endif diff --git a/src/engine/platform/sound/snes/blargg_config.h b/src/engine/platform/sound/snes/blargg_config.h new file mode 100644 index 000000000..94570ca0b --- /dev/null +++ b/src/engine/platform/sound/snes/blargg_config.h @@ -0,0 +1,26 @@ +// snes_spc 0.9.0 user configuration file. Don't replace when updating library. + +// snes_spc 0.9.0 +#ifndef BLARGG_CONFIG_H +#define BLARGG_CONFIG_H + +// Uncomment to disable debugging checks +#ifndef NDEBUG + #define NDEBUG 1 +#endif + +// Uncomment to enable platform-specific (and possibly non-portable) optimizations +//#define BLARGG_NONPORTABLE 1 + +// Uncomment if automatic byte-order determination doesn't work +//#define BLARGG_BIG_ENDIAN 1 + +// Uncomment if you get errors in the bool section of blargg_common.h +//#define BLARGG_COMPILER_HAS_BOOL 1 + +// Use standard config.h if present +#ifdef HAVE_CONFIG_H + #include "config.h" +#endif + +#endif diff --git a/src/engine/platform/sound/snes/blargg_endian.h b/src/engine/platform/sound/snes/blargg_endian.h new file mode 100644 index 000000000..f2daca641 --- /dev/null +++ b/src/engine/platform/sound/snes/blargg_endian.h @@ -0,0 +1,185 @@ +// CPU Byte Order Utilities + +// snes_spc 0.9.0 +#ifndef BLARGG_ENDIAN +#define BLARGG_ENDIAN + +#include "blargg_common.h" + +// BLARGG_CPU_CISC: Defined if CPU has very few general-purpose registers (< 16) +#if defined (_M_IX86) || defined (_M_IA64) || defined (__i486__) || \ + defined (__x86_64__) || defined (__ia64__) || defined (__i386__) + #define BLARGG_CPU_X86 1 + #define BLARGG_CPU_CISC 1 +#endif + +#if defined (__powerpc__) || defined (__ppc__) || defined (__POWERPC__) || defined (__powerc) + #define BLARGG_CPU_POWERPC 1 + #define BLARGG_CPU_RISC 1 +#endif + +// BLARGG_BIG_ENDIAN, BLARGG_LITTLE_ENDIAN: Determined automatically, otherwise only +// one may be #defined to 1. Only needed if something actually depends on byte order. +#if !defined (BLARGG_BIG_ENDIAN) && !defined (BLARGG_LITTLE_ENDIAN) +#ifdef __GLIBC__ + // GCC handles this for us + #include + #if __BYTE_ORDER == __LITTLE_ENDIAN + #define BLARGG_LITTLE_ENDIAN 1 + #elif __BYTE_ORDER == __BIG_ENDIAN + #define BLARGG_BIG_ENDIAN 1 + #endif +#else + +#if defined (LSB_FIRST) || defined (__LITTLE_ENDIAN__) || BLARGG_CPU_X86 || \ + (defined (LITTLE_ENDIAN) && LITTLE_ENDIAN+0 != 1234) + #define BLARGG_LITTLE_ENDIAN 1 +#endif + +#if defined (MSB_FIRST) || defined (__BIG_ENDIAN__) || defined (WORDS_BIGENDIAN) || \ + defined (__sparc__) || BLARGG_CPU_POWERPC || \ + (defined (BIG_ENDIAN) && BIG_ENDIAN+0 != 4321) + #define BLARGG_BIG_ENDIAN 1 +#elif !defined (__mips__) + // No endian specified; assume little-endian, since it's most common + #define BLARGG_LITTLE_ENDIAN 1 +#endif +#endif +#endif + +#if BLARGG_LITTLE_ENDIAN && BLARGG_BIG_ENDIAN + #undef BLARGG_LITTLE_ENDIAN + #undef BLARGG_BIG_ENDIAN +#endif + +inline void blargg_verify_byte_order() +{ + #ifndef NDEBUG + #if BLARGG_BIG_ENDIAN + volatile int i = 1; + assert( *(volatile char*) &i == 0 ); + #elif BLARGG_LITTLE_ENDIAN + volatile int i = 1; + assert( *(volatile char*) &i != 0 ); + #endif + #endif +} + +inline unsigned get_le16( void const* p ) +{ + return (unsigned) ((unsigned char const*) p) [1] << 8 | + (unsigned) ((unsigned char const*) p) [0]; +} + +inline unsigned get_be16( void const* p ) +{ + return (unsigned) ((unsigned char const*) p) [0] << 8 | + (unsigned) ((unsigned char const*) p) [1]; +} + +inline blargg_ulong get_le32( void const* p ) +{ + return (blargg_ulong) ((unsigned char const*) p) [3] << 24 | + (blargg_ulong) ((unsigned char const*) p) [2] << 16 | + (blargg_ulong) ((unsigned char const*) p) [1] << 8 | + (blargg_ulong) ((unsigned char const*) p) [0]; +} + +inline blargg_ulong get_be32( void const* p ) +{ + return (blargg_ulong) ((unsigned char const*) p) [0] << 24 | + (blargg_ulong) ((unsigned char const*) p) [1] << 16 | + (blargg_ulong) ((unsigned char const*) p) [2] << 8 | + (blargg_ulong) ((unsigned char const*) p) [3]; +} + +inline void set_le16( void* p, unsigned n ) +{ + ((unsigned char*) p) [1] = (unsigned char) (n >> 8); + ((unsigned char*) p) [0] = (unsigned char) n; +} + +inline void set_be16( void* p, unsigned n ) +{ + ((unsigned char*) p) [0] = (unsigned char) (n >> 8); + ((unsigned char*) p) [1] = (unsigned char) n; +} + +inline void set_le32( void* p, blargg_ulong n ) +{ + ((unsigned char*) p) [0] = (unsigned char) n; + ((unsigned char*) p) [1] = (unsigned char) (n >> 8); + ((unsigned char*) p) [2] = (unsigned char) (n >> 16); + ((unsigned char*) p) [3] = (unsigned char) (n >> 24); +} + +inline void set_be32( void* p, blargg_ulong n ) +{ + ((unsigned char*) p) [3] = (unsigned char) n; + ((unsigned char*) p) [2] = (unsigned char) (n >> 8); + ((unsigned char*) p) [1] = (unsigned char) (n >> 16); + ((unsigned char*) p) [0] = (unsigned char) (n >> 24); +} + +#if BLARGG_NONPORTABLE + // Optimized implementation if byte order is known + #if BLARGG_LITTLE_ENDIAN + #define GET_LE16( addr ) (*(BOOST::uint16_t*) (addr)) + #define GET_LE32( addr ) (*(BOOST::uint32_t*) (addr)) + #define SET_LE16( addr, data ) (void) (*(BOOST::uint16_t*) (addr) = (data)) + #define SET_LE32( addr, data ) (void) (*(BOOST::uint32_t*) (addr) = (data)) + #elif BLARGG_BIG_ENDIAN + #define GET_BE16( addr ) (*(BOOST::uint16_t*) (addr)) + #define GET_BE32( addr ) (*(BOOST::uint32_t*) (addr)) + #define SET_BE16( addr, data ) (void) (*(BOOST::uint16_t*) (addr) = (data)) + #define SET_BE32( addr, data ) (void) (*(BOOST::uint32_t*) (addr) = (data)) + + #if BLARGG_CPU_POWERPC + // PowerPC has special byte-reversed instructions + #if defined (__MWERKS__) + #define GET_LE16( addr ) (__lhbrx( addr, 0 )) + #define GET_LE32( addr ) (__lwbrx( addr, 0 )) + #define SET_LE16( addr, in ) (__sthbrx( in, addr, 0 )) + #define SET_LE32( addr, in ) (__stwbrx( in, addr, 0 )) + #elif defined (__GNUC__) + #define GET_LE16( addr ) ({unsigned ppc_lhbrx_; asm( "lhbrx %0,0,%1" : "=r" (ppc_lhbrx_) : "r" (addr), "0" (ppc_lhbrx_) ); ppc_lhbrx_;}) + #define GET_LE32( addr ) ({unsigned ppc_lwbrx_; asm( "lwbrx %0,0,%1" : "=r" (ppc_lwbrx_) : "r" (addr), "0" (ppc_lwbrx_) ); ppc_lwbrx_;}) + #define SET_LE16( addr, in ) ({asm( "sthbrx %0,0,%1" : : "r" (in), "r" (addr) );}) + #define SET_LE32( addr, in ) ({asm( "stwbrx %0,0,%1" : : "r" (in), "r" (addr) );}) + #endif + #endif + #endif +#endif + +#ifndef GET_LE16 + #define GET_LE16( addr ) get_le16( addr ) + #define SET_LE16( addr, data ) set_le16( addr, data ) +#endif + +#ifndef GET_LE32 + #define GET_LE32( addr ) get_le32( addr ) + #define SET_LE32( addr, data ) set_le32( addr, data ) +#endif + +#ifndef GET_BE16 + #define GET_BE16( addr ) get_be16( addr ) + #define SET_BE16( addr, data ) set_be16( addr, data ) +#endif + +#ifndef GET_BE32 + #define GET_BE32( addr ) get_be32( addr ) + #define SET_BE32( addr, data ) set_be32( addr, data ) +#endif + +// auto-selecting versions + +inline void set_le( BOOST::uint16_t* p, unsigned n ) { SET_LE16( p, n ); } +inline void set_le( BOOST::uint32_t* p, blargg_ulong n ) { SET_LE32( p, n ); } +inline void set_be( BOOST::uint16_t* p, unsigned n ) { SET_BE16( p, n ); } +inline void set_be( BOOST::uint32_t* p, blargg_ulong n ) { SET_BE32( p, n ); } +inline unsigned get_le( BOOST::uint16_t* p ) { return GET_LE16( p ); } +inline blargg_ulong get_le( BOOST::uint32_t* p ) { return GET_LE32( p ); } +inline unsigned get_be( BOOST::uint16_t* p ) { return GET_BE16( p ); } +inline blargg_ulong get_be( BOOST::uint32_t* p ) { return GET_BE32( p ); } + +#endif diff --git a/src/engine/platform/sound/snes/blargg_source.h b/src/engine/platform/sound/snes/blargg_source.h new file mode 100644 index 000000000..5e45c4fb4 --- /dev/null +++ b/src/engine/platform/sound/snes/blargg_source.h @@ -0,0 +1,100 @@ +/* Included at the beginning of library source files, after all other #include lines. +Sets up helpful macros and services used in my source code. They don't need +module an annoying module prefix on their names since they are defined after +all other #include lines. */ + +// snes_spc 0.9.0 +#ifndef BLARGG_SOURCE_H +#define BLARGG_SOURCE_H + +// If debugging is enabled, abort program if expr is false. Meant for checking +// internal state and consistency. A failed assertion indicates a bug in the module. +// void assert( bool expr ); +#include + +// If debugging is enabled and expr is false, abort program. Meant for checking +// caller-supplied parameters and operations that are outside the control of the +// module. A failed requirement indicates a bug outside the module. +// void require( bool expr ); +#undef require +#define require( expr ) assert( expr ) + +// Like printf() except output goes to debug log file. Might be defined to do +// nothing (not even evaluate its arguments). +// void dprintf( const char* format, ... ); +static inline void blargg_dprintf_( const char*, ... ) { } +#undef dprintf +#define dprintf (1) ? (void) 0 : blargg_dprintf_ + +// If enabled, evaluate expr and if false, make debug log entry with source file +// and line. Meant for finding situations that should be examined further, but that +// don't indicate a problem. In all cases, execution continues normally. +#undef check +#define check( expr ) ((void) 0) + +// If expr yields error string, return it from current function, otherwise continue. +#undef RETURN_ERR +#define RETURN_ERR( expr ) do { \ + blargg_err_t blargg_return_err_ = (expr); \ + if ( blargg_return_err_ ) return blargg_return_err_; \ + } while ( 0 ) + +// If ptr is 0, return out of memory error string. +#undef CHECK_ALLOC +#define CHECK_ALLOC( ptr ) do { if ( (ptr) == 0 ) return "Out of memory"; } while ( 0 ) + +// Avoid any macros which evaluate their arguments multiple times +#undef min +#undef max + +#define DEF_MIN_MAX( type ) \ + static inline type min( type x, type y ) { if ( x < y ) return x; return y; }\ + static inline type max( type x, type y ) { if ( y < x ) return x; return y; } + +DEF_MIN_MAX( int ) +DEF_MIN_MAX( unsigned ) +DEF_MIN_MAX( long ) +DEF_MIN_MAX( unsigned long ) +DEF_MIN_MAX( float ) +DEF_MIN_MAX( double ) + +#undef DEF_MIN_MAX + +/* +// using const references generates crappy code, and I am currenly only using these +// for built-in types, so they take arguments by value + +// TODO: remove +inline int min( int x, int y ) +template +inline T min( T x, T y ) +{ + if ( x < y ) + return x; + return y; +} + +template +inline T max( T x, T y ) +{ + if ( x < y ) + return y; + return x; +} +*/ + +// TODO: good idea? bad idea? +#undef byte +#define byte byte_ +typedef unsigned char byte; + +// deprecated +#define BLARGG_CHECK_ALLOC CHECK_ALLOC +#define BLARGG_RETURN_ERR RETURN_ERR + +// BLARGG_SOURCE_BEGIN: If defined, #included, allowing redefition of dprintf and check +#ifdef BLARGG_SOURCE_BEGIN + #include BLARGG_SOURCE_BEGIN +#endif + +#endif diff --git a/src/gui/gui.h b/src/gui/gui.h index 2ee6170aa..005b2f26e 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -154,6 +154,7 @@ enum FurnaceGUIColors { GUI_COLOR_INSTR_SU, GUI_COLOR_INSTR_NAMCO, GUI_COLOR_INSTR_OPL_DRUMS, + GUI_COLOR_INSTR_SNES, GUI_COLOR_INSTR_UNKNOWN, GUI_COLOR_CHANNEL_FM, diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 80f9b5d85..9eb1e6d09 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -113,6 +113,7 @@ const char* insTypes[DIV_INS_MAX+1]={ "Sound Unit", "Namco WSG", "OPL (drums)", + "SNES", NULL }; @@ -126,7 +127,7 @@ const char* sampleDepths[17]={ "ADPCM-B", NULL, "8-bit PCM", - NULL, // "BRR", + "BRR", "VOX", NULL, NULL, From b49b596fbe99fef1d92c099a4f67feee595c4339 Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Fri, 10 Jun 2022 22:59:44 +0700 Subject: [PATCH 2/5] WIP SNES support, part 2 Make the code build again, still no sound though --- src/engine/instrument.h | 1 - src/engine/platform/snes.cpp | 282 +++++++++++++-------- src/engine/platform/snes.h | 48 ++-- src/engine/platform/sound/snes/SPC_DSP.cpp | 9 +- src/engine/platform/sound/snes/SPC_DSP.h | 17 +- src/engine/sample.h | 3 +- src/gui/gui.h | 1 - src/gui/guiConst.cpp | 2 +- src/gui/presets.cpp | 14 +- src/gui/sysConf.cpp | 1 + 10 files changed, 249 insertions(+), 129 deletions(-) diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 66197a0fe..c4af6f042 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -60,7 +60,6 @@ enum DivInstrumentType: unsigned short { DIV_INS_SU=30, DIV_INS_NAMCO=31, DIV_INS_OPL_DRUMS=32, - DIV_INS_SNES=33, DIV_INS_MAX, DIV_INS_NULL }; diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index de96ee968..8f930342a 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -19,11 +19,12 @@ #include "snes.h" #include "../engine.h" +#include "../../ta-log.h" #include -#define CHIP_DIVIDER 16 +#define CHIP_FREQBASE 4096 -#define rWrite(a,v) {dsp->write(a,v); regPool[(a)&0x7f]=v; } +#define rWrite(a,v) {dsp.write(a,v); regPool[(a)&0x7f]=v; } const char* regCheatSheetSNESDSP[]={ "VxVOLL", "x0", @@ -61,111 +62,134 @@ const char** DivPlatformSNES::getRegisterSheet() { return regCheatSheetSNESDSP; } +const char* DivPlatformSNES::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xx: Set echo feedback level (signed 8-bit)"; + break; + // TODO + } + return NULL; +} + void DivPlatformSNES::acquire(short* bufL, short* bufR, size_t start, size_t len) { + short out[2]; + short chOut[16]; for (size_t h=start; hset_output(out,1); - dsp->run(32); + dsp.set_output(out,1); + dsp.run(32); + dsp.get_voice_outputs(chOut); bufL[h]=out[0]; bufR[h]=out[1]; + for (int i=0; i<8; i++) { + oscBuf[i]->data[oscBuf[i]->needle++]=chOut[i*2]+chOut[i*2+1]; + } } } void DivPlatformSNES::tick() { + // KON/KOFF can't be written several times per one sample + // so they have to be accumulated + unsigned char kon=0; + unsigned char koff=0; for (int i=0; i<8; i++) { chan[i].std.next(); - if (chan[i].std.hadVol) { - chan[i].outVol=((chan[i].vol%65)*MIN(64,chan[i].std.vol))>>6; + if (chan[i].std.vol.had) { + // TODO handle gain writes } - double off=1.0; - if (chan[i].sample>=0 && chan[i].samplesong.sampleLen) { - DivSample* s=parent->getSample(chan[i].sample); - if (s->centerRate<1) { - off=1.0; - } else { - off=8363.0/(double)s->centerRate; - } - } - if (chan[i].std.hadArp) { + if (chan[i].std.arp.had) { if (!chan[i].inPorta) { - if (chan[i].std.arpMode) { - chan[i].baseFreq=off*NOTE_PERIODIC(chan[i].std.arp); + if (chan[i].std.arp.mode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp.val); } else { - chan[i].baseFreq=off*NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+chan[i].std.arp.val); } } chan[i].freqChanged=true; } else { - if (chan[i].std.arpMode && chan[i].std.finishedArp) { - chan[i].baseFreq=off*NOTE_PERIODIC(chan[i].note); + if (chan[i].std.arp.mode && chan[i].std.arp.finished) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); chan[i].freqChanged=true; } } - if (chan[i].std.hadWave) { - if (chan[i].wave!=chan[i].std.wave) { - chan[i].wave=chan[i].std.wave; - if (!chan[i].keyOff) chan[i].keyOn=true; + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-32768,32767); + } else { + chan[i].pitch2=chan[i].std.pitch.val; } + chan[i].freqChanged=true; + } + if (chan[i].std.panL.had) { + int val=chan[i].std.panL.val&0x7f; + chan[i].panL=(val<<1)|(val>>6); + } + if (chan[i].std.panR.had) { + int val=chan[i].std.panR.val&0x7f; + chan[i].panR=(val<<1)|(val>>6); + } + if (chan[i].std.panL.had || chan[i].std.panR.had) { + writeOutVol(i); + } + if (chan[i].setPos) { + // force keyon + chan[i].keyOn=true; + chan[i].setPos=false; + } else { + chan[i].audPos=0; } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { - //DivInstrument* ins=parent->getIns(chan[i].ins); - chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); - if (chan[i].freq>4095) chan[i].freq=4095; - if (chan[i].note>0x5d) chan[i].freq=0x01; + DivSample* s=parent->getSample(chan[i].sample); + double off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; + chan[i].freq=(unsigned int)(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE)); + if (chan[i].freq>16383) chan[i].freq=16383; if (chan[i].keyOn) { - if (chan[i].wave<0) { - chan[i].wave=0; - } + // TODO handle sample offsets + kon|=(1<>8); + chan[i].freqChanged=false; } - if (chan[i].keyOn) chan[i].keyOn=false; - if (chan[i].keyOff) chan[i].keyOff=false; - chan[i].freqChanged=false; } } + rWrite(0x4c,kon); + rWrite(0x5c,koff); } int DivPlatformSNES::dispatch(DivCommand c) { switch (c.cmd) { case DIV_CMD_NOTE_ON: { - DivInstrument* ins=parent->getIns(chan[c.chan].ins); - chan[c.chan].sample=ins->amiga.initSample; - double off=1.0; - if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { - DivSample* s=parent->getSample(chan[c.chan].sample); - if (s->centerRate<1) { - off=1.0; - } else { - off=8363.0/(double)s->centerRate; - } - } + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA); + chan[c.chan].sample=ins->amiga.getSample(c.value); if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].baseFreq=off*NOTE_PERIODIC(c.value); + chan[c.chan].baseFreq=round(NOTE_FREQUENCY(c.value)); } - if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { + if (chan[c.chan].useWave || chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { chan[c.chan].sample=-1; } - if (chan[c.chan].setPos) { - chan[c.chan].setPos=false; - } else { - chan[c.chan].audPos=0; - } - chan[c.chan].audSub=0; if (c.value!=DIV_NOTE_NULL) { 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(ins); + chan[c.chan].macroInit(ins); + chan[c.chan].insChanged=false; break; } case DIV_CMD_NOTE_OFF: chan[c.chan].sample=-1; chan[c.chan].active=false; chan[c.chan].keyOff=true; - chan[c.chan].std.init(NULL); + chan[c.chan].macroInit(NULL); break; case DIV_CMD_NOTE_OFF_ENV: case DIV_CMD_ENV_RELEASE: @@ -174,32 +198,35 @@ int DivPlatformSNES::dispatch(DivCommand c) { case DIV_CMD_INSTRUMENT: if (chan[c.chan].ins!=c.value || c.value2==1) { chan[c.chan].ins=c.value; + chan[c.chan].insChanged=true; } 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; - } + writeOutVol(c.chan); } break; + // case DIV_CMD_GLOBAL_VOLUME: + // gblVolL=MIN(c.value,127); + // gblVolR=MIN(c.value,127); + // rWrite(0x0c,gblVolL); + // rWrite(0x1c,gblVolR); + // break; case DIV_CMD_GET_VOLUME: - if (chan[c.chan].std.hasVol) { - return chan[c.chan].vol; - } - return chan[c.chan].outVol; + return chan[c.chan].vol; + break; + case DIV_CMD_PANNING: + chan[c.chan].panL=c.value; + chan[c.chan].panR=c.value2; + writeOutVol(c.chan); break; case DIV_CMD_PITCH: chan[c.chan].pitch=c.value; chan[c.chan].freqChanged=true; break; - case DIV_CMD_WAVE: - chan[c.chan].wave=c.value; - chan[c.chan].keyOn=true; - break; case DIV_CMD_NOTE_PORTA: { - int destFreq=NOTE_PERIODIC(c.value2); + int destFreq=round(NOTE_FREQUENCY(c.value2)); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value; @@ -222,23 +249,14 @@ int DivPlatformSNES::dispatch(DivCommand c) { break; } case DIV_CMD_LEGATO: { - double off=1.0; - if (chan[c.chan].sample>=0 && chan[c.chan].samplesong.sampleLen) { - DivSample* s=parent->getSample(chan[c.chan].sample); - if (s->centerRate<1) { - off=1.0; - } else { - off=8363.0/(double)s->centerRate; - } - } - chan[c.chan].baseFreq=off*NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp-12):(0))); + chan[c.chan].baseFreq=round(NOTE_FREQUENCY(c.value+((chan[c.chan].std.arp.will && !chan[c.chan].std.arp.mode)?(chan[c.chan].std.arp.val):(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)); + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA)); } chan[c.chan].inPorta=c.value; break; @@ -246,28 +264,37 @@ int DivPlatformSNES::dispatch(DivCommand c) { chan[c.chan].audPos=c.value; chan[c.chan].setPos=true; break; + // TODO SNES-specific commands case DIV_CMD_GET_VOLMAX: return 127; break; - case DIV_ALWAYS_SET_VOLUME: - return 1; - break; default: break; } return 1; } +void DivPlatformSNES::writeOutVol(int ch) { + // TODO negative (inverted) panning support + int outL=0; + int outR=0; + if (!isMuted[ch]) { + outL=chan[ch].vol*chan[ch].panL/255; + outR=chan[ch].vol*chan[ch].panR/255; + } + rWrite(0+ch*16,outL); + rWrite(1+ch*16,outR); +} + void DivPlatformSNES::muteChannel(int ch, bool mute) { isMuted[ch]=mute; + writeOutVol(ch); } void DivPlatformSNES::forceIns() { for (int i=0; i<8; i++) { chan[i].insChanged=true; chan[i].freqChanged=true; - chan[i].audPos=131072; - chan[i].audDat=0; chan[i].sample=-1; } } @@ -276,12 +303,21 @@ void* DivPlatformSNES::getChanState(int ch) { return &chan[ch]; } +DivMacroInt* DivPlatformSNES::getChanMacroInt(int ch) { + return &chan[ch].std; +} + +DivDispatchOscBuffer* DivPlatformSNES::getOscBuffer(int ch) { + return oscBuf[ch]; +} + unsigned char* DivPlatformSNES::getRegisterPool() { // get states from emulator for (int i=0; i<0x80; i+=0x10) { - regPool[i+8]=dsp->read(i+8); - regPool[i+9]=dsp->read(i+9); + regPool[i+8]=dsp.read(i+8); + regPool[i+9]=dsp.read(i+9); } + regPool[0x7c]=dsp.read(0x7c); // ENDX return regPool; } @@ -290,22 +326,25 @@ int DivPlatformSNES::getRegisterPoolSize() { } void DivPlatformSNES::reset() { + dsp.init(sampleMem); + dsp.set_output(NULL,0); + memset(regPool,0,128); + // TODO more initial values + sampleTableBase=0; + rWrite(0x5d,sampleTableBase>>8); + rWrite(0x0c,127); // global volume left + rWrite(0x1c,127); // global volume right for (int i=0; i<8; i++) { chan[i]=Channel(); + chan[i].std.setEngine(parent); + writeOutVol(i); } - dsp->init(&aram); - dsp->set_output(NULL, 0); - memset(regPool,0,128); } bool DivPlatformSNES::isStereo() { return true; } -bool DivPlatformSNES::keyOffAffectsArp(int ch) { - return true; -} - void DivPlatformSNES::notifyInsChange(int ins) { for (int i=0; i<8; i++) { if (chan[i].ins==ins) { @@ -314,10 +353,6 @@ void DivPlatformSNES::notifyInsChange(int ins) { } } -void DivPlatformSNES::notifyWaveChange(int wave) { - // TODO when wavetables are added -} - void DivPlatformSNES::notifyInsDeletion(void* ins) { for (int i=0; i<8; i++) { chan[i].std.notifyInsDeletion((DivInstrument*)ins); @@ -332,14 +367,59 @@ void DivPlatformSNES::poke(std::vector& wlist) { for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); } +const void* DivPlatformSNES::getSampleMem(int index) { + return index == 0 ? sampleMem : NULL; +} + +size_t DivPlatformSNES::getSampleMemCapacity(int index) { + // TODO change it based on current echo buffer size + return index == 0 ? 65536 : 0; +} + +size_t DivPlatformSNES::getSampleMemUsage(int index) { + return index == 0 ? sampleMemLen : 0; +} + +void DivPlatformSNES::renderSamples() { + memset(sampleMem,0,getSampleMemCapacity()); + + // skip past sample table + size_t memPos=sampleTableBase+0x400; + for (int i=0; isong.sampleLen; i++) { + DivSample* s=parent->song.sample[i]; + int length=s->lengthBRR; + int actualLength=MIN((int)(getSampleMemCapacity()-memPos)/9*9,length); + if (actualLength>0) { + size_t tabAddr=sampleTableBase+i*4; + size_t loopPos=memPos; + if (s->loopStart>=0) loopPos+=s->loopStart; + s->offSNES=memPos; + sampleMem[tabAddr+0]=memPos&0xff; + sampleMem[tabAddr+1]=memPos>>8; + sampleMem[tabAddr+2]=loopPos&0xff; + sampleMem[tabAddr+3]=loopPos>>8; + memcpy(&sampleMem[memPos],s->data8,actualLength); + memPos+=actualLength; + } + if (actualLength& wlist); const char** getRegisterSheet(); + const char* getEffectName(unsigned char effect); + const void* getSampleMem(int index = 0); + size_t getSampleMemCapacity(int index = 0); + size_t getSampleMemUsage(int index = 0); + void renderSamples(); int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); void quit(); + private: + void writeOutVol(int ch); }; #endif diff --git a/src/engine/platform/sound/snes/SPC_DSP.cpp b/src/engine/platform/sound/snes/SPC_DSP.cpp index abbf827db..6510f460a 100644 --- a/src/engine/platform/sound/snes/SPC_DSP.cpp +++ b/src/engine/platform/sound/snes/SPC_DSP.cpp @@ -491,10 +491,11 @@ VOICE_CLOCK( V3c ) if ( !v->kon_delay ) run_envelope( v ); } -inline void SPC_DSP::voice_output( voice_t const* v, int ch ) +inline void SPC_DSP::voice_output( voice_t* const v, int ch ) { // Apply left/right volume int amp = (m.t_output * (int8_t) VREG(v->regs,voll + ch)) >> 7; + v->out [ch] = (sample_t) amp; // Furnace addition // Add to output total m.t_main_out [ch] += amp; @@ -801,8 +802,12 @@ void SPC_DSP::run( int clocks_remain ) switch ( phase ) { loop: - + // GCC, why +#ifdef __GNUC__ + #define PHASE( n ) if ( n && !--clocks_remain ) break; __attribute__ ((fallthrough)); case n: +#else #define PHASE( n ) if ( n && !--clocks_remain ) break; case n: +#endif GEN_DSP_TIMING #undef PHASE diff --git a/src/engine/platform/sound/snes/SPC_DSP.h b/src/engine/platform/sound/snes/SPC_DSP.h index d59284797..879ee703d 100644 --- a/src/engine/platform/sound/snes/SPC_DSP.h +++ b/src/engine/platform/sound/snes/SPC_DSP.h @@ -64,6 +64,9 @@ public: // Returns non-zero if new key-on events occurred since last call bool check_kon(); + + // Furnace addition, gets all current voice outputs to an array of samples + void get_voice_outputs( sample_t* outs ); // DSP register addresses @@ -118,6 +121,7 @@ public: int env; // current envelope level int hidden_env; // used by GAIN mode 7, very obscure quirk uint8_t t_envx_out; + sample_t out[2]; // Furnace addition, for per-channel oscilloscope }; private: enum { brr_block_size = 9 }; @@ -200,7 +204,7 @@ private: void misc_29(); void misc_30(); - void voice_output( voice_t const* v, int ch ); + void voice_output( voice_t* const v, int ch ); void voice_V1( voice_t* const ); void voice_V2( voice_t* const ); void voice_V3( voice_t* const ); @@ -283,6 +287,17 @@ inline bool SPC_DSP::check_kon() return old; } +inline void SPC_DSP::get_voice_outputs( sample_t* outs ) +{ + int i; + for ( i = 0; i < voice_count; i++ ) + { + voice_t* v = &m.voices [i]; + outs [i * 2] = v->out [0]; + outs [i * 2 + 1] = v->out [1]; + } +} + #if !SPC_NO_COPY_STATE_FUNCS class SPC_State_Copier { diff --git a/src/engine/sample.h b/src/engine/sample.h index d9ad633c9..5195da1be 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -86,7 +86,7 @@ struct DivSample { unsigned int length8, length16, length1, lengthDPCM, lengthZ, lengthQSoundA, lengthA, lengthB, lengthBRR, lengthVOX; unsigned int off8, off16, off1, offDPCM, offZ, offQSoundA, offA, offB, offBRR, offVOX; - unsigned int offSegaPCM, offQSound, offX1_010, offSU, offYMZ280B, offRF5C68; + unsigned int offSegaPCM, offQSound, offX1_010, offSU, offYMZ280B, offRF5C68, offSNES; unsigned int samples; @@ -249,6 +249,7 @@ struct DivSample { offX1_010(0), offSU(0), offRF5C68(0), + offSNES(0), samples(0) {} ~DivSample(); }; diff --git a/src/gui/gui.h b/src/gui/gui.h index 005b2f26e..2ee6170aa 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -154,7 +154,6 @@ enum FurnaceGUIColors { GUI_COLOR_INSTR_SU, GUI_COLOR_INSTR_NAMCO, GUI_COLOR_INSTR_OPL_DRUMS, - GUI_COLOR_INSTR_SNES, GUI_COLOR_INSTR_UNKNOWN, GUI_COLOR_CHANNEL_FM, diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index 9eb1e6d09..532df4e17 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -113,7 +113,6 @@ const char* insTypes[DIV_INS_MAX+1]={ "Sound Unit", "Namco WSG", "OPL (drums)", - "SNES", NULL }; @@ -895,6 +894,7 @@ const int availableSystems[]={ DIV_SYSTEM_MSM6258, DIV_SYSTEM_MSM6295, DIV_SYSTEM_RF5C68, + DIV_SYSTEM_SNES, 0 // don't remove this last one! }; diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index 994902e8e..b2c86440b 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -307,7 +307,13 @@ void FurnaceGUI::initSystemPresets() { DIV_SYSTEM_MSM6295, 64, 0, 0, 0 } - )); + )); + cat.systems.push_back(FurnaceGUISysDef( + "SNES", { + DIV_SYSTEM_SNES, 64, 0, 0, + 0 + } + )); sysCategories.push_back(cat); cat=FurnaceGUISysCategory("Wavetable","chips which use user-specified waveforms to generate sound."); @@ -589,6 +595,12 @@ void FurnaceGUI::initSystemPresets() { 0 } )); + cat.systems.push_back(FurnaceGUISysDef( + "SNES", { + DIV_SYSTEM_SNES, 64, 0, 0, + 0 + } + )); cat.systems.push_back(FurnaceGUISysDef( "Mattel Intellivision", { DIV_SYSTEM_AY8910, 64, 0, 48, diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index 89d35b0b2..9ab85da2a 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -489,6 +489,7 @@ void FurnaceGUI::drawSysConf(int chan, DivSystem type, unsigned int& flags, bool case DIV_SYSTEM_SCC: case DIV_SYSTEM_SCC_PLUS: case DIV_SYSTEM_YMZ280B: + case DIV_SYSTEM_SNES: ImGui::Text("nothing to configure"); break; default: From 73c6adb82159ba98547aa7f6ac35f8a3e1d3c7f5 Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Sun, 12 Jun 2022 14:42:16 +0700 Subject: [PATCH 3/5] wip snes 3 --- .gitignore | 1 + src/engine/platform/snes.cpp | 2 +- src/engine/platform/snes.h | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 25bd6d0e0..52fc43f4b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ winbuild/ win32build/ macbuild/ linuxbuild/ +webbuild/ *.swp .cache/ .DS_Store diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index 8f930342a..c13fa6bf8 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -87,7 +87,7 @@ void DivPlatformSNES::acquire(short* bufL, short* bufR, size_t start, size_t len } } -void DivPlatformSNES::tick() { +void DivPlatformSNES::tick(bool sysTick) { // KON/KOFF can't be written several times per one sample // so they have to be accumulated unsigned char kon=0; diff --git a/src/engine/platform/snes.h b/src/engine/platform/snes.h index 2b49a68c2..7e8d6882b 100644 --- a/src/engine/platform/snes.h +++ b/src/engine/platform/snes.h @@ -82,7 +82,7 @@ class DivPlatformSNES: public DivDispatch { int getRegisterPoolSize(); void reset(); void forceIns(); - void tick(); + void tick(bool sysTick=true); void muteChannel(int ch, bool mute); bool isStereo(); void notifyInsChange(int ins); From 7956d41f1bcf5d96461b92dfcdc80f35569d469e Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Sun, 18 Sep 2022 17:59:58 +0700 Subject: [PATCH 4/5] SNES: Get wavesynth and envelope working No samples, inverted volumes and E/P/N yet It's been 3 months... --- .gitignore | 1 - src/engine/instrument.cpp | 9 +- src/engine/instrument.h | 18 +--- src/engine/platform/snes.cpp | 142 ++++++++++++++++++++++------ src/engine/platform/snes.h | 15 ++- src/gui/insEdit.cpp | 178 ++++++++++++++++++----------------- 6 files changed, 224 insertions(+), 139 deletions(-) diff --git a/.gitignore b/.gitignore index 52fc43f4b..25bd6d0e0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ winbuild/ win32build/ macbuild/ linuxbuild/ -webbuild/ *.swp .cache/ .DS_Store diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index 22cdddf4b..b6470fc5f 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -558,9 +558,10 @@ void DivInstrument::putInsData(SafeWriter* w) { w->writeC(es5506.envelope.k2Slow); // SNES + // @tildearrow please update this w->writeC(snes.useEnv); - w->writeC(snes.gainMode); - w->writeC(snes.gain); + w->writeC(0); + w->writeC(0); w->writeC(snes.a); w->writeC(snes.d); w->writeC(snes.s); @@ -1250,8 +1251,8 @@ DivDataErrors DivInstrument::readInsData(SafeReader& reader, short version) { // SNES if (version>=109) { snes.useEnv=reader.readC(); - snes.gainMode=(DivInstrumentSNES::GainMode)reader.readC(); - snes.gain=reader.readC(); + reader.readC(); + reader.readC(); snes.a=reader.readC(); snes.d=reader.readC(); snes.s=reader.readC(); diff --git a/src/engine/instrument.h b/src/engine/instrument.h index e5372933d..d0dc78f31 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -501,25 +501,17 @@ struct DivInstrumentES5506 { }; struct DivInstrumentSNES { - enum GainMode: unsigned char { - GAIN_MODE_DIRECT=0, - GAIN_MODE_DEC_LINEAR=4, - GAIN_MODE_DEC_LOG=5, - GAIN_MODE_INC_LINEAR=6, - GAIN_MODE_INC_INVLOG=7 - }; - bool useEnv; - GainMode gainMode; - unsigned char gain; + bool useEnv, applyFIR; unsigned char a, d, s, r; + signed char fir[8]; DivInstrumentSNES(): useEnv(true), - gainMode(GAIN_MODE_DIRECT), - gain(127), + applyFIR(false), a(15), d(7), s(7), - r(0) {} + r(0), + fir{127, 0, 0, 0, 0, 0, 0, 0} {} }; struct DivInstrument { diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index c13fa6bf8..c4239c603 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -22,9 +22,12 @@ #include "../../ta-log.h" #include -#define CHIP_FREQBASE 4096 +#define CHIP_FREQBASE 131072 #define rWrite(a,v) {dsp.write(a,v); regPool[(a)&0x7f]=v; } +#define chWrite(c,a,v) {rWrite((a)+(c)*16,v)} +#define sampleTableAddr(c) (sampleTableBase+(c)*4) +#define waveTableAddr(c) (sampleTableBase+8*4+(c)*9*16) const char* regCheatSheetSNESDSP[]={ "VxVOLL", "x0", @@ -62,16 +65,6 @@ const char** DivPlatformSNES::getRegisterSheet() { return regCheatSheetSNESDSP; } -const char* DivPlatformSNES::getEffectName(unsigned char effect) { - switch (effect) { - case 0x10: - return "10xx: Set echo feedback level (signed 8-bit)"; - break; - // TODO - } - return NULL; -} - void DivPlatformSNES::acquire(short* bufL, short* bufR, size_t start, size_t len) { short out[2]; short chOut[16]; @@ -90,12 +83,23 @@ void DivPlatformSNES::acquire(short* bufL, short* bufR, size_t start, size_t len void DivPlatformSNES::tick(bool sysTick) { // KON/KOFF can't be written several times per one sample // so they have to be accumulated + // TODO due to pipelining, KON/KOFF writes need to be delayed to accomodate sample address changes in the table unsigned char kon=0; unsigned char koff=0; for (int i=0; i<8; i++) { + DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_AMIGA); + bool hadGain=chan[i].std.vol.had || chan[i].std.ex1.had || chan[i].std.ex2.had; chan[i].std.next(); - if (chan[i].std.vol.had) { - // TODO handle gain writes + if (ins->type==DIV_INS_AMIGA && chan[i].std.vol.had) { + chWrite(i,7,MIN(127,chan[i].std.vol.val*2)); + } else if (!chan[i].useEnv && hadGain) { + if (chan[i].std.ex1.val==0) { + // direct gain + chWrite(i,7,chan[i].std.vol.val); + } else { + // inc/dec + chWrite(i,7,chan[i].std.ex2.val|((chan[i].std.ex1.val-1)<<5)|0x80); + } } if (chan[i].std.arp.had) { if (!chan[i].inPorta) { @@ -112,6 +116,12 @@ void DivPlatformSNES::tick(bool sysTick) { chan[i].freqChanged=true; } } + if (chan[i].useWave && chan[i].std.wave.had) { + if (chan[i].wave!=chan[i].std.wave.val || chan[i].ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + chan[i].ws.changeWave1(chan[i].wave); + } + } if (chan[i].std.pitch.had) { if (chan[i].std.pitch.mode) { chan[i].pitch2+=chan[i].std.pitch.val; @@ -139,13 +149,46 @@ void DivPlatformSNES::tick(bool sysTick) { } else { chan[i].audPos=0; } + if (chan[i].useWave && chan[i].active) { + if (chan[i].ws.tick()) { + updateWave(i); + } + } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { DivSample* s=parent->getSample(chan[i].sample); double off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; chan[i].freq=(unsigned int)(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE)); if (chan[i].freq>16383) chan[i].freq=16383; if (chan[i].keyOn) { - // TODO handle sample offsets + unsigned int start, end, loop; + size_t tabAddr=sampleTableAddr(i); + if (chan[i].useEnv) { + chWrite(i,5,ins->snes.a|(ins->snes.d<<4)|0x80); + chWrite(i,6,ins->snes.r|(ins->snes.s<<5)); + } else { + chWrite(i,5,0); + } + if (chan[i].useWave) { + start=waveTableAddr(i); + loop=start; + } else { + start=s->offSNES; + end=MIN(start+MAX(s->lengthBRR,1),getSampleMemCapacity()); + loop=MAX(start,end-1); + if (chan[i].audPos>0) { + start=start+MIN(chan[i].audPos,s->lengthBRR-1)/16*9; + } + if (s->loopStart>=0) { + loop=start+s->loopStart/16*9; + } + } + sampleMem[tabAddr+0]=start&0xff; + sampleMem[tabAddr+1]=start>>8; + sampleMem[tabAddr+2]=loop&0xff; + sampleMem[tabAddr+3]=loop>>8; + if (!hadGain) { + chWrite(i,7,0x7f); + } kon|=(1<>8); + chWrite(i,2,chan[i].freq&0xff); + chWrite(i,3,chan[i].freq>>8); chan[i].freqChanged=false; } } @@ -168,20 +211,36 @@ int DivPlatformSNES::dispatch(DivCommand c) { switch (c.cmd) { case DIV_CMD_NOTE_ON: { DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA); - chan[c.chan].sample=ins->amiga.getSample(c.value); - if (c.value!=DIV_NOTE_NULL) { - chan[c.chan].baseFreq=round(NOTE_FREQUENCY(c.value)); + if (ins->amiga.useWave) { + chan[c.chan].useWave=true; + chan[c.chan].wtLen=(unsigned int)(ins->amiga.waveLen)+1; + if (chan[c.chan].insChanged) { + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + chan[c.chan].ws.setWidth(chan[c.chan].wtLen); + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + } + } + } else { + chan[c.chan].sample=ins->amiga.getSample(c.value); + chan[c.chan].useWave=false; } if (chan[c.chan].useWave || chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { chan[c.chan].sample=-1; } if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=round(NOTE_FREQUENCY(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].macroInit(ins); + if (ins->type==DIV_INS_SNES) { + // initialize to max gain in case of direct gain mode macro without gain level macro + chan[c.chan].std.vol.val=0x7f; + chan[c.chan].useEnv=ins->snes.useEnv; + } chan[c.chan].insChanged=false; break; } @@ -274,6 +333,20 @@ int DivPlatformSNES::dispatch(DivCommand c) { return 1; } +void DivPlatformSNES::updateWave(int ch) { + // Due to the overflow bug in hardware's resampler, the written amplitude here is half of maximum + size_t pos=waveTableAddr(ch); + for (int i=0; i>8); rWrite(0x0c,127); // global volume left rWrite(0x1c,127); // global volume right + rWrite(0x6c,0); // get DSP out of reset for (int i=0; i<8; i++) { chan[i]=Channel(); chan[i].std.setEngine(parent); + chan[i].ws.setEngine(parent); + chan[i].ws.init(NULL,32,255); writeOutVol(i); + chWrite(i,4,i); // source number } } @@ -353,6 +431,17 @@ void DivPlatformSNES::notifyInsChange(int ins) { } } +void DivPlatformSNES::notifyWaveChange(int wave) { + for (int i=0; i<8; i++) { + if (chan[i].useWave && chan[i].wave==wave) { + chan[i].ws.changeWave1(wave); + if (chan[i].active) { + updateWave(i); + } + } + } +} + void DivPlatformSNES::notifyInsDeletion(void* ins) { for (int i=0; i<8; i++) { chan[i].std.notifyInsDeletion((DivInstrument*)ins); @@ -383,21 +472,14 @@ size_t DivPlatformSNES::getSampleMemUsage(int index) { void DivPlatformSNES::renderSamples() { memset(sampleMem,0,getSampleMemCapacity()); - // skip past sample table - size_t memPos=sampleTableBase+0x400; + // skip past sample table and wavetable buffer + size_t memPos=sampleTableBase+8*4+8*9*16; for (int i=0; isong.sampleLen; i++) { DivSample* s=parent->song.sample[i]; int length=s->lengthBRR; int actualLength=MIN((int)(getSampleMemCapacity()-memPos)/9*9,length); if (actualLength>0) { - size_t tabAddr=sampleTableBase+i*4; - size_t loopPos=memPos; - if (s->loopStart>=0) loopPos+=s->loopStart; s->offSNES=memPos; - sampleMem[tabAddr+0]=memPos&0xff; - sampleMem[tabAddr+1]=memPos>>8; - sampleMem[tabAddr+2]=loopPos&0xff; - sampleMem[tabAddr+3]=loopPos>>8; memcpy(&sampleMem[memPos],s->data8,actualLength); memPos+=actualLength; } diff --git a/src/engine/platform/snes.h b/src/engine/platform/snes.h index 7e8d6882b..058df21fc 100644 --- a/src/engine/platform/snes.h +++ b/src/engine/platform/snes.h @@ -22,19 +22,22 @@ #include "../dispatch.h" #include "../macroInt.h" +#include "../waveSynth.h" #include #include "sound/snes/SPC_DSP.h" class DivPlatformSNES: public DivDispatch { struct Channel { int freq, baseFreq, pitch, pitch2; - unsigned int audPos; - int sample, ins; + unsigned int audPos, wtLen; + int sample, wave, ins; int note; int panL, panR; bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave, setPos; signed char vol; + bool useEnv; DivMacroInt std; + DivWaveSynth ws; void macroInit(DivInstrument* which) { std.init(which); pitch2=0; @@ -45,7 +48,9 @@ class DivPlatformSNES: public DivDispatch { pitch(0), pitch2(0), audPos(0), + wtLen(16), sample(-1), + wave(-1), ins(-1), note(0), panL(255), @@ -58,7 +63,8 @@ class DivPlatformSNES: public DivDispatch { inPorta(false), useWave(false), setPos(false), - vol(127) {} + vol(127), + useEnv(false) {} }; Channel chan[8]; DivDispatchOscBuffer* oscBuf[8]; @@ -86,11 +92,11 @@ class DivPlatformSNES: public DivDispatch { void muteChannel(int ch, bool mute); bool isStereo(); void notifyInsChange(int ins); + void notifyWaveChange(int wave); 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); const void* getSampleMem(int index = 0); size_t getSampleMemCapacity(int index = 0); size_t getSampleMemUsage(int index = 0); @@ -98,6 +104,7 @@ class DivPlatformSNES: public DivDispatch { int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); void quit(); private: + void updateWave(int ch); void writeOutVol(int ch); }; diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 50fd286b8..a2558a604 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -306,6 +306,14 @@ const char* gbHWSeqCmdTypes[6]={ "Loop until Release" }; +const char* snesGainModes[5]={ + "Direct", + "Decrease (linear)", + "Decrease (logarithmic)", + "Increase (linear)", + "Increase (bent line)" +}; + // do not change these! // anything other than a checkbox will look ugly! // @@ -3603,6 +3611,18 @@ void FurnaceGUI::drawInsEdit() { } } } + if (ins->type==DIV_INS_SNES) { + P(ImGui::Checkbox("Use wavetable",&ins->amiga.useWave)); + if (ins->amiga.useWave) { + int len=ins->amiga.waveLen+1; + if (ImGui::InputInt("Width",&len,16,64)) { + if (len<16) len=16; + if (len>256) len=256; + ins->amiga.waveLen=(len&(~15))-1; + PARAMETER + } + } + } ImGui::BeginDisabled(ins->amiga.useWave); P(ImGui::Checkbox("Use sample map (does not work yet!)",&ins->amiga.useNoteMap)); if (ins->amiga.useNoteMap) { @@ -3868,95 +3888,62 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_SNES) if (ImGui::BeginTabItem("SNES")) { P(ImGui::Checkbox("Use envelope",&ins->snes.useEnv)); ImVec2 sliderSize=ImVec2(20.0f*dpiScale,128.0*dpiScale); - if (ins->snes.useEnv) { - if (ImGui::BeginTable("SNESEnvParams",5,ImGuiTableFlags_NoHostExtendX)) { - ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); - ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); - ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); - ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); - ImGui::TableSetupColumn("c4",ImGuiTableColumnFlags_WidthStretch); + if (ImGui::BeginTable("SNESEnvParams",5,ImGuiTableFlags_NoHostExtendX)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); + ImGui::TableSetupColumn("c4",ImGuiTableColumnFlags_WidthStretch); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - CENTER_TEXT("A"); - ImGui::TextUnformatted("A"); - ImGui::TableNextColumn(); - CENTER_TEXT("D"); - ImGui::TextUnformatted("D"); - ImGui::TableNextColumn(); - CENTER_TEXT("S"); - ImGui::TextUnformatted("S"); - ImGui::TableNextColumn(); - CENTER_TEXT("R"); - ImGui::TextUnformatted("R"); - ImGui::TableNextColumn(); - CENTER_TEXT("Envelope"); - ImGui::TextUnformatted("Envelope"); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + CENTER_TEXT("A"); + ImGui::TextUnformatted("A"); + ImGui::TableNextColumn(); + CENTER_TEXT("D"); + ImGui::TextUnformatted("D"); + ImGui::TableNextColumn(); + CENTER_TEXT("S"); + ImGui::TextUnformatted("S"); + ImGui::TableNextColumn(); + CENTER_TEXT("R"); + ImGui::TextUnformatted("R"); + ImGui::TableNextColumn(); + CENTER_TEXT("Envelope"); + ImGui::TextUnformatted("Envelope"); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - P(CWVSliderScalar("##Attack",sliderSize,ImGuiDataType_U8,&ins->snes.a,&_ZERO,&_FIFTEEN)); - ImGui::TableNextColumn(); - P(CWVSliderScalar("##Decay",sliderSize,ImGuiDataType_U8,&ins->snes.d,&_ZERO,&_SEVEN)); - ImGui::TableNextColumn(); - P(CWVSliderScalar("##Sustain",sliderSize,ImGuiDataType_U8,&ins->snes.s,&_ZERO,&_SEVEN)); - ImGui::TableNextColumn(); - P(CWVSliderScalar("##Release",sliderSize,ImGuiDataType_U8,&ins->snes.r,&_ZERO,&_THIRTY_ONE)); - ImGui::TableNextColumn(); - drawFMEnv(0,ins->snes.a+1,1+ins->snes.d*2,ins->snes.r,ins->snes.r,(14-ins->snes.s*2),(ins->snes.r==0),0,0,7,16,ImVec2(ImGui::GetContentRegionAvail().x,sliderSize.y),ins->type); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + P(CWVSliderScalar("##Attack",sliderSize,ImGuiDataType_U8,&ins->snes.a,&_ZERO,&_FIFTEEN)); + ImGui::TableNextColumn(); + P(CWVSliderScalar("##Decay",sliderSize,ImGuiDataType_U8,&ins->snes.d,&_ZERO,&_SEVEN)); + ImGui::TableNextColumn(); + P(CWVSliderScalar("##Sustain",sliderSize,ImGuiDataType_U8,&ins->snes.s,&_ZERO,&_SEVEN)); + ImGui::TableNextColumn(); + P(CWVSliderScalar("##Release",sliderSize,ImGuiDataType_U8,&ins->snes.r,&_ZERO,&_THIRTY_ONE)); + ImGui::TableNextColumn(); + drawFMEnv(0,ins->snes.a+1,1+ins->snes.d*2,ins->snes.r,ins->snes.r,(14-ins->snes.s*2),(ins->snes.r==0),0,0,7,16,ImVec2(ImGui::GetContentRegionAvail().x,sliderSize.y),ins->type); - ImGui::EndTable(); + ImGui::EndTable(); + } + P(ImGui::Checkbox("Apply echo filter",&ins->snes.applyFIR)); + if (ins->snes.applyFIR) { + double inBuf[8]; + fftw_complex outBuf[8]; + float curve[5]; + fftw_plan plan=fftw_plan_dft_r2c_1d(8,inBuf,outBuf,FFTW_ESTIMATE); + + ImGui::Text("Coefficients"); + ImGui::SameLine(); + P(ImGui::DragScalarN("##FIRCoeff",ImGuiDataType_S8,ins->snes.fir,8,1,&_MINUS_ONE_HUNDRED_TWENTY_EIGHT,&_ONE_HUNDRED_TWENTY_SEVEN)); rightClickable + for (int i=0; i<8; i++) { + inBuf[i] = ins->snes.fir[i]; } - } else { - if (ImGui::BeginTable("SNESGainParams",3,ImGuiTableFlags_NoHostExtendX)) { - ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed,sliderSize.x); - ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthStretch); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - CENTER_TEXT("Gain Mode"); - ImGui::TextUnformatted("Gain Mode"); - ImGui::TableNextColumn(); - CENTER_TEXT("Gain"); - ImGui::TextUnformatted("Gain"); - ImGui::TableNextColumn(); - CENTER_TEXT("Envelope"); - ImGui::TextUnformatted("Envelope"); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - if (ImGui::RadioButton("Direct",ins->snes.gainMode==DivInstrumentSNES::GAIN_MODE_DIRECT)) { - ins->snes.gainMode=DivInstrumentSNES::GAIN_MODE_DIRECT; - PARAMETER; - } - if (ImGui::RadioButton("Decrease (linear)",ins->snes.gainMode==DivInstrumentSNES::GAIN_MODE_DEC_LINEAR)) { - ins->snes.gainMode=DivInstrumentSNES::GAIN_MODE_DEC_LINEAR; - PARAMETER; - } - if (ImGui::RadioButton("Decrease (logarithmic)",ins->snes.gainMode==DivInstrumentSNES::GAIN_MODE_DEC_LOG)) { - ins->snes.gainMode=DivInstrumentSNES::GAIN_MODE_DEC_LOG; - PARAMETER; - } - if (ImGui::RadioButton("Increase (linear)",ins->snes.gainMode==DivInstrumentSNES::GAIN_MODE_INC_LINEAR)) { - ins->snes.gainMode=DivInstrumentSNES::GAIN_MODE_INC_LINEAR; - PARAMETER; - } - if (ImGui::RadioButton("Increase (bent line)",ins->snes.gainMode==DivInstrumentSNES::GAIN_MODE_INC_INVLOG)) { - ins->snes.gainMode=DivInstrumentSNES::GAIN_MODE_INC_INVLOG; - PARAMETER; - } - - ImGui::TableNextColumn(); - unsigned char gainMax=(ins->snes.gainMode==DivInstrumentSNES::GAIN_MODE_DIRECT)?127:31; - if (ins->snes.gain>gainMax) ins->snes.gain=gainMax; - P(CWVSliderScalar("##Gain",sliderSize,ImGuiDataType_U8,&ins->snes.gain,&_ZERO,&gainMax)); - - ImGui::TableNextColumn(); - ImGui::Text("Envelope goes here..."); - - ImGui::EndTable(); + fftw_execute(plan); + for (int i=0; i<5; i++) { + curve[i] = sqrtf(powf(outBuf[i][0],2)+powf(outBuf[i][1],2))/128.f; } + ImGui::PlotLines("##FIRResponse",curve,5,0,"Frequency response",0.0,8.0,ImVec2(ImGui::GetContentRegionAvail().x,100.0f*dpiScale)); } ImGui::EndTabItem(); } @@ -4159,6 +4146,14 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_ES5506) { volMax=65535; } + if (ins->type==DIV_INS_SNES) { + if (ins->snes.useEnv) { + volMax=0; + } else { + volumeLabel="Gain Level"; + volMax=127; + } + } const char* dutyLabel="Duty/Noise"; int dutyMin=0; @@ -4203,7 +4198,7 @@ void FurnaceGUI::drawInsEdit() { dutyLabel="Noise"; dutyMax=8; } - if (ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS || ins->type==DIV_INS_VRC6_SAW || ins->type==DIV_INS_FDS || ins->type==DIV_INS_MULTIPCM) { + if (ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS || ins->type==DIV_INS_VRC6_SAW || ins->type==DIV_INS_FDS || ins->type==DIV_INS_MULTIPCM || ins->type==DIV_INS_SNES) { dutyMax=0; } if (ins->type==DIV_INS_VERA) { @@ -4244,7 +4239,7 @@ void FurnaceGUI::drawInsEdit() { waveMax=8; bitMode=true; } - + if (ins->type==DIV_INS_OPLL) { waveLabel="Patch"; } @@ -4284,6 +4279,10 @@ void FurnaceGUI::drawInsEdit() { ex1Max=65535; ex2Max=65535; } + if (ins->type==DIV_INS_SNES && !ins->snes.useEnv) { + ex1Max=4; + ex2Max=31; + } int panMin=0; int panMax=0; @@ -4373,7 +4372,8 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_MULTIPCM || ins->type==DIV_INS_SU || ins->type==DIV_INS_MIKEY || - ins->type==DIV_INS_ES5506) { + ins->type==DIV_INS_ES5506 || + ins->type==DIV_INS_SNES) { macroList.push_back(FurnaceGUIMacroDesc("Phase Reset",&ins->std.phaseResetMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true)); } if (ex1Max>0) { @@ -4391,6 +4391,8 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("Cutoff",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } else if (ins->type==DIV_INS_ES5506) { macroList.push_back(FurnaceGUIMacroDesc("Filter K1",&ins->std.ex1Macro,((ins->std.ex1Macro.mode==1)?(-ex1Max):0),ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER],false,macroRelativeMode)); + } else if (ins->type==DIV_INS_SNES) { + macroList.push_back(FurnaceGUIMacroDesc("Gain Mode",&ins->std.ex1Macro,0,ex1Max,64,uiColors[GUI_COLOR_MACRO_VOLUME],false,NULL,NULL,false,snesGainModes)); } else { macroList.push_back(FurnaceGUIMacroDesc("Duty",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } @@ -4406,6 +4408,8 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("Resonance",&ins->std.ex2Macro,0,ex2Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } else if (ins->type==DIV_INS_ES5506) { macroList.push_back(FurnaceGUIMacroDesc("Filter K2",&ins->std.ex2Macro,((ins->std.ex2Macro.mode==1)?(-ex2Max):0),ex2Max,160,uiColors[GUI_COLOR_MACRO_OTHER],false,macroRelativeMode)); + } else if (ins->type==DIV_INS_SNES) { + macroList.push_back(FurnaceGUIMacroDesc("Gain Rate",&ins->std.ex2Macro,0,ex2Max,160,uiColors[GUI_COLOR_MACRO_VOLUME])); } else { macroList.push_back(FurnaceGUIMacroDesc("Envelope",&ins->std.ex2Macro,0,ex2Max,ex2Bit?64:160,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,ex2Bit,ayEnvBits)); } From 18d793dc2051c09354e9213c2cae3d7c8b81a96d Mon Sep 17 00:00:00 2001 From: Natt Akuma Date: Mon, 19 Sep 2022 01:01:46 +0700 Subject: [PATCH 5/5] SNES: Fix wavesynth and channel 1 --- src/engine/platform/snes.cpp | 11 +++++++---- src/engine/platform/snes.h | 5 +++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index c4239c603..2f3e972f9 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -83,7 +83,6 @@ void DivPlatformSNES::acquire(short* bufL, short* bufR, size_t start, size_t len void DivPlatformSNES::tick(bool sysTick) { // KON/KOFF can't be written several times per one sample // so they have to be accumulated - // TODO due to pipelining, KON/KOFF writes need to be delayed to accomodate sample address changes in the table unsigned char kon=0; unsigned char koff=0; for (int i=0; i<8; i++) { @@ -203,7 +202,10 @@ void DivPlatformSNES::tick(bool sysTick) { } } } - rWrite(0x4c,kon); + if (kon!=0) { + rWrite(0x4c,kon); + } + // always write KOFF as it's constantly polled rWrite(0x5c,koff); } @@ -213,7 +215,7 @@ int DivPlatformSNES::dispatch(DivCommand c) { DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA); if (ins->amiga.useWave) { chan[c.chan].useWave=true; - chan[c.chan].wtLen=(unsigned int)(ins->amiga.waveLen)+1; + chan[c.chan].wtLen=ins->amiga.waveLen+1; if (chan[c.chan].insChanged) { if (chan[c.chan].wave<0) { chan[c.chan].wave=0; @@ -221,6 +223,7 @@ int DivPlatformSNES::dispatch(DivCommand c) { chan[c.chan].ws.changeWave1(chan[c.chan].wave); } } + chan[c.chan].ws.init(ins,chan[c.chan].wtLen,15,chan[c.chan].insChanged); } else { chan[c.chan].sample=ins->amiga.getSample(c.value); chan[c.chan].useWave=false; @@ -404,7 +407,7 @@ void DivPlatformSNES::reset() { dsp.set_output(NULL,0); memset(regPool,0,128); // TODO more initial values - sampleTableBase=0; + sampleTableBase=0x100; // hack: this can't be 0 or channel 1 won't play?? rWrite(0x5d,sampleTableBase>>8); rWrite(0x0c,127); // global volume left rWrite(0x1c,127); // global volume right diff --git a/src/engine/platform/snes.h b/src/engine/platform/snes.h index 058df21fc..65c8a6656 100644 --- a/src/engine/platform/snes.h +++ b/src/engine/platform/snes.h @@ -29,12 +29,13 @@ class DivPlatformSNES: public DivDispatch { struct Channel { int freq, baseFreq, pitch, pitch2; - unsigned int audPos, wtLen; + unsigned int audPos; int sample, wave, ins; int note; int panL, panR; bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, useWave, setPos; signed char vol; + int wtLen; bool useEnv; DivMacroInt std; DivWaveSynth ws; @@ -48,7 +49,6 @@ class DivPlatformSNES: public DivDispatch { pitch(0), pitch2(0), audPos(0), - wtLen(16), sample(-1), wave(-1), ins(-1), @@ -64,6 +64,7 @@ class DivPlatformSNES: public DivDispatch { useWave(false), setPos(false), vol(127), + wtLen(16), useEnv(false) {} }; Channel chan[8];