diff --git a/CMakeLists.txt b/CMakeLists.txt index dab89e086..bbbbeea7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -607,6 +607,8 @@ src/engine/platform/sound/ted-sound.c src/engine/platform/sound/c140_c219.c +src/engine/platform/sound/dave/dave.cpp + src/engine/platform/oplAInterface.cpp src/engine/platform/ym2608Interface.cpp src/engine/platform/ym2610Interface.cpp @@ -707,6 +709,7 @@ src/engine/platform/ted.cpp src/engine/platform/c140.cpp src/engine/platform/esfm.cpp src/engine/platform/powernoise.cpp +src/engine/platform/dave.cpp src/engine/platform/pcmdac.cpp src/engine/platform/dummy.cpp @@ -916,8 +919,19 @@ set(USED_SOURCES ${ENGINE_SOURCES} ${AUDIO_SOURCES} ${CLI_SOURCES} src/main.cpp) if (USE_BACKWARD) list(APPEND USED_SOURCES src/backtrace.cpp) - if (WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - list(APPEND DEPENDENCIES_LIBRARIES dbghelp psapi) + if (WIN32) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-gcodeview GCC_CODEVIEW) + if (GCC_CODEVIEW) + set(CMAKE_EXE_LINKER_FLAGS "-Wl,--pdb= ") + add_compile_options(-gcodeview) + message(STATUS "Enabling -gcodeview flag for backward-cpp.") + else() + message(WARNING "Could not enable -gcodeview! backward-cpp will not work.") + endif() + list(APPEND DEPENDENCIES_LIBRARIES dbghelp psapi) + endif() endif() find_library(EXECINFO_IS_LIBRARY execinfo) if (EXECINFO_IS_LIBRARY) diff --git a/papers/format.md b/papers/format.md index 149c49869..43a4dd78d 100644 --- a/papers/format.md +++ b/papers/format.md @@ -238,6 +238,7 @@ size | description | - 0xd1: ESFM - 18 channels | - 0xd2: Ensoniq ES5503 (hard pan) - 32 channels (UNAVAILABLE) | - 0xd4: PowerNoise - 4 channels + | - 0xd5: Dave - 6 channels (UNAVAILABLE) | - 0xde: YM2610B extended - 19 channels | - 0xe0: QSound - 19 channels | - 0xfc: Pong - 1 channel diff --git a/scripts/release-win32.sh b/scripts/release-win32.sh index 604e17251..388ceb5aa 100755 --- a/scripts/release-win32.sh +++ b/scripts/release-win32.sh @@ -15,7 +15,7 @@ fi cd win32build # TODO: potential Arch-ism? -i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2 -march=i586" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -march=i586" -DBUILD_SHARED_LIBS=OFF -DSUPPORT_XP=OFF -DWITH_RENDER_DX11=ON -DUSE_BACKWARD=OFF -DSDL_SSE2=OFF -DSDL_SSE3=OFF -DENABLE_SSE=OFF -DENABLE_SSE2=OFF -DENABLE_AVX=OFF -DENABLE_AVX2=OFF .. || exit 1 +i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2 -march=i586" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -march=i586" -DBUILD_SHARED_LIBS=OFF -DSUPPORT_XP=OFF -DWITH_RENDER_DX11=ON -DUSE_BACKWARD=ON -DSDL_SSE2=OFF -DSDL_SSE3=OFF -DENABLE_SSE=OFF -DENABLE_SSE2=OFF -DENABLE_AVX=OFF -DENABLE_AVX2=OFF .. || exit 1 make -j8 || exit 1 cd .. diff --git a/scripts/release-win64.sh b/scripts/release-win64.sh index edd4007fc..8ec97a258 100755 --- a/scripts/release-win64.sh +++ b/scripts/release-win64.sh @@ -15,7 +15,7 @@ fi cd winbuild # TODO: potential Arch-ism? -x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Wno-deprecated-declarations -Werror" .. || exit 1 +x86_64-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type -Wno-deprecated-declarations -Werror" -DUSE_BACKWARD=ON .. || exit 1 make -j8 || exit 1 cd .. diff --git a/scripts/release-winxp.sh b/scripts/release-winxp.sh index 71ff675ea..5901f1643 100755 --- a/scripts/release-winxp.sh +++ b/scripts/release-winxp.sh @@ -15,7 +15,7 @@ fi cd xpbuild # TODO: potential Arch-ism? -i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type" -DBUILD_SHARED_LIBS=OFF -DSUPPORT_XP=ON -DWITH_RENDER_DX11=OFF -DSDL_SSE2=OFF -DSDL_SSE3=OFF -DENABLE_SSE=OFF -DENABLE_SSE2=OFF -DENABLE_AVX=OFF -DENABLE_AVX2=OFF .. || exit 1 +i686-w64-mingw32-cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_FLAGS="-O2" -DCMAKE_CXX_FLAGS="-O2 -Wall -Wextra -Wno-unused-parameter -Wno-cast-function-type" -DBUILD_SHARED_LIBS=OFF -DSUPPORT_XP=ON -DWITH_RENDER_DX11=OFF -DSDL_SSE2=OFF -DSDL_SSE3=OFF -DENABLE_SSE=OFF -DENABLE_SSE2=OFF -DENABLE_AVX=OFF -DENABLE_AVX2=OFF -DUSE_BACKWARD=ON .. || exit 1 make -j8 || exit 1 cd .. diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 06049a69f..8118c4a54 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -84,6 +84,7 @@ #include "platform/pcmdac.h" #include "platform/esfm.h" #include "platform/powernoise.h" +#include "platform/dave.h" #include "platform/dummy.h" #include "../ta-log.h" #include "song.h" @@ -652,6 +653,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_POWERNOISE: dispatch=new DivPlatformPowerNoise; break; + case DIV_SYSTEM_DAVE: + dispatch=new DivPlatformDave; + break; case DIV_SYSTEM_DUMMY: dispatch=new DivPlatformDummy; break; diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index 990af8da3..31be38638 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -1062,6 +1062,8 @@ void DivInstrument::putInsData2(SafeWriter* w, bool fui, const DivSong* song, bo case DIV_INS_POWERNOISE_SLOPE: featurePN=true; break; + case DIV_INS_DAVE: + break; case DIV_INS_MAX: break; case DIV_INS_NULL: diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 80bd8f251..14cc1cfec 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -88,6 +88,7 @@ enum DivInstrumentType: unsigned short { DIV_INS_ESFM=55, DIV_INS_POWERNOISE=56, DIV_INS_POWERNOISE_SLOPE=57, + DIV_INS_DAVE=58, DIV_INS_MAX, DIV_INS_NULL }; diff --git a/src/engine/platform/dave.cpp b/src/engine/platform/dave.cpp new file mode 100644 index 000000000..d316ae58f --- /dev/null +++ b/src/engine/platform/dave.cpp @@ -0,0 +1,545 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 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 "dave.h" +#include "../engine.h" +#include "furIcons.h" +#include + +//#define rWrite(a,v) pendingWrites[a]=v; +#define rWrite(a,v) if (!skipRegisterWrites) {writes.push(QueuedWrite(a,v)); if (dumpWrites) {addWrite(a,v);} } + +#define CHIP_DIVIDER 8 + +const char* regCheatSheetDave[]={ + "Freq0", "00", + "Control0", "01", + "Freq1", "02", + "Control1", "03", + "Freq2", "04", + "Control2", "05", + "Control3", "06", + "SoundCtrl", "07", + "Vol0L", "08", + "Vol1L", "09", + "Vol2L", "0A", + "Vol3L", "0B", + "Vol0R", "0C", + "Vol1R", "0D", + "Vol2R", "0E", + "Vol3R", "0F", + "ClockDiv", "1F", + NULL +}; + +const unsigned char snapPeriodLong[15]={ + 0, 1, 3, 3, 3, 6, 6, 7, 7, 10, 10, 12, 12, 13, 13 +}; + +const unsigned char snapPeriodShort[15]={ + 2, 2, 2, 2, 5, 5, 5, 8, 8, 8, 11, 11, 11, 11, 11 +}; + +const unsigned char waveMap[8]={ + 0, 1, 1, 2, 3, 0, 0, 0 +}; + +const char** DivPlatformDave::getRegisterSheet() { + return regCheatSheetDave; +} + +void DivPlatformDave::acquire(short** buf, size_t len) { + for (size_t h=0; hrate) { + DivSample* s=parent->getSample(chan[i].dacSample); + if (s->samples<=0) { + chan[i].dacSample=-1; + writeControl=true; + chan[0].writeVol=true; + continue; + } + signed char dacData=(s->data8[chan[i].dacPos]*chan[i].outVol)>>8; + chan[i].dacOut=dacData+32; + chan[i].dacPos++; + if (!isMuted[i]) { + rWrite(8+((i-4)<<2),chan[i].dacOut&0x3f); + } + if (s->isLoopable() && chan[i].dacPos>=(unsigned int)s->loopEnd) { + chan[i].dacPos=s->loopStart; + } else if (chan[i].dacPos>=s->samples) { + chan[i].dacSample=-1; + writeControl=true; + chan[0].writeVol=true; + } + chan[i].dacPeriod-=rate; + } + } + } + + if (!writes.empty()) { + QueuedWrite w=writes.front(); + dave->writePort(w.addr,w.val); + regPool[w.addr&0x1f]=w.val; + writes.pop(); + } + + unsigned int next=dave->runOneCycle(); + unsigned short nextL=next&0xffff; + unsigned short nextR=next>>16; + + buf[0][h]=(short)nextL; + buf[1][h]=(short)nextR; + } +} + +void DivPlatformDave::tick(bool sysTick) { + for (int i=0; i<6; i++) { + chan[i].std.next(); + if (chan[i].std.vol.had) { + chan[i].outVol=VOL_SCALE_LINEAR(chan[i].vol&63,MIN(63,chan[i].std.vol.val),63); + chan[i].writeVol=true; + } + if (chan[i].std.duty.had) { + chan[i].noiseFreq=chan[i].std.duty.val&3; + chan[i].freqChanged=true; + } + if (NEW_ARP_STRAT) { + chan[i].handleArp(); + } else if (chan[i].std.arp.had) { + if (!chan[i].inPorta) { + if (i>=4) { + chan[i].baseFreq=parent->calcBaseFreq(1,1,parent->calcArp(chan[i].note,chan[i].std.arp.val),false); + } else { + chan[i].baseFreq=NOTE_PERIODIC(parent->calcArp(chan[i].note,chan[i].std.arp.val)); + } + } + chan[i].freqChanged=true; + } + if (chan[i].std.wave.had) { + chan[i].wave=chan[i].std.wave.val&7; + if (i==3 && chan[i].wave>3) chan[i].wave=3; + chan[i].freqChanged=true; + } + if (chan[i].std.panL.had) { + chan[i].panL=chan[i].std.panL.val&63; + } + if (chan[i].std.panR.had) { + chan[i].panR=chan[i].std.panR.val&63; + } + if (chan[i].std.panL.had || chan[i].std.panR.had) { + chan[i].writeVol=true; + } + if (chan[i].std.ex1.had) { + chan[i].highPass=chan[i].std.ex1.val&1; + chan[i].ringMod=chan[i].std.ex1.val&2; + chan[i].swapCounters=chan[i].std.ex1.val&4; + chan[i].lowPass=chan[i].std.ex1.val&8; + chan[i].freqChanged=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.phaseReset.had && chan[i].std.phaseReset.val==1) { + if (i>=4) { + if (chan[i].active && chan[i].dacSample>=0 && chan[i].dacSamplesong.sampleLen) { + chan[i].dacPos=0; + chan[i].dacPeriod=0; + chan[i].keyOn=true; + } + } else { + chan[i].resetPhase=true; + writeControl=true; + } + } + + if (chan[i].writeVol) { + if (i<4) { + if (chan[i].active && !isMuted[i]) { + if (i!=0 || chan[4].dacSample<0) { + rWrite(8+i,(63+chan[i].outVol*chan[i].panL)>>6); + } + if (i!=0 || chan[5].dacSample<0) { + rWrite(12+i,(63+chan[i].outVol*chan[i].panR)>>6); + } + } else { + if (i!=0 || chan[4].dacSample<0) { + rWrite(8+i,0); + } + if (i!=0 || chan[5].dacSample<0) { + rWrite(12+i,0); + } + } + } + chan[i].writeVol=false; + } + + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + if (i>=4) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,false,0,chan[i].pitch2,1,1); + } else { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,true,0,chan[i].pitch2,chipClock,CHIP_DIVIDER); + } + + if (i<3) { + switch (chan[i].wave) { + case 0: + chan[i].freq>>=2; + break; + case 1: + chan[i].freq/=5; + chan[i].freq>>=1; + break; + case 2: + chan[i].freq/=15; + chan[i].freq>>=1; + break; + case 3: + chan[i].freq/=63; + break; + case 4: + chan[i].freq>>=5; + break; + } + } + + if (i<4) { + if (chan[i].freq<1) chan[i].freq=1; + if (chan[i].freq>4095) chan[i].freq=4095; + } + + if (i<3) { + if (chan[i].wave==1) { // short 1 + chan[i].freq=15*(chan[i].freq/15)+snapPeriodShort[(chan[i].freq%15)]; + } else if (chan[i].wave==2) { // long 1 + chan[i].freq=15*(chan[i].freq/15)+snapPeriodLong[(chan[i].freq%15)]; + } else if (chan[i].wave==3) { // long 2 (30, 61, 92, 123... result in silence) + if ((chan[i].freq%30)==(chan[i].freq/30)-1) chan[i].freq++; + } + rWrite((i<<1),chan[i].freq&0xff); + rWrite(1+(i<<1),(chan[i].freq>>8)|((waveMap[chan[i].wave])<<4)|(chan[i].highPass?0x40:0)|(chan[i].ringMod?0x80:0)); + } else if (i==3) { + rWrite(6,(chan[i].noiseFreq&3)|((chan[i].wave&3)<<2)|(chan[i].swapCounters?0x10:0)|(chan[i].lowPass?0x20:0)|(chan[i].highPass?0x40:0)|(chan[i].ringMod?0x80:0)); + } + + if (chan[i].keyOn) chan[i].keyOn=false; + if (chan[i].keyOff) chan[i].keyOff=false; + chan[i].freqChanged=false; + } + } + + if (writeControl) { + rWrite(7,(chan[0].resetPhase?1:0)|(chan[1].resetPhase?2:0)|(chan[2].resetPhase?4:0)|((chan[4].dacSample>=0)?8:0)|((chan[5].dacSample>=0)?16:0)); + rWrite(7,((chan[4].dacSample>=0)?8:0)|((chan[5].dacSample>=0)?16:0)); + chan[0].resetPhase=false; + chan[1].resetPhase=false; + chan[2].resetPhase=false; + chan[3].resetPhase=false; + writeControl=false; + } +} + +int DivPlatformDave::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=NULL; + // DAC + if (c.chan>=4) { + ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA); + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].dacSample=ins->amiga.getSample(c.value); + chan[c.chan].sampleNote=c.value; + c.value=ins->amiga.getFreq(c.value); + chan[c.chan].sampleNoteDelta=c.value-chan[c.chan].sampleNote; + } else if (chan[c.chan].sampleNote!=DIV_NOTE_NULL) { + chan[c.chan].dacSample=ins->amiga.getSample(chan[c.chan].sampleNote); + c.value=ins->amiga.getFreq(chan[c.chan].sampleNote); + } + if (chan[c.chan].dacSample<0 || chan[c.chan].dacSample>=parent->song.sampleLen) { + chan[c.chan].dacSample=-1; + chan[0].writeVol=true; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=parent->calcBaseFreq(1,1,c.value,false); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].dacPos=0; + chan[c.chan].dacPeriod=0; + writeControl=true; + } else { + ins=parent->getIns(chan[c.chan].ins,DIV_INS_DAVE); + chan[c.chan].sampleNote=DIV_NOTE_NULL; + chan[c.chan].sampleNoteDelta=0; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].writeVol=true; + chan[c.chan].macroInit(ins); + if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + chan[c.chan].insChanged=false; + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].writeVol=true; + if (c.chan>=4) { + chan[c.chan].dacSample=-1; + chan[0].writeVol=true; + writeControl=true; + } + chan[c.chan].macroInit(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; + 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.vol.has) { + chan[c.chan].outVol=c.value; + if (chan[c.chan].active) { + chan[c.chan].writeVol=true; + } + } + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + 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].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2+chan[c.chan].sampleNoteDelta); + 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_PANNING: { + chan[c.chan].panL=c.value>>2; + chan[c.chan].panR=c.value2>>2; + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+chan[c.chan].sampleNoteDelta); + 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].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_DAVE)); + } + if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) chan[c.chan].baseFreq=NOTE_PERIODIC(chan[c.chan].note); + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 31; + break; + case DIV_CMD_MACRO_OFF: + chan[c.chan].std.mask(c.value,true); + break; + case DIV_CMD_MACRO_ON: + chan[c.chan].std.mask(c.value,false); + break; + case DIV_CMD_MACRO_RESTART: + chan[c.chan].std.restart(c.value); + break; + default: + break; + } + return 1; +} + +void DivPlatformDave::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + chan[ch].writeVol=true; +} + +void DivPlatformDave::forceIns() { + for (int i=0; i<6; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + chan[i].writeVol=true; + } + writeControl=true; +} + +void* DivPlatformDave::getChanState(int ch) { + return &chan[ch]; +} + +DivMacroInt* DivPlatformDave::getChanMacroInt(int ch) { + return &chan[ch].std; +} + +unsigned short DivPlatformDave::getPan(int ch) { + return (chan[ch].panL<<2)|chan[ch].panR; +} + +DivChannelPair DivPlatformDave::getPaired(int ch) { + return DivChannelPair(); +} + +DivChannelModeHints DivPlatformDave::getModeHints(int ch) { + DivChannelModeHints ret; + return ret; +} + +DivSamplePos DivPlatformDave::getSamplePos(int ch) { + if (ch<4 || ch>=6) return DivSamplePos(); + return DivSamplePos( + chan[ch].dacSample, + chan[ch].dacPos, + chan[ch].dacRate + ); +} + +DivDispatchOscBuffer* DivPlatformDave::getOscBuffer(int ch) { + return oscBuf[ch]; +} + +int DivPlatformDave::mapVelocity(int ch, float vel) { + return round(31.0*pow(vel,0.22)); +} + +unsigned char* DivPlatformDave::getRegisterPool() { + return regPool; +} + +int DivPlatformDave::getRegisterPoolSize() { + return 32; +} + +void DivPlatformDave::reset() { + writes.clear(); + memset(regPool,0,32); + for (int i=0; i<6; i++) { + chan[i]=DivPlatformDave::Channel(); + chan[i].std.setEngine(parent); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } + writeControl=false; + dave->reset(true); +} + +int DivPlatformDave::getOutputCount() { + return 2; +} + +bool DivPlatformDave::keyOffAffectsArp(int ch) { + return true; +} + +void DivPlatformDave::notifyInsDeletion(void* ins) { + for (int i=0; i<6; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformDave::setFlags(const DivConfig& flags) { + chipClock=8000000.0; + CHECK_CUSTOM_CLOCK; + rate=chipClock/16; + for (int i=0; i<6; i++) { + oscBuf[i]->rate=rate; + } +} + +void DivPlatformDave::poke(unsigned int addr, unsigned short val) { + rWrite(addr,val); +} + +void DivPlatformDave::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +int DivPlatformDave::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + dave=new Ep128::Dave; + for (int i=0; i<6; i++) { + isMuted[i]=false; + oscBuf[i]=new DivDispatchOscBuffer; + } + setFlags(flags); + reset(); + return 6; +} + +void DivPlatformDave::quit() { + for (int i=0; i<6; i++) { + delete oscBuf[i]; + } + delete dave; +} + +DivPlatformDave::~DivPlatformDave() { +} diff --git a/src/engine/platform/dave.h b/src/engine/platform/dave.h new file mode 100644 index 000000000..9fea7a3b1 --- /dev/null +++ b/src/engine/platform/dave.h @@ -0,0 +1,100 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 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 _DAVE_H +#define _DAVE_H + +#include "../dispatch.h" +#include "../../fixedQueue.h" +#include "sound/dave/dave.hpp" + +class DivPlatformDave: public DivDispatch { + struct Channel: public SharedChannel { + int dacPeriod, dacRate, dacOut; + unsigned int dacPos; + int dacSample; + unsigned char noiseFreq; + unsigned char panL; + unsigned char panR; + unsigned char wave; + bool writeVol, highPass, ringMod, swapCounters, lowPass, resetPhase; + Channel(): + SharedChannel(63), + dacPeriod(0), + dacRate(0), + dacOut(0), + dacPos(0), + dacSample(-1), + noiseFreq(0), + panL(63), + panR(63), + wave(0), + writeVol(false), + highPass(false), + ringMod(false), + swapCounters(false), + lowPass(false), + resetPhase(false) {} + }; + Channel chan[6]; + DivDispatchOscBuffer* oscBuf[6]; + bool isMuted[6]; + struct QueuedWrite { + unsigned char addr; + unsigned char val; + QueuedWrite(): addr(0), val(9) {} + QueuedWrite(unsigned char a, unsigned char v): addr(a), val(v) {} + }; + FixedQueue writes; + bool writeControl; + + Ep128::Dave* dave; + unsigned char regPool[32]; + friend void putDispatchChip(void*,int); + friend void putDispatchChan(void*,int,int); + public: + void acquire(short** buf, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + DivMacroInt* getChanMacroInt(int ch); + unsigned short getPan(int chan); + DivChannelPair getPaired(int chan); + DivChannelModeHints getModeHints(int chan); + DivSamplePos getSamplePos(int ch); + DivDispatchOscBuffer* getOscBuffer(int chan); + int mapVelocity(int ch, float vel); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + int getOutputCount(); + bool keyOffAffectsArp(int ch); + void setFlags(const DivConfig& flags); + 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, const DivConfig& flags); + void quit(); + ~DivPlatformDave(); +}; + +#endif diff --git a/src/engine/platform/sound/dave/dave.cpp b/src/engine/platform/sound/dave/dave.cpp new file mode 100644 index 000000000..59d2f5f20 --- /dev/null +++ b/src/engine/platform/sound/dave/dave.cpp @@ -0,0 +1,813 @@ + +// ep128emu -- portable Enterprise 128 emulator +// Copyright (C) 2003-2016 Istvan Varga +// https://github.com/istvan-v/ep128emu/ +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +#include "dave.hpp" +#include + +#define EP128EMU_UNLIKELY(x) x + +// Generate polynomial counter of log2(m0) + 1 bits length, and store +// (m0 * 2 - 1) samples at 'tabptr' in reverse order. +// log2(m1) is the second bit to be used in the XOR operation when +// calculating the next bit of output. + +static void calculate_polycnt(uint8_t *tabptr, uint32_t m0, uint32_t m1) +{ + uint32_t sr = 0xFFFFFFFFU; + int n = int(m0 << 1) - 1; + while (--n >= 0) { + uint8_t b0 = uint8_t(bool(sr & m0)); + uint8_t b1 = uint8_t(bool(sr & m1)); + tabptr[n] = b0; + sr = (sr << 1) | uint32_t(b0 ^ b1); + } +} + +namespace Ep128 { + DaveTables::DaveTables() + { + static const uint32_t polycnt_params[14] = { + 0x00000008U, 0x00000004U, // 4-bit: poly = 11001 + 0x00000010U, 0x00000004U, // 5-bit: poly = 101001 + 0x00000040U, 0x00000020U, // 7-bit: poly = 11000001 + 0x00000100U, 0x00000010U, // 9-bit: poly = 1000100001 + 0x00000400U, 0x00000100U, // 11-bit: poly = 101000000001 + 0x00004000U, 0x00002000U, // 15-bit: poly = 1100000000000001 + 0x00010000U, 0x00002000U // 17-bit: poly = 100100000000000001 + }; + polycnt4_table = new uint8_t[15 + 31 + 127 + 511 + 2047 + 32767 + 131071]; + polycnt5_table = &(polycnt4_table[15]); + polycnt7_table = &(polycnt5_table[31]); + polycnt9_table = &(polycnt7_table[127]); + polycnt11_table = &(polycnt9_table[511]); + polycnt15_table = &(polycnt11_table[2047]); + polycnt17_table = &(polycnt15_table[32767]); + uint8_t *bufp = polycnt4_table; + for (int i = 0; i < 14; i += 2) { + calculate_polycnt(bufp, polycnt_params[i], polycnt_params[i + 1]); + bufp = bufp + ((polycnt_params[i] << 1) - 1U); + } + } + + DaveTables::~DaveTables() + { + delete[] polycnt4_table; + } + + // handle timer interrupts + + inline void Dave::triggerIntSnd() + { + // trigger interrupt on edge only + if (int_snd_active) + return; + // mark as active + int_snd_active = 1; + interruptRequest(); + } + + inline void Dave::triggerInt1Hz() + { + // trigger interrupt on edge only + if (int_1hz_active) + return; + // mark as active + int_1hz_active = 1; + interruptRequest(); + } + + // run DAVE emulation, and also trigger any sound or timer interrupts + + uint32_t Dave::runOneCycle_() + { + // update polynomial counters + if (--polycnt4_phase < 0) // 4-bit + polycnt4_phase = 14; + polycnt4_state = (int) t.polycnt4_table[polycnt4_phase]; + if (--polycnt5_phase < 0) // 5-bit + polycnt5_phase = 30; + polycnt5_state = (int) t.polycnt5_table[polycnt5_phase]; + if (!noise_polycnt_is_7bit) { + // channel 3 uses the variable length polynomial counter + if (--polycnt7_phase < 0) // 7-bit + polycnt7_phase = 126; + polycnt7_state = (int) t.polycnt7_table[polycnt7_phase]; + // channel 3 polynomial counter: updated on negative edge + if (*chn3_clk_source < chn3_clk_source_prv) { + if (--polycntVL_phase < 0) // variable length + polycntVL_phase = polycntVL_maxphase; + polycntVL_state = (int) polycntVL_table[polycntVL_phase]; + chn3_state1 = polycntVL_state; // input signal to channel 3 + if (!chn3_lp_2) + chn3_state2 = polycntVL_state; + } + chn3_clk_source_prv = *chn3_clk_source; + } + else { + // channel 3 uses the 7-bit polynomial counter + if (*chn3_clk_source < chn3_clk_source_prv) { + // update on negative edge + if (--polycnt7_phase < 0) // 7-bit + polycnt7_phase = 126; + polycnt7_state = (int) t.polycnt7_table[polycnt7_phase]; + chn3_state1 = polycnt7_state; // input signal to channel 3 + if (!chn3_lp_2) + chn3_state2 = polycnt7_state; + } + chn3_clk_source_prv = *chn3_clk_source; + if (--polycntVL_phase < 0) // variable length + polycntVL_phase = polycntVL_maxphase; + polycntVL_state = (int) polycntVL_table[polycntVL_phase]; + } + + // update the phase of all oscillators + clk_62500_phase--; + clk_1000_phase--; + chn0_phase -= chn0_run; + chn1_phase -= chn1_run; + chn2_phase -= chn2_run; + + // reload phase counters if necessary + if (EP128EMU_UNLIKELY(clk_1000_phase < 0)) { + clk_50_phase--; + // trigger interrupts if enabled + if ((*int_snd_phase) < 0) { + // will reload counter later + int_snd_state = (int_snd_state & 1) ^ 1; // invert state + if (enable_int_snd) + triggerIntSnd(); + } + clk_1000_phase = clk_1000_frq; + if (EP128EMU_UNLIKELY(clk_50_phase < 0)) { + clk_50_phase = clk_50_frq; + clk_1_phase--; + if (EP128EMU_UNLIKELY(clk_1_phase < 0)) { + clk_1_phase = clk_1_frq; // reload counter + int_1hz_state = (int_1hz_state & 1) ^ 1; // invert state + if (enable_int_1hz) + triggerInt1Hz(); + } + } + } + else if (EP128EMU_UNLIKELY((*int_snd_phase) < 0)) { + // trigger interrupt if enabled + int_snd_state = (int_snd_state & 1) ^ 1; // invert state + if (enable_int_snd) + triggerIntSnd(); + } + + // calculate oscillator outputs + if (clk_62500_phase < 0) { + // simple 31250 Hz oscillator + clk_62500_phase = clk_62500_frq; // reload counter + clk_62500_state = (clk_62500_state & 1) ^ 1; // invert state + } + // ---- channel 3 ---- + chn3_prv = chn3_state; // save previous output + if (chn3_lp_2 && (chn2_state < chn2_prv)) { + // lowpass filter holds signal until negative edge in channel 2 + chn3_state2 = chn3_state1; + } + if (chn3_hp_0 && (chn0_state < chn0_prv)) { + // highpass filter: sets level to 0 on negative edge in channel 0 + chn3_state2 = 0; + } + // store final output signal in chn3_state + chn3_state = chn3_state2; + if (chn3_rm_1) { + // ring modulation: XNOR by channel 1 + chn3_state ^= (chn1_state ^ 1); + } + // ---- channel 2 ---- + chn2_prv = chn2_state; // save previous output + if (chn2_phase < 0) { + chn2_phase = chn2_frqcode; // reload counter + if (chn2_input_polycnt == NULL) { + // square wave + chn2_state1 = (chn2_state1 & 1) ^ 1; + } + else { + // get input from polynomial counter + chn2_state1 = *chn2_input_polycnt; + } + } + if (chn2_hp_3 && (chn3_state < chn3_prv)) { + // highpass filter: sets level to 0 on negative edge in channel 3 + chn2_state1 = 0; + } + // store final output signal in chn2_state + chn2_state = chn2_state1; + if (chn2_rm_0) { + // ring modulation: XNOR by channel 0 + chn2_state ^= (chn0_state ^ 1); + } + // ---- channel 1 ---- + chn1_prv = chn1_state; // save previous output + if (chn1_phase < 0) { + chn1_phase = chn1_frqcode; // reload counter + if (chn1_input_polycnt == NULL) { + // square wave + chn1_state1 = (chn1_state1 & 1) ^ 1; + } + else { + // get input from polynomial counter + chn1_state1 = *chn1_input_polycnt; + } + } + if (chn1_hp_2 && (chn2_state < chn2_prv)) { + // highpass filter: sets level to 0 on negative edge in channel 2 + chn1_state1 = 0; + } + // store final output signal in chn1_state + chn1_state = chn1_state1; + if (chn1_rm_3) { + // ring modulation: XNOR by channel 3 + chn1_state ^= (chn3_state ^ 1); + } + // ---- channel 0 ---- + chn0_prv = chn0_state; // save previous output + if (chn0_phase < 0) { + chn0_phase = chn0_frqcode; // reload counter + if (chn0_input_polycnt == NULL) { + // square wave + chn0_state1 = (chn0_state1 & 1) ^ 1; + } + else { + // get input from polynomial counter + chn0_state1 = *chn0_input_polycnt; + } + } + if (chn0_hp_1 && (chn1_state < chn1_prv)) { + // highpass filter: sets level to 0 on negative edge in channel 1 + chn0_state1 = 0; + } + // store final output signal in chn0_state + chn0_state = chn0_state1; + if (chn0_rm_2) { + // ring modulation: XNOR by channel 2 + chn0_state ^= (chn2_state ^ 1); + } + + // and now the final DAC output (left/right) values + // total output range (not including tape feedback): 0 to 252 + unsigned int lval = + ((tape_feedback & tape_input) == 0 ? 0U : 0x3FU); // tape feedback + unsigned int rval = lval; + if (dac_mode_left) { + lval += (chn0_left << 2); + if (dac_mode_right) { + // simplest case: both channels in DAC mode + rval += (chn0_right << 2); + } + else { + // left channel is in DAC mode, but right is not + if (chn0_state) + rval += chn0_right; + if (chn1_state) + rval += chn1_right; + if (chn2_state) + rval += chn2_right; + if (chn3_state) + rval += chn3_right; + } + } + else if (dac_mode_right) { + // right channel is in DAC mode, but left is not + rval += (chn0_right << 2); + if (chn0_state) + lval += chn0_left; + if (chn1_state) + lval += chn1_left; + if (chn2_state) + lval += chn2_left; + if (chn3_state) + lval += chn3_left; + } + else { + // neither channel is in DAC mode + if (chn0_state) { + lval += chn0_left; + rval += chn0_right; + } + if (chn1_state) { + lval += chn1_left; + rval += chn1_right; + } + if (chn2_state) { + lval += chn2_left; + rval += chn2_right; + } + if (chn3_state) { + lval += chn3_left; + rval += chn3_right; + } + } + + audioOutput = uint32_t(lval + (rval << 16)) << 7; + return audioOutput; + } + + // returns pointer to the polynomial counter for channels 0, 1, and 2 + // selected by 'n' (allowed values for 'n' are 0x00, 0x10, 0x20, and 0x30) + + int * Dave::findPolycntForToneChannel(int n) + { + switch (n) { + case 0x10: + return (&polycnt4_state); // 4-bit + case 0x20: + return (&polycnt5_state); // 5-bit + case 0x30: + if (!noise_polycnt_is_7bit) { + return (&polycnt7_state); // 7-bit + } + else { + return (&polycntVL_state); // variable length + } + } + // default to square wave + return (int*) NULL; + } + + // write to DAVE registers + + void Dave::writePort(uint16_t addr, uint8_t value) + { + switch (uint8_t(addr & 0x1F)) { + case 0x00: + // channel 0 frequency + chn0_frqcode = (chn0_frqcode & 0x0F00) | (int) value; + break; + case 0x01: + // channel 0 frequency and mode + chn0_frqcode = (chn0_frqcode & 0x00FF) | (((int) value & 0x0F) << 8); + // select distortion mode + chn0_input_polycnt = findPolycntForToneChannel((int) value & 0x30); + chn0_hp_1 = ((int) value & 0x40 ? 1 : 0); // highpass + chn0_rm_2 = ((int) value & 0x80 ? 1 : 0); // ringmod + break; + case 0x02: + // channel 1 frequency + chn1_frqcode = (chn1_frqcode & 0x0F00) | (int) value; + break; + case 0x03: + // channel 1 frequency and mode + chn1_frqcode = (chn1_frqcode & 0x00FF) | (((int) value & 0x0F) << 8); + // select distortion mode + chn1_input_polycnt = findPolycntForToneChannel((int) value & 0x30); + chn1_hp_2 = ((int) value & 0x40 ? 1 : 0); // highpass + chn1_rm_3 = ((int) value & 0x80 ? 1 : 0); // ringmod + break; + case 0x04: + // channel 2 frequency + chn2_frqcode = (chn2_frqcode & 0x0F00) | (int) value; + break; + case 0x05: + // channel 2 frequency and mode + chn2_frqcode = (chn2_frqcode & 0x00FF) | (((int) value & 0x0F) << 8); + // select distortion mode + chn2_input_polycnt = findPolycntForToneChannel((int) value & 0x30); + chn2_hp_3 = ((int) value & 0x40 ? 1 : 0); // highpass + chn2_rm_0 = ((int) value & 0x80 ? 1 : 0); // ringmod + break; + case 0x06: + // channel 3 parameters + switch ((int) value & 0x03) { + // polynomial counter clock source + case 0x00: chn3_clk_source = &clk_62500_state; break; + case 0x01: chn3_clk_source = &chn0_state; break; + case 0x02: chn3_clk_source = &chn1_state; break; + case 0x03: chn3_clk_source = &chn2_state; break; + } + // select variable length polynomial counter + switch ((int) value & 0x0C) { + case 0x00: + polycntVL_table = t.polycnt17_table; // 17-bit + polycntVL_maxphase = 131070; + break; + case 0x04: + polycntVL_table = t.polycnt15_table; // 15-bit + polycntVL_maxphase = 32766; + break; + case 0x08: + polycntVL_table = t.polycnt11_table; // 11-bit + polycntVL_maxphase = 2046; + break; + case 0x0C: + polycntVL_table = t.polycnt9_table; // 9-bit + polycntVL_maxphase = 510; + break; + } + // wrap the phase of variable length polynomial counter to table length + polycntVL_phase = polycntVL_phase % (polycntVL_maxphase + 1); + // bit 4: swap 7-bit and variable length polynomial counters if set + if ((int) value & 0x10) { + noise_polycnt_is_7bit = 1; + chn3_input_polycnt = &polycnt7_state; + if (chn0_input_polycnt == &polycnt7_state) + chn0_input_polycnt = &polycntVL_state; + if (chn1_input_polycnt == &polycnt7_state) + chn1_input_polycnt = &polycntVL_state; + if (chn2_input_polycnt == &polycnt7_state) + chn2_input_polycnt = &polycntVL_state; + } + else { + noise_polycnt_is_7bit = 0; + chn3_input_polycnt = &polycntVL_state; + if (chn0_input_polycnt == &polycntVL_state) + chn0_input_polycnt = &polycnt7_state; + if (chn1_input_polycnt == &polycntVL_state) + chn1_input_polycnt = &polycnt7_state; + if (chn2_input_polycnt == &polycntVL_state) + chn2_input_polycnt = &polycnt7_state; + } + chn3_lp_2 = ((int) value & 0x20 ? 1 : 0); // lowpass with channel 2 + chn3_hp_0 = ((int) value & 0x40 ? 1 : 0); // highpass with channel 0 + chn3_rm_1 = ((int) value & 0x80 ? 1 : 0); // ring mod. with channel 1 + break; + case 0x07: + // sound/interrupt control register + if ((int) value & 0x01) { + chn0_run = 0; // channel 0 sync + chn0_state1 = 0; + } + else if (!chn0_run) { + chn0_phase = chn0_frqcode; // reset phase + chn0_run = 1; + } + if ((int) value & 0x02) { + chn1_run = 0; // channel 1 sync + chn1_state1 = 0; + } + else if (!chn1_run) { + chn1_phase = chn1_frqcode; // reset phase + chn1_run = 1; + } + if ((int) value & 0x04) { + chn2_run = 0; // channel 2 sync + chn2_state1 = 0; + } + else if (!chn2_run) { + chn2_phase = chn2_frqcode; // reset phase + chn2_run = 1; + } + dac_mode_left = ((int) value & 0x08 ? 1 : 0); // analogue mode + dac_mode_right = ((int) value & 0x10 ? 1 : 0); + switch ((int) value & 0x60) { + // sound interrupt mode + case 0x00: + int_snd_phase = &clk_1000_phase; + break; + case 0x20: + int_snd_phase = &clk_50_phase; + break; + case 0x40: + int_snd_phase = &chn0_phase; + break; + case 0x60: + int_snd_phase = &chn1_phase; + break; + } + break; + case 0x08: + // channel 0 left volume + chn0_left = int(value & 0x3F); + break; + case 0x09: + // channel 1 left volume + chn1_left = int(value & 0x3F); + break; + case 0x0A: + // channel 2 left volume + chn2_left = int(value & 0x3F); + break; + case 0x0B: + // channel 3 left volume + chn3_left = int(value & 0x3F); + break; + case 0x0C: + // channel 0 right volume + chn0_right = int(value & 0x3F); + break; + case 0x0D: + // channel 1 right volume + chn1_right = int(value & 0x3F); + break; + case 0x0E: + // channel 2 right volume + chn2_right = int(value & 0x3F); + break; + case 0x0F: + // channel 3 right volume + chn3_right = int(value & 0x3F); + break; + case 0x10: + // memory page 0 + page0Segment = value; + setMemoryPage(0, value); + break; + case 0x11: + // memory page 1 + page1Segment = value; + setMemoryPage(1, value); + break; + case 0x12: + // memory page 2 + page2Segment = value; + setMemoryPage(2, value); + break; + case 0x13: + // memory page 3 + page3Segment = value; + setMemoryPage(3, value); + break; + case 0x14: + // interrupt control register + { + int prv = (int_snd_active | int_1hz_active + | int_1_active | int_2_active); + uint8_t tmp = (uint8_t) value ^ (uint8_t) 0x55; + // sound/timer interrupt + enable_int_snd = (tmp & (uint8_t) 0x01 ? 0 : 1); + if (tmp & (uint8_t) 0x03) + int_snd_active = 0; + // 1 Hz interrupt + enable_int_1hz = (tmp & (uint8_t) 0x04 ? 0 : 1); + if (tmp & (uint8_t) 0x0C) + int_1hz_active = 0; + // INT 1 (video interrupt) + enable_int_1 = (tmp & (uint8_t) 0x10 ? 0 : 1); + if (tmp & (uint8_t) 0x30) + int_1_active = 0; + // INT 2 + enable_int_2 = (tmp & (uint8_t) 0x40 ? 0 : 1); + if (tmp & (uint8_t) 0xC0) + int_2_active = 0; + if (prv && !(int_snd_active | int_1hz_active + | int_1_active | int_2_active)) { + // no more active interrupts: clear request to CPU + clearInterruptRequest(); + } + } + break; + case 0x15: + // select keyboard row + keyboardRow = int(value & 0x0F); + tape_feedback = int(!(value & 0x20)); // tape control + setRemote1State(value & 0x40 ? 1 : 0); + setRemote2State(value & 0x80 ? 1 : 0); + break; + case 0x1F: // system configuration register + { + // CPU wait cycle control + setMemoryWaitMode(int(value & 0x0C) >> 2); + // input clock frequency (note: the frequency is always 8 MHz, + // this bit sets the assumed value) + // 0: 8 MHz, dave_clock_freq = input_freq / 32 + // 1: 12 MHz, dave_clock_freq = input_freq / 48 + clockDiv = ((value & 0x02) == 0 ? 2 : 3); + } + break; + } + } + + // read from DAVE registers + + uint8_t Dave::readPort(uint16_t addr) const + { + switch (uint8_t(addr & 0x1F)) { + case 0x10: + return page0Segment; + case 0x11: + return page1Segment; + case 0x12: + return page2Segment; + case 0x13: + return page3Segment; + case 0x14: + { + // interrupt state + return uint8_t((int_snd_state | (int_snd_active << 1)) + | ((int_1hz_state | (int_1hz_active << 1)) << 2) + | ((int_1_state | (int_1_active << 1)) << 4) + | ((int_2_state | (int_2_active << 1)) << 6)); + } + break; + case 0x15: + // read currently selected keyboard row + return (keyboardRow < 10 ? keyboardState[keyboardRow] : 0xFF); + case 0x16: + { + // tape input + uint8_t n = + uint8_t(((tape_input_level - 1) & 0x40) | ((tape_input - 1) & 0x80) + | 0x0F); + if (keyboardRow < 5) { + if (keyboardRow == 0) { + // EnterMice buttons (left and right) + n &= uint8_t(0xF9 | (mouseInput >> 3)); + // EXT1 joystick fire buttons + n &= uint8_t(0xF8 | (keyboardState[14] >> 4)); + } + else { + if (!(mouseInput & 0x80)) // EnterMice data input on column K + n &= uint8_t(0xFD | ((mouseInput << 1) >> (keyboardRow - 1))); + // EXT1 joystick (mapped to row 14) + n &= uint8_t(0xFE | (keyboardState[14] >> (4 - keyboardRow))); + } + } + else if (keyboardRow < 10) { + // external joystick 2 (mapped to keyboard row 15) + if (keyboardRow == 5) // fire buttons + n &= uint8_t(0xF8 | (keyboardState[15] >> 4)); + else + n &= uint8_t(0xFE | (keyboardState[15] >> (9 - keyboardRow))); + } + return n; + } + } + // anything else is either handled elsewhere, or is write-only + return 0xFF; + } + + // set hardware interrupt 1 state, and (possibly) trigger interrupt + + void Dave::setInt1State(int new_state) + { + int prv = int_1_state; + // set new state + int_1_state = (new_state ? 1 : 0); + // on negative edge, trigger CPU interrupt + // (assuming it is enabled, and not active already) + if (!enable_int_1) + return; // disabled + if (int_1_state || !prv) + return; // not on negative edge + if (int_1_active) + return; // already active + // now active + int_1_active = 1; + // send request to CPU + interruptRequest(); + } + + // set hardware interrupt 2 state, and (possibly) trigger interrupt + + void Dave::setInt2State(int new_state) + { + int prv = int_2_state; + // set new state + int_2_state = (new_state ? 1 : 0); + // on negative edge, trigger CPU interrupt + // (assuming it is enabled, and not active already) + if (!enable_int_2) + return; // disabled + if (int_2_state || !prv) + return; // not on negative edge + if (int_2_active) + return; // already active + // now active + int_2_active = 1; + // send request to CPU + interruptRequest(); + } + + Dave::Dave() + { + clockDiv = 2; + clockCnt = 1; + polycnt4_state = 0; + polycnt5_state = 0; + polycnt7_state = 0; + polycntVL_state = 0; + clk_62500_state = 0; + chn0_state = 0; + chn0_prv = 0; + chn0_state1 = 0; + chn0_frqcode = 0; + chn0_run = 1; + chn1_state = 0; + chn1_prv = 0; + chn1_state1 = 0; + chn1_frqcode = 0; + chn1_run = 1; + chn2_state = 0; + chn2_prv = 0; + chn2_state1 = 0; + chn2_frqcode = 0; + chn2_run = 1; + chn3_state = 0; + chn3_prv = 0; + chn3_state1 = 0; + chn3_state2 = 0; + chn3_clk_source_prv = 0; + int_snd_state = 0; + int_1hz_state = 0; + int_1_state = 1; + int_2_state = 1; + int_snd_active = 0; + int_1hz_active = 0; + int_1_active = 0; + int_2_active = 0; + audioOutput = 0; + tape_input = 0; + tape_input_level = 0; + this->reset(true); + } + + Dave::~Dave() + { + } + + void Dave::reset(bool isColdReset) + { + polycnt4_phase = 0; + polycnt5_phase = 0; + polycnt7_phase = 0; + polycntVL_phase = 0; + chn0_phase = 0; + chn1_phase = 0; + chn2_phase = 0; + clk_62500_phase = 0; + clk_1000_phase = 0; + clk_50_phase = 0; + clk_1_phase = 0; + // initialize registers + // this will also reset many variables to the default value + for (uint16_t i = 0x00; i < 0x20; i++) + writePort(i, 0); + // clear all interrupts + writePort(0x14, 0xAA); + if (isColdReset) { + // reset keyboard state + for (int i = 0; i < 16; i++) + keyboardState[i] = 0xFF; + } + mouseInput = 0xFF; + } + + void Dave::setTapeInput(int state, int level) + { + tape_input = (state ? 1 : 0); + tape_input_level = (level > 0 ? 1 : 0); + } + + void Dave::setKeyboardState(int keyCode, int state) + { + int row = (keyCode & 0x78) >> 3; + uint8_t mask = uint8_t(1 << (keyCode & 0x07)); + if (!state) + keyboardState[row] |= mask; + else + keyboardState[row] &= (~mask); + } + + // -------------------------------------------------------------------------- + + void Dave::setMemoryPage(uint8_t page, uint8_t segment) + { + (void) page; + (void) segment; + } + + void Dave::setMemoryWaitMode(int mode) + { + (void) mode; + } + + void Dave::setRemote1State(int state) + { + (void) state; + } + + void Dave::setRemote2State(int state) + { + (void) state; + } + + void Dave::interruptRequest() + { + } + + void Dave::clearInterruptRequest() + { + } + +} // namespace Ep128 + diff --git a/src/engine/platform/sound/dave/dave.hpp b/src/engine/platform/sound/dave/dave.hpp new file mode 100644 index 000000000..8e44ca49a --- /dev/null +++ b/src/engine/platform/sound/dave/dave.hpp @@ -0,0 +1,251 @@ + +// ep128emu -- portable Enterprise 128 emulator +// Copyright (C) 2003-2016 Istvan Varga +// https://github.com/istvan-v/ep128emu/ +// +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +#ifndef EP128EMU_DAVE_HPP +#define EP128EMU_DAVE_HPP + +#include + +namespace Ep128 { + + class DaveTables { + public: + // tables for polynomial counters + // (note: the table data is stored in reverse order) + uint8_t *polycnt4_table; // length = 15, poly = 0x0000000C + uint8_t *polycnt5_table; // length = 31, poly = 0x00000014 + uint8_t *polycnt7_table; // length = 127, poly = 0x00000060 + uint8_t *polycnt9_table; // length = 511, poly = 0x00000110 + uint8_t *polycnt11_table; // length = 2047, poly = 0x00000500 + uint8_t *polycnt15_table; // length = 32767, poly = 0x00006000 + uint8_t *polycnt17_table; // length = 131071, poly = 0x00012000 + // ---------------- + DaveTables(); + ~DaveTables(); + }; + + class Dave { + private: + DaveTables t; + int clockDiv; // 2 if bit 1 of port 0xBF is 0, 3 otherwise + int clockCnt; // counts from 'clockDiv' towards zero + // variable length counter uses one of the 9, 11, 15, and 17 bit tables + uint8_t *polycntVL_table; + // polynomial counters + int polycnt4_phase; // 4-bit counter phase (14 -> 0) + int polycnt5_phase; // 5-bit counter phase (30 -> 0) + int polycnt7_phase; // 7-bit counter phase (126 -> 0) + int polycntVL_phase; // variable length counter phase ... + int polycntVL_maxphase; // ... counts from this value to zero + int polycnt4_state; // 4-bit counter output + int polycnt5_state; // 5-bit counter output + int polycnt7_state; // 7-bit counter output + int polycntVL_state; // variable length counter output + // fixed frequency counters (f = 250000 / (n + 1)) + static const int clk_62500_frq = 3; + static const int clk_1000_frq = 249; + static const int clk_50_frq = 19; // clocked by the 1 kHz counter + static const int clk_1_frq = 49; // clocked by the 50 Hz counter + int clk_62500_phase; + int clk_1000_phase; + int clk_50_phase; + int clk_1_phase; + int clk_62500_state; + // channel 0 parameters + int chn0_state; // current output state + int chn0_prv; // previous output state + int chn0_state1; // oscillator output + int chn0_phase; // phase (frqcode -> 0) + int chn0_frqcode; // frequency code (0 - 4095) + int *chn0_input_polycnt; // polynomial counter + int chn0_hp_1; // enable highpass filter + int chn0_rm_2; // enable ring modulation + int chn0_run; // 1: oscillator is running + int chn0_left; // left volume (0 - 63) + int chn0_right; // right volume (0 - 63) + // channel 1 parameters + int chn1_state; // current output state + int chn1_prv; // previous output state + int chn1_state1; // oscillator output + int chn1_phase; // phase (frqcode -> 0) + int chn1_frqcode; // frequency code (0 - 4095) + int *chn1_input_polycnt; // polynomial counter + int chn1_hp_2; // enable highpass filter + int chn1_rm_3; // enable ring modulation + int chn1_run; // 1: oscillator is running + int chn1_left; // left volume (0 - 63) + int chn1_right; // right volume (0 - 63) + // channel 2 parameters + int chn2_state; // current output state + int chn2_prv; // previous output state + int chn2_state1; // oscillator output + int chn2_phase; // phase (frqcode -> 0) + int chn2_frqcode; // frequency code (0 - 4095) + int *chn2_input_polycnt; // polynomial counter + int chn2_hp_3; // enable highpass filter + int chn2_rm_0; // enable ring modulation + int chn2_run; // 1: oscillator is running + int chn2_left; // left volume (0 - 63) + int chn2_right; // right volume (0 - 63) + // channel 3 (noise) parameters + int chn3_state; // current output state + int chn3_prv; // previous output state + int chn3_state1; // polynomial counter output + int chn3_state2; // lowpass filter output + int *chn3_clk_source; // clock input signal + int chn3_clk_source_prv; // previous clock input + int noise_polycnt_is_7bit; // 0xA6 port bit 4 + int *chn3_input_polycnt; // polynomial counter + int chn3_lp_2; // enable lowpass filter + int chn3_hp_0; // enable highpass filter + int chn3_rm_1; // enable ring modulation + int chn3_left; // left volume (0 - 63) + int chn3_right; // right volume (0 - 63) + // enable DAC mode for left/right channel + int dac_mode_left; + int dac_mode_right; + // interrupts + int *int_snd_phase; + int enable_int_snd; + int enable_int_1hz; + int enable_int_1; + int enable_int_2; + int int_snd_state; + int int_1hz_state; + int int_1_state; + int int_2_state; + int int_snd_active; + int int_1hz_active; + int int_1_active; + int int_2_active; + uint32_t audioOutput; + uint8_t page0Segment; + uint8_t page1Segment; + uint8_t page2Segment; + uint8_t page3Segment; + int tape_feedback; + int tape_input; + int tape_input_level; + int keyboardRow; + uint8_t keyboardState[16]; + // b0..b3 = current nibble + // b4 = button 1 (left) state (0 = pressed) + // b5 = button 2 (right) state + uint8_t mouseInput; // b7 == 1: mouse data bits inactive + // ---------------- + inline void triggerIntSnd(); + inline void triggerInt1Hz(); + int * findPolycntForToneChannel(int n); + uint32_t runOneCycle_(); + public: + Dave(); + virtual ~Dave(); + protected: + virtual void setMemoryPage(uint8_t page, uint8_t segment); + virtual void setMemoryWaitMode(int mode); + virtual void setRemote1State(int state); + virtual void setRemote2State(int state); + virtual void interruptRequest(); + virtual void clearInterruptRequest(); + public: + /*! + * Run DAVE emulation for 2 us (clock frequency = 500 kHz). + * Return value is audio output in left_channel + (right_channel << 16) + * format, where the range for a single channel is 0 to 40320 (sum of 4 + * sound generators and tape feedback, 0 to 8064 each). + */ + inline uint32_t runOneCycle() + { + if (--clockCnt > 0) + return audioOutput; + clockCnt = clockDiv; + return runOneCycle_(); + } + /*! + * Write to a DAVE register. + */ + void writePort(uint16_t addr, uint8_t value); + /*! + * Read from a DAVE register. + */ + uint8_t readPort(uint16_t addr) const; + /*! + * Set hardware interrupt 1 state, and (possibly) trigger interrupt. + */ + void setInt1State(int new_state); + /*! + * Set hardware interrupt 2 state, and (possibly) trigger interrupt. + */ + void setInt2State(int new_state); + /*! + * Set tape input state, and level (0: low, 1: high). + */ + void setTapeInput(int state, int level); + /*! + * Set state of key 'keyCode' (0 to 127, see table below) to pressed + * (state != 0) or released (state == 0). + * +-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | + * | 0x08 | 0x09 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0E | 0x0F | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x00 | N | \ | B | C | V | X | Z | SHF_L | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x08 | H | LOCK | G | D | F | S | A | CTRL | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x10 | U | Q | Y | R | T | E | W | TAB | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x18 | 7 | 1 | 6 | 4 | 5 | 3 | 2 | ESC | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x20 | F4 | F8 | F3 | F6 | F5 | F7 | F2 | F1 | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x28 | 8 | | 9 | - | 0 | ^ | ERASE | | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x30 | J | | K | ; | L | : | ] | | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x38 | STOP | DOWN | RIGHT | UP | HOLD | LEFT | ENTER | ALT | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x40 | M | DEL | , | / | . | SHF_R | SPACE | INS | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x48 | I | | O | @ | P | [ | | | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x70 | JOY1R | JOY1L | JOY1D | JOY1U | JOY1F | JOY1F2| JOY1F3| | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + * | 0x78 | JOY2R | JOY2L | JOY2D | JOY2U | JOY2F | JOY2F2| JOY2F3| | + * +------+-------+-------+-------+-------+-------+-------+-------+-------+ + */ + void setKeyboardState(int keyCode, int state); + inline void setMouseInput(uint8_t value) + { + mouseInput = value; + } + inline void clearMouseInput() + { + // clear data bits, but not the buttons + mouseInput = mouseInput | 0xCF; + } + /*! + * Reset DAVE. + */ + void reset(bool isColdReset = false); + }; + +} // namespace Ep128 + +#endif // EP128EMU_DAVE_HPP + diff --git a/src/engine/song.h b/src/engine/song.h index a4b60238a..932e3924d 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -134,6 +134,7 @@ enum DivSystem { DIV_SYSTEM_C219, DIV_SYSTEM_ESFM, DIV_SYSTEM_POWERNOISE, + DIV_SYSTEM_DAVE, }; enum DivEffectType: unsigned short { diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 65b4cf1f0..96b5228e2 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -2002,6 +2002,21 @@ void DivEngine::registerSystems() { {} ); + sysDefs[DIV_SYSTEM_DAVE]=new DivSysDef( + "Dave", NULL, 0xd5, 0, 6, false, true, 0, false, 1U<type==DIV_INS_PCE || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_SM8521) { volMax=31; } - if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS || ins->type==DIV_INS_VERA || ins->type==DIV_INS_VRC6_SAW || ins->type==DIV_INS_ESFM) { + if (ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPL_DRUMS || ins->type==DIV_INS_VERA || ins->type==DIV_INS_VRC6_SAW || ins->type==DIV_INS_ESFM || ins->type==DIV_INS_DAVE) { volMax=63; } if (ins->type==DIV_INS_AMIGA) { @@ -6916,6 +6920,10 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_POWERNOISE_SLOPE) { dutyMax=0; } + if (ins->type==DIV_INS_DAVE) { + dutyLabel="Noise Freq"; + dutyMax=3; + } const char* waveLabel="Waveform"; int waveMax=(ins->type==DIV_INS_VERA)?3:(MAX(1,e->song.waveLen-1)); @@ -6953,6 +6961,7 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_POWERNOISE) waveMax=0; if (ins->type==DIV_INS_POWERNOISE_SLOPE) waveMax=0; if (ins->type==DIV_INS_SU || ins->type==DIV_INS_POKEY) waveMax=7; + if (ins->type==DIV_INS_DAVE) waveMax=4; if (ins->type==DIV_INS_PET) { waveMax=8; waveBitMode=true; @@ -7019,12 +7028,15 @@ void FurnaceGUI::drawInsEdit() { ex1Max=5; ex2Max=11; } + if (ins->type==DIV_INS_DAVE) { + ex1Max=4; + } int panMin=0; int panMax=0; bool panSingle=false; bool panSingleNoBit=false; - if (ins->type==DIV_INS_STD ||//Game Gear + if (ins->type==DIV_INS_STD || // Game Gear ins->type==DIV_INS_FM || ins->type==DIV_INS_OPM || ins->type==DIV_INS_GB || @@ -7096,6 +7108,9 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_POWERNOISE_SLOPE) { panMax=15; } + if (ins->type==DIV_INS_DAVE) { + panMax=63; + } if (volMax>0) { macroList.push_back(FurnaceGUIMacroDesc(volumeLabel,&ins->std.volMacro,volMin,volMax,160,uiColors[GUI_COLOR_MACRO_VOLUME])); @@ -7185,7 +7200,8 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_TED || ins->type==DIV_INS_ESFM || ins->type==DIV_INS_POWERNOISE || - ins->type==DIV_INS_POWERNOISE_SLOPE) { + ins->type==DIV_INS_POWERNOISE_SLOPE || + ins->type==DIV_INS_DAVE) { macroList.push_back(FurnaceGUIMacroDesc("Phase Reset",&ins->std.phaseResetMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true)); } if (ex1Max>0) { @@ -7219,6 +7235,8 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("Special",&ins->std.ex1Macro,0,ex1Max,96,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,snesModeBits)); } else if (ins->type==DIV_INS_MSM5232) { macroList.push_back(FurnaceGUIMacroDesc("Group Attack",&ins->std.ex1Macro,0,ex1Max,96,uiColors[GUI_COLOR_MACRO_OTHER])); + } else if (ins->type==DIV_INS_DAVE) { + macroList.push_back(FurnaceGUIMacroDesc("Control",&ins->std.ex1Macro,0,ex1Max,64,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,daveControlBits)); } else { macroList.push_back(FurnaceGUIMacroDesc("Duty",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index c5d087de5..18a0ebdc9 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -952,6 +952,12 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_SAA1099, 1.0f, 0, "") } ); + ENTRY( + "Enterprise 128", { + CH(DIV_SYSTEM_DAVE, 1.0f, 0, "") + }, + "tickRate=50" + ); ENTRY( "BBC Micro", { CH(DIV_SYSTEM_SMS, 1.0f, 0, @@ -2861,6 +2867,12 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_POWERNOISE, 1.0f, 0, "") } ); + ENTRY( + "Dave", { + CH(DIV_SYSTEM_DAVE, 1.0f, 0, "") + }, + "tickRate=50" + ); CATEGORY_END; CATEGORY_BEGIN("DefleMask-compatible","these configurations are compatible with DefleMask.\nselect this if you need to save as .dmf or work with that program."); diff --git a/src/icon/furIcons.h b/src/icon/furIcons.h index fb1c8a7b5..fe70ba1f5 100644 --- a/src/icon/furIcons.h +++ b/src/icon/furIcons.h @@ -1,7 +1,7 @@ // not auto-generated. update every time you change icons.ttf! #define ICON_MIN_FUR 0xe0f0 -#define ICON_MAX_FUR 0xe15b +#define ICON_MAX_FUR 0xe15c // test #define ICON_FUR_TEST0 u8"\ue0f0" @@ -69,6 +69,7 @@ #define ICON_FUR_INS_ESFM u8"\ue143" #define ICON_FUR_INS_POWERNOISE u8"\ue15a" #define ICON_FUR_INS_POWERNOISE_SAW u8"\ue15b" +#define ICON_FUR_INS_DAVE u8"\ue15c" // sample editor #define ICON_FUR_SAMPLE_APPLY_SILENCE u8"\ue136"