diff --git a/.gitmodules b/.gitmodules index c78fee42a..498a32410 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,3 +15,6 @@ [submodule "extern/portaudio"] path = extern/portaudio url = https://github.com/PortAudio/portaudio.git +[submodule "extern/adpcm-xq"] + path = extern/adpcm-xq + url = https://github.com/dbry/adpcm-xq.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4059c694f..0f6942bbc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -485,6 +485,8 @@ extern/adpcm/yma_codec.c extern/adpcm/ymb_codec.c extern/adpcm/ymz_codec.c +extern/adpcm-xq/adpcm-lib.c + extern/opn/ym3438.c extern/Nuked-PSG/ympsg.c extern/opm/opm.c @@ -609,6 +611,8 @@ src/engine/platform/sound/c140_c219.c src/engine/platform/sound/dave/dave.cpp +src/engine/platform/sound/nds.cpp + src/engine/platform/oplAInterface.cpp src/engine/platform/ym2608Interface.cpp src/engine/platform/ym2610Interface.cpp @@ -620,6 +624,7 @@ src/engine/fileOps/ftm.cpp src/engine/fileOps/fur.cpp src/engine/fileOps/mod.cpp src/engine/fileOps/s3m.cpp +src/engine/fileOps/text.cpp src/engine/blip_buf.c src/engine/brrUtils.c @@ -719,6 +724,7 @@ src/engine/platform/powernoise.cpp src/engine/platform/dave.cpp src/engine/platform/gbadma.cpp src/engine/platform/gbaminmod.cpp +src/engine/platform/nds.cpp src/engine/platform/pcmdac.cpp src/engine/platform/dummy.cpp diff --git a/demos/specs2/rastaline_dub.fur b/demos/specs2/rastaline_dub.fur new file mode 100644 index 000000000..6de407b4f Binary files /dev/null and b/demos/specs2/rastaline_dub.fur differ diff --git a/extern/adpcm-xq b/extern/adpcm-xq new file mode 160000 index 000000000..6220fed76 --- /dev/null +++ b/extern/adpcm-xq @@ -0,0 +1 @@ +Subproject commit 6220fed7655e86a29702b45dbc641a028ed5a4bf diff --git a/papers/format.md b/papers/format.md index 97c1f1302..e28a14773 100644 --- a/papers/format.md +++ b/papers/format.md @@ -564,9 +564,13 @@ size | description | - 4: QSound ADPCM | - 5: ADPCM-A | - 6: ADPCM-B + | - 7: K05 ADPCM | - 8: 8-bit PCM | - 9: BRR (SNES) | - 10: VOX + | - 11: 8-bit μ-law PCM + | - 12: C219 PCM + | - 13: IMA ADPCM | - 16: 16-bit PCM 1 | loop direction (>=123) or reserved | - 0: forward diff --git a/papers/newIns.md b/papers/newIns.md index d301d27c2..f37a54deb 100644 --- a/papers/newIns.md +++ b/papers/newIns.md @@ -124,6 +124,8 @@ the following instrument types are available: - 55: ESFM - 56: PowerNoise (noise) - 57: PowerNoise (slope) +- 58: Dave +- 59: NDS the following feature codes are recognized: diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 2840ce740..8d4983c77 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -87,6 +87,7 @@ #include "platform/esfm.h" #include "platform/powernoise.h" #include "platform/dave.h" +#include "platform/nds.h" #include "platform/dummy.h" #include "../ta-log.h" #include "song.h" @@ -669,6 +670,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_DAVE: dispatch=new DivPlatformDave; break; + case DIV_SYSTEM_NDS: + dispatch=new DivPlatformNDS; + break; case DIV_SYSTEM_DUMMY: dispatch=new DivPlatformDummy; break; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index cfc864c20..05387fd8f 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -989,6 +989,7 @@ void DivEngine::delUnusedSamples() { i->type==DIV_INS_K053260 || i->type==DIV_INS_C140 || i->type==DIV_INS_C219 || + i->type==DIV_INS_NDS || i->type==DIV_INS_GBA_DMA || i->type==DIV_INS_GBA_MINMOD) { if (i->amiga.initSample>=0 && i->amiga.initSamplewriteText("- macros:\n"); - wroteMacroHeader=true; - } - w->writeText(fmt::sprintf(" - %s:",name)); - int len=m.len; - switch (m.open&6) { - case 2: - len=16; - w->writeText(" [ADSR]"); - break; - case 4: - len=16; - w->writeText(" [LFO]"); - break; - } - if (m.mode) { - w->writeText(fmt::sprintf(" [MODE %d]",m.mode)); - } - if (m.delay>0) { - w->writeText(fmt::sprintf(" [DELAY %d]",m.delay)); - } - if (m.speed>1) { - w->writeText(fmt::sprintf(" [SPEED %d]",m.speed)); - } - for (int i=0; iwriteText(" |"); - } - if (i==m.rel) { - w->writeText(" /"); - } - w->writeText(fmt::sprintf(" %d",m.val[i])); - } - w->writeText("\n"); -} - -SafeWriter* DivEngine::saveText(bool separatePatterns) { - saveLock.lock(); - - SafeWriter* w=new SafeWriter; - w->init(); - - w->writeText(fmt::sprintf("# Furnace Text Export\n\ngenerated by Furnace %s (%d)\n\n# Song Information\n\n",DIV_VERSION,DIV_ENGINE_VERSION)); - w->writeText(fmt::sprintf("- name: %s\n",song.name)); - w->writeText(fmt::sprintf("- author: %s\n",song.author)); - w->writeText(fmt::sprintf("- album: %s\n",song.category)); - w->writeText(fmt::sprintf("- system: %s\n",song.systemName)); - w->writeText(fmt::sprintf("- tuning: %g\n\n",song.tuning)); - - w->writeText(fmt::sprintf("- instruments: %d\n",song.insLen)); - w->writeText(fmt::sprintf("- wavetables: %d\n",song.waveLen)); - w->writeText(fmt::sprintf("- samples: %d\n\n",song.sampleLen)); - - w->writeText("# Sound Chips\n\n"); - - for (int i=0; iwriteText(fmt::sprintf("- %s\n",getSystemName(song.system[i]))); - w->writeText(fmt::sprintf(" - id: %.2X\n",(int)song.system[i])); - w->writeText(fmt::sprintf(" - volume: %g\n",song.systemVol[i])); - w->writeText(fmt::sprintf(" - panning: %g\n",song.systemPan[i])); - w->writeText(fmt::sprintf(" - front/rear: %g\n",song.systemPanFR[i])); - - String sysFlags=song.systemFlags[i].toString(); - - if (!sysFlags.empty()) { - w->writeText(fmt::sprintf(" - flags:\n```\n%s\n```\n",sysFlags)); - } - } - - if (!song.notes.empty()) { - w->writeText("\n# Song Comments\n\n"); - w->writeText(song.notes); - } - - w->writeText("\n# Instruments\n\n"); - - for (int i=0; iwriteText(fmt::sprintf("## %.2X: %s\n\n",i,ins->name)); - - w->writeText(fmt::sprintf("- type: %d\n",(int)ins->type)); - - if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPZ || ins->type==DIV_INS_OPL_DRUMS || ins->type==DIV_INS_OPM || ins->type==DIV_INS_ESFM) { - w->writeText("- FM parameters:\n"); - w->writeText(fmt::sprintf(" - ALG: %d\n",ins->fm.alg)); - w->writeText(fmt::sprintf(" - FB: %d\n",ins->fm.fb)); - w->writeText(fmt::sprintf(" - FMS: %d\n",ins->fm.fms)); - w->writeText(fmt::sprintf(" - AMS: %d\n",ins->fm.ams)); - w->writeText(fmt::sprintf(" - FMS2: %d\n",ins->fm.fms2)); - w->writeText(fmt::sprintf(" - AMS2: %d\n",ins->fm.ams2)); - w->writeText(fmt::sprintf(" - operators: %d\n",ins->fm.ops)); - w->writeText(fmt::sprintf(" - OPLL patch: %d\n",ins->fm.opllPreset)); - w->writeText(fmt::sprintf(" - fixed drum freq: %s\n",trueFalse[ins->fm.fixedDrums?1:0])); - w->writeText(fmt::sprintf(" - kick freq: %.4X\n",ins->fm.kickFreq)); - w->writeText(fmt::sprintf(" - snare/hat freq: %.4X\n",ins->fm.snareHatFreq)); - w->writeText(fmt::sprintf(" - tom/top freq: %.4X\n",ins->fm.tomTopFreq)); - - for (int j=0; jfm.ops; j++) { - DivInstrumentFM::Operator& op=ins->fm.op[j]; - - w->writeText(fmt::sprintf(" - operator %d:\n",j)); - w->writeText(fmt::sprintf(" - enabled: %s\n",trueFalse[op.enable?1:0])); - w->writeText(fmt::sprintf(" - AM: %d\n",op.am)); - w->writeText(fmt::sprintf(" - AR: %d\n",op.ar)); - w->writeText(fmt::sprintf(" - DR: %d\n",op.dr)); - w->writeText(fmt::sprintf(" - MULT: %d\n",op.mult)); - w->writeText(fmt::sprintf(" - RR: %d\n",op.rr)); - w->writeText(fmt::sprintf(" - SL: %d\n",op.sl)); - w->writeText(fmt::sprintf(" - TL: %d\n",op.tl)); - w->writeText(fmt::sprintf(" - DT2: %d\n",op.dt2)); - w->writeText(fmt::sprintf(" - RS: %d\n",op.rs)); - w->writeText(fmt::sprintf(" - DT: %d\n",op.dt)); - w->writeText(fmt::sprintf(" - D2R: %d\n",op.d2r)); - w->writeText(fmt::sprintf(" - SSG-EG: %d\n",op.ssgEnv)); - w->writeText(fmt::sprintf(" - DAM: %d\n",op.dam)); - w->writeText(fmt::sprintf(" - DVB: %d\n",op.dvb)); - w->writeText(fmt::sprintf(" - EGT: %d\n",op.egt)); - w->writeText(fmt::sprintf(" - KSL: %d\n",op.ksl)); - w->writeText(fmt::sprintf(" - SUS: %d\n",op.sus)); - w->writeText(fmt::sprintf(" - VIB: %d\n",op.vib)); - w->writeText(fmt::sprintf(" - WS: %d\n",op.ws)); - w->writeText(fmt::sprintf(" - KSR: %d\n",op.ksr)); - w->writeText(fmt::sprintf(" - TL volume scale: %d\n",op.kvs)); - } - } - - if (ins->type==DIV_INS_ESFM) { - w->writeText("- ESFM parameters:\n"); - w->writeText(fmt::sprintf(" - noise mode: %d\n",ins->esfm.noise)); - - for (int j=0; jfm.ops; j++) { - DivInstrumentESFM::Operator& opE=ins->esfm.op[j]; - - w->writeText(fmt::sprintf(" - operator %d:\n",j)); - w->writeText(fmt::sprintf(" - DL: %d\n",opE.delay)); - w->writeText(fmt::sprintf(" - OL: %d\n",opE.outLvl)); - w->writeText(fmt::sprintf(" - MI: %d\n",opE.modIn)); - w->writeText(fmt::sprintf(" - output left: %s\n",trueFalse[opE.left?1:0])); - w->writeText(fmt::sprintf(" - output right: %s\n",trueFalse[opE.right?1:0])); - w->writeText(fmt::sprintf(" - CT: %d\n",opE.ct)); - w->writeText(fmt::sprintf(" - DT: %d\n",opE.dt)); - w->writeText(fmt::sprintf(" - fixed frequency: %s\n",trueFalse[opE.fixed?1:0])); - } - } - - if (ins->type==DIV_INS_GB) { - w->writeText("- Game Boy parameters:\n"); - w->writeText(fmt::sprintf(" - volume: %d\n",ins->gb.envVol)); - w->writeText(fmt::sprintf(" - direction: %s\n",gbEnvDir[ins->gb.envDir?1:0])); - w->writeText(fmt::sprintf(" - length: %d\n",ins->gb.envLen)); - w->writeText(fmt::sprintf(" - sound length: %d\n",ins->gb.soundLen)); - w->writeText(fmt::sprintf(" - use software envelope: %s\n",trueFalse[ins->gb.softEnv?1:0])); - w->writeText(fmt::sprintf(" - always initialize: %s\n",trueFalse[ins->gb.softEnv?1:0])); - if (ins->gb.hwSeqLen>0) { - w->writeText(" - hardware sequence:\n"); - for (int j=0; jgb.hwSeqLen; j++) { - w->writeText(fmt::sprintf(" - %d: %.2X %.4X\n",j,ins->gb.hwSeq[j].cmd,ins->gb.hwSeq[j].data)); - } - } - } - - bool header=false; - writeTextMacro(w,ins->std.volMacro,"vol",header); - writeTextMacro(w,ins->std.arpMacro,"arp",header); - writeTextMacro(w,ins->std.dutyMacro,"duty",header); - writeTextMacro(w,ins->std.waveMacro,"wave",header); - writeTextMacro(w,ins->std.pitchMacro,"pitch",header); - writeTextMacro(w,ins->std.panLMacro,"panL",header); - writeTextMacro(w,ins->std.panRMacro,"panR",header); - writeTextMacro(w,ins->std.phaseResetMacro,"phaseReset",header); - writeTextMacro(w,ins->std.ex1Macro,"ex1",header); - writeTextMacro(w,ins->std.ex2Macro,"ex2",header); - writeTextMacro(w,ins->std.ex3Macro,"ex3",header); - writeTextMacro(w,ins->std.ex4Macro,"ex4",header); - writeTextMacro(w,ins->std.ex5Macro,"ex5",header); - writeTextMacro(w,ins->std.ex6Macro,"ex6",header); - writeTextMacro(w,ins->std.ex7Macro,"ex7",header); - writeTextMacro(w,ins->std.ex8Macro,"ex8",header); - writeTextMacro(w,ins->std.algMacro,"alg",header); - writeTextMacro(w,ins->std.fbMacro,"fb",header); - writeTextMacro(w,ins->std.fmsMacro,"fms",header); - writeTextMacro(w,ins->std.amsMacro,"ams",header); - - // TODO: the rest - w->writeText("\n"); - } - - w->writeText("\n# Wavetables\n\n"); - - for (int i=0; iwriteText(fmt::sprintf("- %d (%dx%d):",i,wave->len+1,wave->max+1)); - for (int j=0; j<=wave->len; j++) { - w->writeText(fmt::sprintf(" %d",wave->data[j])); - } - w->writeText("\n"); - } - - w->writeText("\n# Samples\n\n"); - - for (int i=0; iwriteText(fmt::sprintf("## %.2X: %s\n\n",i,sample->name)); - - w->writeText(fmt::sprintf("- format: %d\n",(int)sample->depth)); - w->writeText(fmt::sprintf("- data length: %d\n",sample->getCurBufLen())); - w->writeText(fmt::sprintf("- samples: %d\n",sample->samples)); - w->writeText(fmt::sprintf("- rate: %d\n",sample->centerRate)); - w->writeText(fmt::sprintf("- compat rate: %d\n",sample->rate)); - w->writeText(fmt::sprintf("- loop: %s\n",trueFalse[sample->loop?1:0])); - if (sample->loop) { - w->writeText(fmt::sprintf(" - start: %d\n",sample->loopStart)); - w->writeText(fmt::sprintf(" - end: %d\n",sample->loopEnd)); - w->writeText(fmt::sprintf(" - mode: %s\n",sampleLoopModes[sample->loopMode&3])); - } - w->writeText(fmt::sprintf("- BRR emphasis: %s\n",trueFalse[sample->brrEmphasis?1:0])); - w->writeText(fmt::sprintf("- dither: %s\n",trueFalse[sample->dither?1:0])); - - // TODO' render matrix - unsigned char* buf=(unsigned char*)sample->getCurBuf(); - unsigned int bufLen=sample->getCurBufLen(); - w->writeText("\n```"); - for (unsigned int i=0; iwriteText(fmt::sprintf("\n%.8X:",i)); - w->writeText(fmt::sprintf(" %.2X",buf[i])); - } - w->writeText("\n```\n\n"); - } - - w->writeText("\n# Subsongs\n\n"); - - for (size_t i=0; iwriteText(fmt::sprintf("## %d: %s\n\n",(int)i,s->name)); - - w->writeText(fmt::sprintf("- tick rate: %g\n",s->hz)); - w->writeText(fmt::sprintf("- speeds:")); - for (int j=0; jspeeds.len; j++) { - w->writeText(fmt::sprintf(" %d",s->speeds.val[j])); - } - w->writeText("\n"); - w->writeText(fmt::sprintf("- virtual tempo: %d/%d\n",s->virtualTempoN,s->virtualTempoD)); - w->writeText(fmt::sprintf("- time base: %d\n",s->timeBase)); - w->writeText(fmt::sprintf("- pattern length: %d\n",s->patLen)); - w->writeText(fmt::sprintf("\norders:\n```\n")); - - for (int j=0; jordersLen; j++) { - w->writeText(fmt::sprintf("%.2X |",j)); - for (int k=0; kwriteText(fmt::sprintf(" %.2X",s->orders.ord[k][j])); - } - w->writeText("\n"); - } - w->writeText("```\n\n## Patterns\n\n"); - - if (separatePatterns) { - w->writeText("TODO: separate patterns\n\n"); - } else { - for (int j=0; jordersLen; j++) { - w->writeText(fmt::sprintf("----- ORDER %.2X\n",j)); - - for (int k=0; kpatLen; k++) { - w->writeText(fmt::sprintf("%.2X ",k)); - - for (int l=0; lpat[l].getPattern(s->orders.ord[l][j],false); - - int note=p->data[k][0]; - int octave=p->data[k][1]; - - if (note==0 && octave==0) { - w->writeText("|... "); - } else if (note==100) { - w->writeText("|OFF "); - } else if (note==101) { - w->writeText("|=== "); - } else if (note==102) { - w->writeText("|REL "); - } else if ((octave>9 && octave<250) || note>12) { - w->writeText("|??? "); - } else { - if (octave>=128) octave-=256; - if (note>11) { - note-=12; - octave++; - } - w->writeText(fmt::sprintf("|%s%d ",(octave<0)?notesNegative[note]:notes[note],(octave<0)?(-octave):octave)); - } - - if (p->data[k][2]==-1) { - w->writeText(".. "); - } else { - w->writeText(fmt::sprintf("%.2X ",p->data[k][2]&0xff)); - } - - if (p->data[k][3]==-1) { - w->writeText(".."); - } else { - w->writeText(fmt::sprintf("%.2X",p->data[k][3]&0xff)); - } - - for (int m=0; mpat[l].effectCols; m++) { - if (p->data[k][4+(m<<1)]==-1) { - w->writeText(" .."); - } else { - w->writeText(fmt::sprintf(" %.2X",p->data[k][4+(m<<1)]&0xff)); - } - if (p->data[k][5+(m<<1)]==-1) { - w->writeText(".."); - } else { - w->writeText(fmt::sprintf("%.2X",p->data[k][5+(m<<1)]&0xff)); - } - } - } - - w->writeText("\n"); - } - } - } - - } - - saveLock.unlock(); - return w; -} diff --git a/src/engine/fileOps/text.cpp b/src/engine/fileOps/text.cpp new file mode 100644 index 000000000..6ededd1fd --- /dev/null +++ b/src/engine/fileOps/text.cpp @@ -0,0 +1,372 @@ +/** + * 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 "fileOpsCommon.h" + +static const char* trueFalse[2]={ + "no", "yes" +}; + +static const char* gbEnvDir[2]={ + "down", "up" +}; + +static const char* notes[12]={ + "C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-" +}; + +static const char* notesNegative[12]={ + "c_", "c+", "d_", "d+", "e_", "f_", "f+", "g_", "g+", "a_", "a+", "b_" +}; + +static const char* sampleLoopModes[4]={ + "forward", "backward", "ping-pong", "invalid" +}; + +void writeTextMacro(SafeWriter* w, DivInstrumentMacro& m, const char* name, bool& wroteMacroHeader) { + if ((m.open&6)==0 && m.len<1) return; + if (!wroteMacroHeader) { + w->writeText("- macros:\n"); + wroteMacroHeader=true; + } + w->writeText(fmt::sprintf(" - %s:",name)); + int len=m.len; + switch (m.open&6) { + case 2: + len=16; + w->writeText(" [ADSR]"); + break; + case 4: + len=16; + w->writeText(" [LFO]"); + break; + } + if (m.mode) { + w->writeText(fmt::sprintf(" [MODE %d]",m.mode)); + } + if (m.delay>0) { + w->writeText(fmt::sprintf(" [DELAY %d]",m.delay)); + } + if (m.speed>1) { + w->writeText(fmt::sprintf(" [SPEED %d]",m.speed)); + } + for (int i=0; iwriteText(" |"); + } + if (i==m.rel) { + w->writeText(" /"); + } + w->writeText(fmt::sprintf(" %d",m.val[i])); + } + w->writeText("\n"); +} + +SafeWriter* DivEngine::saveText(bool separatePatterns) { + saveLock.lock(); + + SafeWriter* w=new SafeWriter; + w->init(); + + w->writeText(fmt::sprintf("# Furnace Text Export\n\ngenerated by Furnace %s (%d)\n\n# Song Information\n\n",DIV_VERSION,DIV_ENGINE_VERSION)); + w->writeText(fmt::sprintf("- name: %s\n",song.name)); + w->writeText(fmt::sprintf("- author: %s\n",song.author)); + w->writeText(fmt::sprintf("- album: %s\n",song.category)); + w->writeText(fmt::sprintf("- system: %s\n",song.systemName)); + w->writeText(fmt::sprintf("- tuning: %g\n\n",song.tuning)); + + w->writeText(fmt::sprintf("- instruments: %d\n",song.insLen)); + w->writeText(fmt::sprintf("- wavetables: %d\n",song.waveLen)); + w->writeText(fmt::sprintf("- samples: %d\n\n",song.sampleLen)); + + w->writeText("# Sound Chips\n\n"); + + for (int i=0; iwriteText(fmt::sprintf("- %s\n",getSystemName(song.system[i]))); + w->writeText(fmt::sprintf(" - id: %.2X\n",(int)song.system[i])); + w->writeText(fmt::sprintf(" - volume: %g\n",song.systemVol[i])); + w->writeText(fmt::sprintf(" - panning: %g\n",song.systemPan[i])); + w->writeText(fmt::sprintf(" - front/rear: %g\n",song.systemPanFR[i])); + + String sysFlags=song.systemFlags[i].toString(); + + if (!sysFlags.empty()) { + w->writeText(fmt::sprintf(" - flags:\n```\n%s\n```\n",sysFlags)); + } + } + + if (!song.notes.empty()) { + w->writeText("\n# Song Comments\n\n"); + w->writeText(song.notes); + } + + w->writeText("\n# Instruments\n\n"); + + for (int i=0; iwriteText(fmt::sprintf("## %.2X: %s\n\n",i,ins->name)); + + w->writeText(fmt::sprintf("- type: %d\n",(int)ins->type)); + + if (ins->type==DIV_INS_FM || ins->type==DIV_INS_OPL || ins->type==DIV_INS_OPLL || ins->type==DIV_INS_OPZ || ins->type==DIV_INS_OPL_DRUMS || ins->type==DIV_INS_OPM || ins->type==DIV_INS_ESFM) { + w->writeText("- FM parameters:\n"); + w->writeText(fmt::sprintf(" - ALG: %d\n",ins->fm.alg)); + w->writeText(fmt::sprintf(" - FB: %d\n",ins->fm.fb)); + w->writeText(fmt::sprintf(" - FMS: %d\n",ins->fm.fms)); + w->writeText(fmt::sprintf(" - AMS: %d\n",ins->fm.ams)); + w->writeText(fmt::sprintf(" - FMS2: %d\n",ins->fm.fms2)); + w->writeText(fmt::sprintf(" - AMS2: %d\n",ins->fm.ams2)); + w->writeText(fmt::sprintf(" - operators: %d\n",ins->fm.ops)); + w->writeText(fmt::sprintf(" - OPLL patch: %d\n",ins->fm.opllPreset)); + w->writeText(fmt::sprintf(" - fixed drum freq: %s\n",trueFalse[ins->fm.fixedDrums?1:0])); + w->writeText(fmt::sprintf(" - kick freq: %.4X\n",ins->fm.kickFreq)); + w->writeText(fmt::sprintf(" - snare/hat freq: %.4X\n",ins->fm.snareHatFreq)); + w->writeText(fmt::sprintf(" - tom/top freq: %.4X\n",ins->fm.tomTopFreq)); + + for (int j=0; jfm.ops; j++) { + DivInstrumentFM::Operator& op=ins->fm.op[j]; + + w->writeText(fmt::sprintf(" - operator %d:\n",j)); + w->writeText(fmt::sprintf(" - enabled: %s\n",trueFalse[op.enable?1:0])); + w->writeText(fmt::sprintf(" - AM: %d\n",op.am)); + w->writeText(fmt::sprintf(" - AR: %d\n",op.ar)); + w->writeText(fmt::sprintf(" - DR: %d\n",op.dr)); + w->writeText(fmt::sprintf(" - MULT: %d\n",op.mult)); + w->writeText(fmt::sprintf(" - RR: %d\n",op.rr)); + w->writeText(fmt::sprintf(" - SL: %d\n",op.sl)); + w->writeText(fmt::sprintf(" - TL: %d\n",op.tl)); + w->writeText(fmt::sprintf(" - DT2: %d\n",op.dt2)); + w->writeText(fmt::sprintf(" - RS: %d\n",op.rs)); + w->writeText(fmt::sprintf(" - DT: %d\n",op.dt)); + w->writeText(fmt::sprintf(" - D2R: %d\n",op.d2r)); + w->writeText(fmt::sprintf(" - SSG-EG: %d\n",op.ssgEnv)); + w->writeText(fmt::sprintf(" - DAM: %d\n",op.dam)); + w->writeText(fmt::sprintf(" - DVB: %d\n",op.dvb)); + w->writeText(fmt::sprintf(" - EGT: %d\n",op.egt)); + w->writeText(fmt::sprintf(" - KSL: %d\n",op.ksl)); + w->writeText(fmt::sprintf(" - SUS: %d\n",op.sus)); + w->writeText(fmt::sprintf(" - VIB: %d\n",op.vib)); + w->writeText(fmt::sprintf(" - WS: %d\n",op.ws)); + w->writeText(fmt::sprintf(" - KSR: %d\n",op.ksr)); + w->writeText(fmt::sprintf(" - TL volume scale: %d\n",op.kvs)); + } + } + + if (ins->type==DIV_INS_ESFM) { + w->writeText("- ESFM parameters:\n"); + w->writeText(fmt::sprintf(" - noise mode: %d\n",ins->esfm.noise)); + + for (int j=0; jfm.ops; j++) { + DivInstrumentESFM::Operator& opE=ins->esfm.op[j]; + + w->writeText(fmt::sprintf(" - operator %d:\n",j)); + w->writeText(fmt::sprintf(" - DL: %d\n",opE.delay)); + w->writeText(fmt::sprintf(" - OL: %d\n",opE.outLvl)); + w->writeText(fmt::sprintf(" - MI: %d\n",opE.modIn)); + w->writeText(fmt::sprintf(" - output left: %s\n",trueFalse[opE.left?1:0])); + w->writeText(fmt::sprintf(" - output right: %s\n",trueFalse[opE.right?1:0])); + w->writeText(fmt::sprintf(" - CT: %d\n",opE.ct)); + w->writeText(fmt::sprintf(" - DT: %d\n",opE.dt)); + w->writeText(fmt::sprintf(" - fixed frequency: %s\n",trueFalse[opE.fixed?1:0])); + } + } + + if (ins->type==DIV_INS_GB) { + w->writeText("- Game Boy parameters:\n"); + w->writeText(fmt::sprintf(" - volume: %d\n",ins->gb.envVol)); + w->writeText(fmt::sprintf(" - direction: %s\n",gbEnvDir[ins->gb.envDir?1:0])); + w->writeText(fmt::sprintf(" - length: %d\n",ins->gb.envLen)); + w->writeText(fmt::sprintf(" - sound length: %d\n",ins->gb.soundLen)); + w->writeText(fmt::sprintf(" - use software envelope: %s\n",trueFalse[ins->gb.softEnv?1:0])); + w->writeText(fmt::sprintf(" - always initialize: %s\n",trueFalse[ins->gb.softEnv?1:0])); + if (ins->gb.hwSeqLen>0) { + w->writeText(" - hardware sequence:\n"); + for (int j=0; jgb.hwSeqLen; j++) { + w->writeText(fmt::sprintf(" - %d: %.2X %.4X\n",j,ins->gb.hwSeq[j].cmd,ins->gb.hwSeq[j].data)); + } + } + } + + bool header=false; + writeTextMacro(w,ins->std.volMacro,"vol",header); + writeTextMacro(w,ins->std.arpMacro,"arp",header); + writeTextMacro(w,ins->std.dutyMacro,"duty",header); + writeTextMacro(w,ins->std.waveMacro,"wave",header); + writeTextMacro(w,ins->std.pitchMacro,"pitch",header); + writeTextMacro(w,ins->std.panLMacro,"panL",header); + writeTextMacro(w,ins->std.panRMacro,"panR",header); + writeTextMacro(w,ins->std.phaseResetMacro,"phaseReset",header); + writeTextMacro(w,ins->std.ex1Macro,"ex1",header); + writeTextMacro(w,ins->std.ex2Macro,"ex2",header); + writeTextMacro(w,ins->std.ex3Macro,"ex3",header); + writeTextMacro(w,ins->std.ex4Macro,"ex4",header); + writeTextMacro(w,ins->std.ex5Macro,"ex5",header); + writeTextMacro(w,ins->std.ex6Macro,"ex6",header); + writeTextMacro(w,ins->std.ex7Macro,"ex7",header); + writeTextMacro(w,ins->std.ex8Macro,"ex8",header); + writeTextMacro(w,ins->std.algMacro,"alg",header); + writeTextMacro(w,ins->std.fbMacro,"fb",header); + writeTextMacro(w,ins->std.fmsMacro,"fms",header); + writeTextMacro(w,ins->std.amsMacro,"ams",header); + + // TODO: the rest + w->writeText("\n"); + } + + w->writeText("\n# Wavetables\n\n"); + + for (int i=0; iwriteText(fmt::sprintf("- %d (%dx%d):",i,wave->len+1,wave->max+1)); + for (int j=0; j<=wave->len; j++) { + w->writeText(fmt::sprintf(" %d",wave->data[j])); + } + w->writeText("\n"); + } + + w->writeText("\n# Samples\n\n"); + + for (int i=0; iwriteText(fmt::sprintf("## %.2X: %s\n\n",i,sample->name)); + + w->writeText(fmt::sprintf("- format: %d\n",(int)sample->depth)); + w->writeText(fmt::sprintf("- data length: %d\n",sample->getCurBufLen())); + w->writeText(fmt::sprintf("- samples: %d\n",sample->samples)); + w->writeText(fmt::sprintf("- rate: %d\n",sample->centerRate)); + w->writeText(fmt::sprintf("- compat rate: %d\n",sample->rate)); + w->writeText(fmt::sprintf("- loop: %s\n",trueFalse[sample->loop?1:0])); + if (sample->loop) { + w->writeText(fmt::sprintf(" - start: %d\n",sample->loopStart)); + w->writeText(fmt::sprintf(" - end: %d\n",sample->loopEnd)); + w->writeText(fmt::sprintf(" - mode: %s\n",sampleLoopModes[sample->loopMode&3])); + } + w->writeText(fmt::sprintf("- BRR emphasis: %s\n",trueFalse[sample->brrEmphasis?1:0])); + w->writeText(fmt::sprintf("- dither: %s\n",trueFalse[sample->dither?1:0])); + + // TODO' render matrix + unsigned char* buf=(unsigned char*)sample->getCurBuf(); + unsigned int bufLen=sample->getCurBufLen(); + w->writeText("\n```"); + for (unsigned int i=0; iwriteText(fmt::sprintf("\n%.8X:",i)); + w->writeText(fmt::sprintf(" %.2X",buf[i])); + } + w->writeText("\n```\n\n"); + } + + w->writeText("\n# Subsongs\n\n"); + + for (size_t i=0; iwriteText(fmt::sprintf("## %d: %s\n\n",(int)i,s->name)); + + w->writeText(fmt::sprintf("- tick rate: %g\n",s->hz)); + w->writeText(fmt::sprintf("- speeds:")); + for (int j=0; jspeeds.len; j++) { + w->writeText(fmt::sprintf(" %d",s->speeds.val[j])); + } + w->writeText("\n"); + w->writeText(fmt::sprintf("- virtual tempo: %d/%d\n",s->virtualTempoN,s->virtualTempoD)); + w->writeText(fmt::sprintf("- time base: %d\n",s->timeBase)); + w->writeText(fmt::sprintf("- pattern length: %d\n",s->patLen)); + w->writeText(fmt::sprintf("\norders:\n```\n")); + + for (int j=0; jordersLen; j++) { + w->writeText(fmt::sprintf("%.2X |",j)); + for (int k=0; kwriteText(fmt::sprintf(" %.2X",s->orders.ord[k][j])); + } + w->writeText("\n"); + } + w->writeText("```\n\n## Patterns\n\n"); + + if (separatePatterns) { + w->writeText("TODO: separate patterns\n\n"); + } else { + for (int j=0; jordersLen; j++) { + w->writeText(fmt::sprintf("----- ORDER %.2X\n",j)); + + for (int k=0; kpatLen; k++) { + w->writeText(fmt::sprintf("%.2X ",k)); + + for (int l=0; lpat[l].getPattern(s->orders.ord[l][j],false); + + int note=p->data[k][0]; + int octave=p->data[k][1]; + + if (note==0 && octave==0) { + w->writeText("|... "); + } else if (note==100) { + w->writeText("|OFF "); + } else if (note==101) { + w->writeText("|=== "); + } else if (note==102) { + w->writeText("|REL "); + } else if ((octave>9 && octave<250) || note>12) { + w->writeText("|??? "); + } else { + if (octave>=128) octave-=256; + if (note>11) { + note-=12; + octave++; + } + w->writeText(fmt::sprintf("|%s%d ",(octave<0)?notesNegative[note]:notes[note],(octave<0)?(-octave):octave)); + } + + if (p->data[k][2]==-1) { + w->writeText(".. "); + } else { + w->writeText(fmt::sprintf("%.2X ",p->data[k][2]&0xff)); + } + + if (p->data[k][3]==-1) { + w->writeText(".."); + } else { + w->writeText(fmt::sprintf("%.2X",p->data[k][3]&0xff)); + } + + for (int m=0; mpat[l].effectCols; m++) { + if (p->data[k][4+(m<<1)]==-1) { + w->writeText(" .."); + } else { + w->writeText(fmt::sprintf(" %.2X",p->data[k][4+(m<<1)]&0xff)); + } + if (p->data[k][5+(m<<1)]==-1) { + w->writeText(".."); + } else { + w->writeText(fmt::sprintf("%.2X",p->data[k][5+(m<<1)]&0xff)); + } + } + } + + w->writeText("\n"); + } + } + } + + } + + saveLock.unlock(); + return w; +} diff --git a/src/engine/platform/nds.cpp b/src/engine/platform/nds.cpp new file mode 100644 index 000000000..b52980198 --- /dev/null +++ b/src/engine/platform/nds.cpp @@ -0,0 +1,594 @@ +/** + * 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 "nds.h" +#include "../engine.h" +#include "../../ta-log.h" +#include + +#define CHIP_DIVIDER 32 +#define CLOCK_DIVIDER 512 // for match to output rate + +#define rRead8(a) (nds.read8(a)) +#define rWrite8(a,v) {if(!skipRegisterWrites) {nds.write8((a),(v)); regPool[(a)]=(v); if(dumpWrites) addWrite((a),(v)); }} +#define rWrite16(a,v) { \ + if(!skipRegisterWrites) { \ + nds.write16((a)>>1,(v)); \ + regPool[(a)+0]=(v)&0xff; \ + regPool[(a)+1]=((v)>>8)&0xff; \ + if(dumpWrites) addWrite((a)+0,(v)&0xff); \ + if(dumpWrites) addWrite((a)+1,((v)>>8)&0xff); \ + } \ +} + +#define rWrite32(a,v) { \ + if(!skipRegisterWrites) { \ + nds.write32((a)>>2,(v)); \ + regPool[(a)+0]=(v)&0xff; \ + regPool[(a)+1]=((v)>>8)&0xff; \ + regPool[(a)+2]=((v)>>16)&0xff; \ + regPool[(a)+3]=((v)>>24)&0xff; \ + if(dumpWrites) addWrite((a)+0,(v)&0xff); \ + if(dumpWrites) addWrite((a)+1,((v)>>8)&0xff); \ + if(dumpWrites) addWrite((a)+2,((v)>>16)&0xff); \ + if(dumpWrites) addWrite((a)+3,((v)>>24)&0xff); \ + } \ +} + +const char* regCheatSheetNDS[]={ + "CHx_Control", "000+x*10", + "CHx_Start", "004+x*10", + "CHx_Freq", "008+x*10", + "CHx_LoopStart", "00A+x*10", + "CHx_Length", "00C+x*10", + "Control", "100", + "Bias", "104", + "CAPx_Control", "108+x*1", + "CAPx_Dest", "110+x*8", + "CAPx_Length", "114+x*8", + NULL +}; + +const char** DivPlatformNDS::getRegisterSheet() { + return regCheatSheetNDS; +} + +void DivPlatformNDS::acquire(short** buf, size_t len) { + for (size_t i=0; i32767) lout=32767; + if (lout<-32768) lout=-32768; + if (rout>32767) rout=32767; + if (rout<-32768) rout=-32768; + buf[0][i]=lout; + buf[1][i]=rout; + + for (int i=0; i<16; i++) { + oscBuf[i]->data[oscBuf[i]->needle++]=(nds.chan_lout(i)+nds.chan_rout(i))>>1; + } + } +} + +u8 DivPlatformNDS::read_byte(u32 addr) { + if (addrcalcArp(chan[i].note,chan[i].std.arp.val)); + } + 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 ((i>=8) && (i<14)) { + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; + if ((!chan[i].pcm)) { // pulse + rWrite8(0x03+i*16,(rRead8(0x03+i*16)&0xe8)|(chan[i].duty&7)); + } + } + } + if (chan[i].std.panL.had) { // panning + chan[i].panning=0x40+chan[i].std.panL.val; + rWrite8(0x02+i*16,chan[i].panning); + } + if (chan[i].std.phaseReset.had) { + if ((chan[i].std.phaseReset.val==1) && chan[i].active) { + chan[i].audPos=0; + chan[i].setPos=true; + if ((rRead8(0x03+i*16)&0x80)==0) + chan[i].busy=true; + } + } + 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) { + unsigned char ctrl=0; + if (chan[i].pcm || i<8) { + DivSample* s=parent->getSample(chan[i].sample); + switch (s->depth) { + case DIV_SAMPLE_DEPTH_IMA_ADPCM: ctrl=0x40; break; + case DIV_SAMPLE_DEPTH_8BIT: ctrl=0x00; break; + case DIV_SAMPLE_DEPTH_16BIT: ctrl=0x20; break; + default: break; + } + double off=(s->centerRate>=1)?(8363.0/(double)s->centerRate):1.0; + chan[i].freq=0x10000-(off*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 (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>65535) chan[i].freq=65535; + if ((!chan[i].keyOn) && ((rRead8(0x03+i*16)&0x80)==0)) + chan[i].busy=false; + ctrl|=(chan[i].busy?0x80:0)|((s->isLoopable())?0x08:0x10); + if (chan[i].keyOn) { + unsigned int start=0; + int loopStart=0; + int loopEnd=0; + int end=0; + if (chan[i].sample>=0 && chan[i].samplesong.sampleLen) { + start=sampleOff[chan[i].sample]; + end=s->getCurBufLen()/4; + } + if (chan[i].audPos>0) { + switch (s->depth) { + case DIV_SAMPLE_DEPTH_IMA_ADPCM: start+=chan[i].audPos/2; end-=(chan[i].audPos/8); break; + case DIV_SAMPLE_DEPTH_8BIT: start+=chan[i].audPos; end-=(chan[i].audPos/4); break; + case DIV_SAMPLE_DEPTH_16BIT: start+=chan[i].audPos*2; end-=(chan[i].audPos/2); break; + default: break; + } + } + if (s->isLoopable()) { + if (chan[i].audPos>0) { + switch (s->depth) { + case DIV_SAMPLE_DEPTH_IMA_ADPCM: + loopStart=(s->loopStart-chan[i].audPos)/8; + loopEnd=(s->loopEnd-s->loopStart)/8; + if (chan[i].audPos>(unsigned int)s->loopStart) { + loopStart=0; + loopEnd-=(chan[i].audPos-s->loopStart)/8; + } + break; + case DIV_SAMPLE_DEPTH_8BIT: + loopStart=(s->loopStart-chan[i].audPos)/4; + loopEnd=(s->loopEnd-s->loopStart)/4; + if (chan[i].audPos>(unsigned int)s->loopStart) { + loopStart=0; + loopEnd-=(chan[i].audPos-s->loopStart)/4; + } + break; + case DIV_SAMPLE_DEPTH_16BIT: + loopStart=(s->loopStart-chan[i].audPos)/2; + loopEnd=(s->loopEnd-s->loopStart)/2; + if (chan[i].audPos>(unsigned int)s->loopStart) { + loopStart=0; + loopEnd-=(chan[i].audPos-s->loopStart)/2; + } + break; + default: break; + } + } else { + switch (s->depth) { + case DIV_SAMPLE_DEPTH_IMA_ADPCM: loopStart=s->loopStart/8; loopEnd=(s->loopEnd-s->loopStart)/8; break; + case DIV_SAMPLE_DEPTH_8BIT: loopStart=s->loopStart/4; loopEnd=(s->loopEnd-s->loopStart)/4; break; + case DIV_SAMPLE_DEPTH_16BIT: loopStart=s->loopStart/2; loopEnd=(s->loopEnd-s->loopStart)/2; break; + default: break; + } + } + loopEnd=CLAMP(loopEnd,0,0x3fffff); + loopStart=CLAMP(loopStart,0,0xffff); + rWrite16(0x0a+i*16,loopStart); + rWrite32(0x0c+i*16,loopEnd); + } else { + end=CLAMP(end,0,0x3fffff); + rWrite16(0x0a+i*16,0); + rWrite32(0x0c+i*16,end&0x3fffff); + } + rWrite8(0x03+i*16,ctrl&~0x80); // force keyoff first + rWrite32(0x04+i*16,start&0x7fffffc); + } + } else { + chan[i].freq=0x10000-(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,8)); + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>65535) chan[i].freq=65535; + ctrl=(chan[i].active?0xe8:0)|(chan[i].duty&7); + rWrite8(0x03+i*16,ctrl&~0x80); // force keyoff first + } + if (!chan[i].std.vol.had) { + chan[i].outVol=chan[i].vol; + writeOutVol(i); + } + chan[i].keyOn=false; + if (chan[i].keyOff) { + chan[i].keyOff=false; + } + if (chan[i].freqChanged) { + rWrite16(0x08+i*16,chan[i].freq&0xffff); + chan[i].freqChanged=false; + } + rWrite8(0x03+i*16,ctrl); + } + } +} + +int DivPlatformNDS::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_NDS); + if (ins->type==DIV_INS_AMIGA || ins->amiga.useSample || (c.chan<8)) { + chan[c.chan].pcm=true; + } + if (chan[c.chan].pcm || (c.chan<8)) { + chan[c.chan].macroVolMul=ins->type==DIV_INS_AMIGA?64:127; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].sample=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; + } + if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { + chan[c.chan].sample=-1; + } + } else { + chan[c.chan].macroVolMul=127; + } + 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].busy=true; + chan[c.chan].keyOn=true; + chan[c.chan].macroInit(ins); + if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].sample=-1; + chan[c.chan].active=false; + chan[c.chan].busy=false; + chan[c.chan].keyOff=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; + } + break; + case DIV_CMD_ADPCMA_GLOBAL_VOLUME: { + if (globalVolume!=(c.value&0x7f)) { + globalVolume=c.value&0x7f; + rWrite32(0x100,0x8000|globalVolume); + } + 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; + writeOutVol(c.chan); + } + } + 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_PANNING: + chan[c.chan].panning=MIN(parent->convertPanSplitToLinearLR(c.value,c.value2,127),127); + rWrite8(0x02+c.chan*16,chan[c.chan].panning); + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_PERIODIC(c.value2+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_STD_NOISE_MODE: + if ((c.chan>=8) && (c.chan<14) && (!chan[c.chan].pcm)) { // pulse + chan[c.chan].duty=c.value; + rWrite8(0x03+c.chan*16,(rRead8(0x03+c.chan*16)&0xe8)|(chan[c.chan].duty&7)); + } + break; + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+chan[c.chan].sampleNoteDelta+((HACKY_LEGATO_MESS)?(chan[c.chan].std.arp.val-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].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_NDS)); + } + 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_SAMPLE_POS: + if (chan[c.chan].pcm || (c.chan<8)) { + chan[c.chan].audPos=c.value; + chan[c.chan].setPos=true; + } + break; + case DIV_CMD_GET_VOLMAX: + return 127; + 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 DivPlatformNDS::writeOutVol(int ch) { + unsigned char val=isMuted[ch]?0:chan[ch].outVol; + rWrite8(0x00+ch*16,val); +} + +void DivPlatformNDS::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + writeOutVol(ch); +} + +void DivPlatformNDS::forceIns() { + for (int i=0; i<16; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + chan[i].sample=-1; + + rWrite8(0x02+i*16,chan[i].panning); + } +} + +void* DivPlatformNDS::getChanState(int ch) { + return &chan[ch]; +} + +DivMacroInt* DivPlatformNDS::getChanMacroInt(int ch) { + return &chan[ch].std; +} + +unsigned short DivPlatformNDS::getPan(int ch) { + return parent->convertPanLinearToSplit(chan[ch].panning,8,127); +} + +DivDispatchOscBuffer* DivPlatformNDS::getOscBuffer(int ch) { + return oscBuf[ch]; +} + +void DivPlatformNDS::reset() { + memset(regPool,0,288); + nds.reset(); + globalVolume=0x7f; + rWrite32(0x100,0x8000|globalVolume); // enable keyon + rWrite32(0x104,0x200); // initialize bias + for (int i=0; i<16; i++) { + chan[i]=DivPlatformNDS::Channel(); + chan[i].std.setEngine(parent); + rWrite32(0x00+i*16,0x40007f); + } +} + +int DivPlatformNDS::getOutputCount() { + return 2; +} + +void DivPlatformNDS::notifyInsChange(int ins) { + for (int i=0; i<16; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } +} + +void DivPlatformNDS::notifyWaveChange(int wave) { + // TODO when wavetables are added + // TODO they probably won't be added unless the samples reside in RAM +} + +void DivPlatformNDS::notifyInsDeletion(void* ins) { + for (int i=0; i<16; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformNDS::poke(unsigned int addr, unsigned short val) { + rWrite8(addr,val); +} + +void DivPlatformNDS::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite8(i.addr,i.val); +} + +unsigned char* DivPlatformNDS::getRegisterPool() { + return regPool; +} + +int DivPlatformNDS::getRegisterPoolSize() { + return 288; +} + +float DivPlatformNDS::getPostAmp() { + return 1.0f; +} + +const void* DivPlatformNDS::getSampleMem(int index) { + return index == 0 ? sampleMem : NULL; +} + +size_t DivPlatformNDS::getSampleMemCapacity(int index) { + return index == 0 ? (isDSi?16777216:4194304) : 0; +} + +size_t DivPlatformNDS::getSampleMemUsage(int index) { + return index == 0 ? sampleMemLen : 0; +} + +bool DivPlatformNDS::isSampleLoaded(int index, int sample) { + if (index!=0) return false; + if (sample<0 || sample>255) return false; + return sampleLoaded[sample]; +} + +const DivMemoryComposition* DivPlatformNDS::getMemCompo(int index) { + if (index!=0) return NULL; + return &memCompo; +} + +void DivPlatformNDS::renderSamples(int sysID) { + memset(sampleMem,0,16777216); + memset(sampleOff,0,256*sizeof(unsigned int)); + memset(sampleLoaded,0,256*sizeof(bool)); + + memCompo=DivMemoryComposition(); + memCompo.name="Main Memory"; + + size_t memPos=0; + for (int i=0; isong.sampleLen; i++) { + DivSample* s=parent->song.sample[i]; + if (!s->renderOn[0][sysID]) { + sampleOff[i]=0; + continue; + } + + int length=MIN(16777212,s->getCurBufLen()); + unsigned char* src=(unsigned char*)s->getCurBuf(); + int actualLength=MIN((int)(getSampleMemCapacity()-memPos),length); + if (actualLength>0) { + memcpy(&sampleMem[memPos],src,actualLength); + sampleOff[i]=memPos; + memCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_SAMPLE,"Sample",i,memPos,memPos+actualLength)); + memPos+=actualLength; + } + if (actualLengthrate=rate; + } +} + +int DivPlatformNDS::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + + for (int i=0; i<16; i++) { + isMuted[i]=false; + oscBuf[i]=new DivDispatchOscBuffer; + } + sampleMem=new unsigned char[16777216]; + sampleMemLen=0; + nds.reset(); + setFlags(flags); + reset(); + + return 16; +} + +void DivPlatformNDS::quit() { + delete[] sampleMem; + for (int i=0; i<16; i++) { + delete oscBuf[i]; + } +} diff --git a/src/engine/platform/nds.h b/src/engine/platform/nds.h new file mode 100644 index 000000000..ec4a84796 --- /dev/null +++ b/src/engine/platform/nds.h @@ -0,0 +1,104 @@ +/** + * 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 _NDS_H +#define _NDS_H + +#include "../dispatch.h" +#include "sound/nds.hpp" + +using namespace nds_sound_emu; + +class DivPlatformNDS: public DivDispatch, public nds_sound_intf { + struct Channel: public SharedChannel { + unsigned int audPos; + int sample, wave; + int panning, duty; + bool setPos, pcm, busy; + int macroVolMul; + Channel(): + SharedChannel(127), + audPos(0), + sample(-1), + wave(-1), + panning(64), + duty(0), + setPos(false), + pcm(false), + busy(false), + macroVolMul(64) {} + }; + Channel chan[16]; + DivDispatchOscBuffer* oscBuf[16]; + bool isMuted[16]; + bool isDSi; + int globalVolume; + unsigned int sampleOff[256]; + bool sampleLoaded[256]; + + unsigned char* sampleMem; + size_t sampleMemLen; + nds_sound_t nds; + DivMemoryComposition memCompo; + unsigned char regPool[288]; + friend void putDispatchChip(void*,int); + friend void putDispatchChan(void*,int,int); + + public: + virtual u8 read_byte(u32 addr) override; + virtual void write_byte(u32 addr, u8 data) override; + + virtual void acquire(short** buf, size_t len) override; + virtual int dispatch(DivCommand c) override; + virtual void* getChanState(int chan) override; + virtual DivMacroInt* getChanMacroInt(int ch) override; + virtual unsigned short getPan(int chan) override; + virtual DivDispatchOscBuffer* getOscBuffer(int chan) override; + virtual unsigned char* getRegisterPool() override; + virtual int getRegisterPoolSize() override; + virtual void reset() override; + virtual void forceIns() override; + virtual void tick(bool sysTick=true) override; + virtual void muteChannel(int ch, bool mute) override; + virtual float getPostAmp() override; + virtual int getOutputCount() override; + virtual void notifyInsChange(int ins) override; + virtual void notifyWaveChange(int wave) override; + virtual void notifyInsDeletion(void* ins) override; + virtual void poke(unsigned int addr, unsigned short val) override; + virtual void poke(std::vector& wlist) override; + virtual const char** getRegisterSheet() override; + virtual const void* getSampleMem(int index = 0) override; + virtual size_t getSampleMemCapacity(int index = 0) override; + virtual size_t getSampleMemUsage(int index = 0) override; + virtual bool isSampleLoaded(int index, int sample) override; + virtual const DivMemoryComposition* getMemCompo(int index) override; + virtual void renderSamples(int chipID) override; + virtual void setFlags(const DivConfig& flags) override; + virtual int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags) override; + virtual void quit() override; + DivPlatformNDS(): + DivDispatch(), + nds_sound_intf(), + nds(*this) {} + private: + void writeOutVol(int ch); +}; + +#endif diff --git a/src/engine/platform/sound/nds.cpp b/src/engine/platform/sound/nds.cpp new file mode 100644 index 000000000..67c45a884 --- /dev/null +++ b/src/engine/platform/sound/nds.cpp @@ -0,0 +1,634 @@ +/* + +============================================================================ + +NDS sound emulator +by cam900 + +This file is licensed under zlib license. + +============================================================================ + +zlib License + +(C) 2024-present cam900 and contributors + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +============================================================================ +TODO: +- needs to further verifications from real hardware + +Tech info: https://problemkaputt.de/gbatek.htm + +*/ + +#include "nds.hpp" + +namespace nds_sound_emu +{ + void nds_sound_t::reset() + { + for (channel_t &elem : m_channel) + elem.reset(); + for (capture_t &elem : m_capture) + elem.reset(); + + m_control = 0; + m_bias = 0; + m_loutput = 0; + m_routput = 0; + } + + void nds_sound_t::tick(s32 cycle) + { + m_loutput = m_routput = (m_bias & 0x3ff); + if (!enable()) + return; + + // mix outputs + s32 lmix = 0, rmix = 0; + for (u8 i = 0; i < 16; i++) + { + channel_t &channel = m_channel[i]; + channel.update(cycle); + // bypass mixer + if (((i == 1) && (mix_ch1())) || ((i == 3) && (mix_ch3()))) + continue; + + lmix += channel.loutput(); + rmix += channel.routput(); + } + + // send mixer output to capture + m_capture[0].update(lmix, cycle); + m_capture[1].update(rmix, cycle); + + // select left/right output source + switch (lout_from()) + { + case 0: // left mixer + break; + case 1: // channel 1 + lmix = m_channel[1].loutput(); + break; + case 2: // channel 3 + lmix = m_channel[3].loutput(); + break; + case 3: // channel 1 + 3 + lmix = m_channel[1].loutput() + m_channel[3].loutput(); + break; + } + + switch (rout_from()) + { + case 0: // right mixer + break; + case 1: // channel 1 + rmix = m_channel[1].routput(); + break; + case 2: // channel 3 + rmix = m_channel[3].routput(); + break; + case 3: // channel 1 + 3 + rmix = m_channel[1].routput() + m_channel[3].routput(); + break; + } + + // adjust master volume + lmix = (lmix * mvol()) >> 13; + rmix = (rmix * mvol()) >> 13; + + // add bias and clip output + m_loutput = clamp((lmix + (m_bias & 0x3ff)), 0, 0x3ff); + m_routput = clamp((rmix + (m_bias & 0x3ff)), 0, 0x3ff); + } + + u8 nds_sound_t::read8(u32 addr) + { + return bitfield(read32(addr >> 2), bitfield(addr, 0, 2) << 3, 8); + } + + u16 nds_sound_t::read16(u32 addr) + { + return bitfield(read32(addr >> 1), bitfield(addr, 0) << 4, 16); + } + + u32 nds_sound_t::read32(u32 addr) + { + addr <<= 2; // word address + + switch (addr & 0x100) + { + case 0x000: + if ((addr & 0xc) == 0) + return m_channel[bitfield(addr, 4, 4)].control(); + break; + case 0x100: + switch (addr & 0xff) + { + case 0x00: + return m_control; + case 0x04: + return m_bias; + case 0x08: + return m_capture[0].control() | (m_capture[1].control() << 8); + case 0x10: + case 0x18: + return m_capture[bitfield(addr, 3)].dstaddr(); + default: + break; + } + break; + } + return 0; + } + + void nds_sound_t::write8(u32 addr, u8 data) + { + const u8 bit = bitfield(addr, 0, 2); + const u32 in = u32(data) << (bit << 3); + const u32 in_mask = 0xff << (bit << 3); + write32(addr >> 2, in, in_mask); + } + + void nds_sound_t::write16(u32 addr, u16 data, u16 mask) + { + const u8 bit = bitfield(addr, 0); + const u32 in = u32(data) << (bit << 4); + const u32 in_mask = u32(mask) << (bit << 4); + write32(addr >> 1, in, in_mask); + } + + void nds_sound_t::write32(u32 addr, u32 data, u32 mask) + { + addr <<= 2; // word address + + switch (addr & 0x100) + { + case 0x000: + m_channel[bitfield(addr, 4, 4)].write(bitfield(addr, 2, 2), data, mask); + break; + case 0x100: + switch (addr & 0xff) + { + case 0x00: + m_control = (m_control & ~mask) | (data & mask); + break; + case 0x04: + mask &= 0x3ff; + m_bias = (m_bias & ~mask) | (data & mask); + break; + case 0x08: + if (bitfield(mask, 0, 8)) + m_capture[0].control_w(data & 0xff); + if (bitfield(mask, 8, 8)) + m_capture[1].control_w((data >> 8) & 0xff); + break; + case 0x10: + case 0x14: + case 0x18: + case 0x1c: + m_capture[bitfield(addr, 3)].addrlen_w(bitfield(addr, 2), data, mask); + break; + default: + break; + } + break; + } + } + + // channels + void nds_sound_t::channel_t::reset() + { + m_control = 0; + m_sourceaddr = 0; + m_freq = 0; + m_loopstart = 0; + m_length = 0; + + m_playing = false; + m_adpcm_out = 0; + m_adpcm_index = 0; + m_prev_adpcm_out = 0; + m_prev_adpcm_index = 0; + m_cur_addr = 0; + m_cur_state = 0; + m_cur_bitaddr = 0; + m_delay = 0; + m_sample = 0; + m_lfsr = 0x7fff; + m_lfsr_out = 0x7fff; + m_counter = 0x10000; + m_output = 0; + m_loutput = 0; + m_routput = 0; + } + + void nds_sound_t::channel_t::write(u32 offset, u32 data, u32 mask) + { + const u32 old = m_control; + switch (offset & 3) + { + case 0: // Control/Status + m_control = (m_control & ~mask) | (data & mask); + if (bitfield(old ^ m_control, 31)) + { + if (busy()) + keyon(); + else if (!busy()) + keyoff(); + } + // reset hold flag + if (!m_playing && !hold()) + { + m_sample = m_lfsr_out = 0; + m_output = m_loutput = m_routput = 0; + } + break; + case 1: // Source address + mask &= 0x7ffffff; + m_sourceaddr = (m_sourceaddr & ~mask) | (data & mask); + break; + case 2: // Frequency, Loopstart + if (bitfield(mask, 0, 16)) + m_freq = (m_freq & bitfield(~mask, 0, 16)) | (bitfield(data & mask, 0, 16)); + if (bitfield(mask, 16, 16)) + m_loopstart = (m_loopstart & bitfield(~mask, 16, 16)) | (bitfield(data & mask, 16, 16)); + break; + case 3: // Length + mask &= 0x3fffff; + m_length = (m_length & ~mask) | (data & mask); + break; + } + } + + void nds_sound_t::channel_t::keyon() + { + if (!m_playing) + { + m_playing = true; + m_delay = format() == 2 ? 11 : 3; // 3 (11 for ADPCM) delay for playing sample + m_cur_bitaddr = m_cur_addr = 0; + m_cur_state = (format() == 2) ? STATE_ADPCM_LOAD : ((m_loopstart == 0) ? STATE_POST_LOOP : STATE_PRE_LOOP); + m_counter = 0x10000; + m_sample = 0; + m_lfsr_out = 0x7fff; + m_lfsr = 0x7fff; + } + } + + + void nds_sound_t::channel_t::keyoff() + { + if (m_playing) + { + if (busy()) + m_control &= ~(1 << 31); + if (!hold()) + { + m_sample = m_lfsr_out = 0; + m_output = m_loutput = m_routput = 0; + } + + m_playing = false; + } + } + + void nds_sound_t::channel_t::update(s32 cycle) + { + if (m_playing) + { + // get output + fetch(); + m_counter -= cycle; + while (m_counter <= m_freq) + { + // advance + advance(); + m_counter += 0x10000 - m_freq; + } + m_output = (m_sample * volume()) >> (7 + voldiv()); + m_loutput = (m_output * lvol()) >> 7; + m_routput = (m_output * rvol()) >> 7; + } + } + + void nds_sound_t::channel_t::fetch() + { + if (m_playing) + { + // fetch samples + switch (format()) + { + case 0: // PCM8 + m_sample = s16(m_host.m_intf.read_byte(addr()) << 8); + break; + case 1: // PCM16 + m_sample = m_host.m_intf.read_word(addr()); + break; + case 2: // ADPCM + m_sample = m_cur_state == STATE_ADPCM_LOAD ? 0 : m_adpcm_out; + break; + case 3: // PSG or Noise + m_sample = 0; + if (m_psg) // psg + m_sample = (duty() == 7) ? -0x8000 : ((m_cur_bitaddr < s32(u32(7) - duty())) ? -0x8000 : 0x7fff); + else if (m_noise) // noise + m_sample = m_lfsr_out; + break; + } + } + + // apply delay + if (format() != 3 && m_delay > 0) + m_sample = 0; + } + + void nds_sound_t::channel_t::advance() + { + if (m_playing) + { + // advance bit address + switch (format()) + { + case 0: // PCM8 + m_cur_bitaddr += 8; + break; + case 1: // PCM16 + m_cur_bitaddr += 16; + break; + case 2: // ADPCM + if (m_cur_state == STATE_ADPCM_LOAD) // load ADPCM data + { + if (m_cur_bitaddr == 0) + m_prev_adpcm_out = m_adpcm_out = m_host.m_intf.read_word(addr()); + if (m_cur_bitaddr == 16) + m_prev_adpcm_index = m_adpcm_index = clamp(m_host.m_intf.read_byte(addr()) & 0x7f, 0, 88); + } + else // decode ADPCM + { + const u8 input = bitfield(m_host.m_intf.read_byte(addr()), m_cur_bitaddr & 4, 4); + s32 diff = ((bitfield(input, 0, 3) * 2 + 1) * m_host.adpcm_diff_table[m_adpcm_index] / 8); + if (bitfield(input, 3)) diff = -diff; + m_adpcm_out = clamp(m_adpcm_out + diff, -0x8000, 0x7fff); + m_adpcm_index = clamp(m_adpcm_index + m_host.adpcm_index_table[bitfield(input, 0, 3)], 0, 88); + } + m_cur_bitaddr += 4; + break; + case 3: // PSG or Noise + if (m_psg) // psg + m_cur_bitaddr = (m_cur_bitaddr + 1) & 7; + else if (m_noise) // noise + { + if (bitfield(m_lfsr, 1)) + { + m_lfsr = (m_lfsr >> 1) ^ 0x6000; + m_lfsr_out = -0x8000; + } + else + { + m_lfsr >>= 1; + m_lfsr_out = 0x7fff; + } + } + break; + } + + // address update + if (format() != 3) + { + // adjust delay + m_delay--; + + // update address, loop + while (m_cur_bitaddr >= 32) + { + // already loaded? + if (format() == 2 && m_cur_state == STATE_ADPCM_LOAD) + { + m_cur_state = m_loopstart == 0 ? STATE_POST_LOOP : STATE_PRE_LOOP; + } + m_cur_addr++; + if (m_cur_state == STATE_PRE_LOOP && m_cur_addr >= m_loopstart) + { + m_cur_state = STATE_POST_LOOP; + m_cur_addr = 0; + if (format() == 2) + { + m_prev_adpcm_out = m_adpcm_out; + m_prev_adpcm_index = m_adpcm_index; + } + } + else if (m_cur_state == STATE_POST_LOOP && m_cur_addr >= m_length) + { + switch (repeat()) + { + case 0: // manual; not correct? + case 2: // one-shot + case 3: // prohibited + keyoff(); + break; + case 1: // loop infinitely + if (format() == 2) + { + if (m_loopstart == 0) // reload ADPCM + { + m_cur_state = STATE_ADPCM_LOAD; + } + else // restore + { + m_adpcm_out = m_prev_adpcm_out; + m_adpcm_index = m_prev_adpcm_index; + } + } + m_cur_addr = 0; + break; + } + } + m_cur_bitaddr -= 32; + } + } + } + } + + // capture + void nds_sound_t::capture_t::reset() + { + m_control = 0; + m_dstaddr = 0; + m_length = 0; + + m_counter = 0x10000; + m_cur_addr = 0; + m_cur_waddr = 0; + m_cur_bitaddr = 0; + m_enable = false; + } + + void nds_sound_t::capture_t::control_w(u8 data) + { + const u8 old = m_control; + m_control = data; + if (bitfield(old ^ m_control, 7)) + { + if (busy()) + capture_on(); + else if (!busy()) + capture_off(); + } + } + + void nds_sound_t::capture_t::addrlen_w(u32 offset, u32 data, u32 mask) + { + switch (offset & 1) + { + case 0: // Destination Address + mask &= 0x7ffffff; + m_dstaddr = (m_dstaddr & ~mask) | (data & mask); + break; + case 1: // Buffer Length + mask &= 0xffff; + m_length = (m_length & ~mask) | (data & mask); + break; + } + } + + void nds_sound_t::capture_t::update(s32 mix, s32 cycle) + { + if (m_enable) + { + s32 inval = 0; + // get inputs + // TODO: hardware bugs aren't emulated, add mode behavior not verified + if (addmode()) + inval = get_source() ? m_input.output() + m_output.output() : mix; + else + inval = get_source() ? m_input.output() : mix; + + // clip output + inval = clamp(inval, -0x8000, 0x7fff); + + // update counter + m_counter -= cycle; + while (m_counter <= m_output.freq()) + { + // write to memory; TODO: verify write behavior + if (format()) // 8 bit output + { + m_fifo[m_fifo_head & 7].write_byte(m_cur_bitaddr & 0x18, (inval >> 8) & 0xff); + m_cur_bitaddr += 8; + } + else + { + m_fifo[m_fifo_head & 7].write_word(m_cur_bitaddr & 0x10, inval & 0xffff); + m_cur_bitaddr += 16; + } + + // update address + while (m_cur_bitaddr >= 32) + { + // clear FIFO empty flag + m_fifo_empty = false; + + // advance FIFO head position + m_fifo_head = (m_fifo_head + 1) & 7; + if ((m_fifo_head & fifo_mask()) == (m_fifo_tail & fifo_mask())) + m_fifo_full = true; + + // update loop + if (++m_cur_addr >= m_length) + { + if (repeat()) + m_cur_addr = 0; + else + capture_off(); + } + + if (m_fifo_full) + { + // execute FIFO + fifo_write(); + + // check repeat + if (m_cur_waddr >= m_length && repeat()) + m_cur_waddr = 0; + } + + m_cur_bitaddr -= 32; + } + m_counter += 0x10000 - m_output.freq(); + } + } + } + + bool nds_sound_t::capture_t::fifo_write() + { + if (m_fifo_empty) + return true; + + // clear FIFO full flag + m_fifo_full = false; + + // write FIFO data to memory + m_host.m_intf.write_dword(waddr(), m_fifo[m_fifo_tail].data()); + m_cur_waddr++; + + // advance FIFO tail position + m_fifo_tail = (m_fifo_tail + 1) & 7; + if ((m_fifo_head & fifo_mask()) == (m_fifo_tail & fifo_mask())) + m_fifo_empty = true; + + return m_fifo_empty; + } + + void nds_sound_t::capture_t::capture_on() + { + if (!m_enable) + { + m_enable = true; + + // reset address + m_cur_bitaddr = 0; + m_cur_addr = m_cur_waddr = 0; + m_counter = 0x10000; + + // reset FIFO + m_fifo_head = m_fifo_tail = 0; + m_fifo_empty = true; + m_fifo_full = false; + } + } + + void nds_sound_t::capture_t::capture_off() + { + if (m_enable) + { + // flush FIFO + while (m_cur_waddr < m_length) + { + if (fifo_write()) + break; + } + + m_enable = false; + if (busy()) + m_control &= ~(1 << 7); + } + } + +}; // namespace nds_sound_emu \ No newline at end of file diff --git a/src/engine/platform/sound/nds.hpp b/src/engine/platform/sound/nds.hpp new file mode 100644 index 000000000..8f67f436a --- /dev/null +++ b/src/engine/platform/sound/nds.hpp @@ -0,0 +1,415 @@ +/* + +============================================================================ + +NDS sound emulator +by cam900 + +This file is licensed under zlib license. + +============================================================================ + +zlib License + +(C) 2024-present cam900 and contributors + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +============================================================================ +TODO: +- needs to further verifications from real hardware + +Tech info: https://problemkaputt.de/gbatek.htm + +*/ + +#ifndef NDS_SOUND_EMU_H +#define NDS_SOUND_EMU_H + +namespace nds_sound_emu +{ + using u8 = unsigned char; + using u16 = unsigned short; + using u32 = unsigned int; + using u64 = unsigned long long; + using s8 = signed char; + using s16 = signed short; + using s32 = signed int; + using s64 = signed long long; + + template + static const inline T bitfield(const T in, const u8 pos) + { + return (in >> pos) & 1; + } // bitfield + + template + static const inline T bitfield(const T in, const u8 pos, const u8 len) + { + return (in >> pos) & ((1 << len) - 1); + } // bitfield + + template + static const inline T clamp(const T in, const T min, const T max) + { + return (in < min) ? min : ((in > max) ? max : in); + } // clamp + + class nds_sound_intf + { + public: + nds_sound_intf() + { + } + + virtual u8 read_byte(u32 addr) { return 0; } + inline u16 read_word(u32 addr) { return read_byte(addr) | (u16(read_byte(addr + 1)) << 8); } + inline u32 read_dword(u32 addr) { return read_word(addr) | (u16(read_word(addr + 2)) << 16); } + + virtual void write_byte(u32 addr, u8 data) {} + inline void write_word(u32 addr, u16 data) + { + write_byte(addr, data & 0xff); + write_byte(addr + 1, data >> 8); + } + inline void write_dword(u32 addr, u32 data) + { + write_word(addr, data & 0xffff); + write_word(addr + 2, data >> 16); + } + }; + + class nds_sound_t + { + public: + nds_sound_t(nds_sound_intf &intf) + : m_intf(intf) + , m_channel{ + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, false, true), channel_t(*this, false, true) + } + , m_capture{ + capture_t(*this, m_channel[0], m_channel[1]), + capture_t(*this, m_channel[2], m_channel[3]) + } + , m_control(0) + , m_bias(0) + , m_loutput(0) + , m_routput(0) + { + } + + void reset(); + void tick(s32 cycle); + + // host accesses + u32 read32(u32 addr); + void write32(u32 addr, u32 data, u32 mask = ~0); + + u16 read16(u32 addr); + void write16(u32 addr, u16 data, u16 mask = ~0); + + u8 read8(u32 addr); + void write8(u32 addr, u8 data); + + s32 loutput() { return m_loutput; } + s32 routput() { return m_routput; } + + // for debug + s32 chan_lout(u8 ch) { return m_channel[ch].loutput(); } + s32 chan_rout(u8 ch) { return m_channel[ch].routput(); } + + private: + // ADPCM tables + s8 adpcm_index_table[8] = + { + -1, -1, -1, -1, 2, 4, 6, 8 + }; + + u16 adpcm_diff_table[89] = + { + 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x0010, + 0x0011, 0x0013, 0x0015, 0x0017, 0x0019, 0x001c, 0x001f, 0x0022, 0x0025, + 0x0029, 0x002d, 0x0032, 0x0037, 0x003c, 0x0042, 0x0049, 0x0050, 0x0058, + 0x0061, 0x006b, 0x0076, 0x0082, 0x008f, 0x009d, 0x00ad, 0x00be, 0x00d1, + 0x00e6, 0x00fd, 0x0117, 0x0133, 0x0151, 0x0173, 0x0198, 0x01c1, 0x01ee, + 0x0220, 0x0256, 0x0292, 0x02d4, 0x031c, 0x036c, 0x03c3, 0x0424, 0x048e, + 0x0502, 0x0583, 0x0610, 0x06ab, 0x0756, 0x0812, 0x08e0, 0x09c3, 0x0abd, + 0x0bd0, 0x0cff, 0x0e4c, 0x0fba, 0x114c, 0x1307, 0x14ee, 0x1706, 0x1954, + 0x1bdc, 0x1ea5, 0x21b6, 0x2515, 0x28ca, 0x2cdf, 0x315b, 0x364b, 0x3bb9, + 0x41b2, 0x4844, 0x4f7e, 0x5771, 0x602f, 0x69ce, 0x7462, 0x7fff + }; + + // structs + enum + { + STATE_ADPCM_LOAD = 0, + STATE_PRE_LOOP, + STATE_POST_LOOP + }; + + class channel_t + { + public: + channel_t(nds_sound_t &host, bool psg, bool noise) + : m_host(host) + + , m_psg(psg) + , m_noise(noise) + + , m_control(0) + , m_sourceaddr(0) + , m_freq(0) + , m_loopstart(0) + , m_length(0) + , m_playing(false) + , m_adpcm_out(0) + , m_adpcm_index(0) + , m_prev_adpcm_out(0) + , m_prev_adpcm_index(0) + , m_cur_addr(0) + , m_cur_state(0) + , m_cur_bitaddr(0) + , m_delay(0) + , m_sample(0) + , m_lfsr(0x7fff) + , m_lfsr_out(0x7fff) + , m_counter(0x10000) + , m_output(0) + , m_loutput(0) + , m_routput(0) + { + } + + void reset(); + void write(u32 offset, u32 data, u32 mask = ~0); + + void update(s32 cycle); + + // getters + // control word + u32 control() const { return m_control; } + u32 freq() const { return m_freq; } + + // outputs + s32 output() const { return m_output; } + s32 loutput() const { return m_loutput; } + s32 routput() const { return m_routput; } + + private: + // inline constants + const u8 m_voldiv_shift[4] = {0, 1, 2, 4}; + + // control bits + s32 volume() const { return bitfield(m_control, 0, 7); } // global volume + u32 voldiv() const { return m_voldiv_shift[bitfield(m_control, 8, 2)]; } // volume shift + bool hold() const { return bitfield(m_control, 15); } // hold bit + u32 pan() const { return bitfield(m_control, 16, 7); } // panning (0...127, 0 = left, 127 = right, 64 = half) + u32 duty() const { return bitfield(m_control, 24, 3); } // PSG duty + u32 repeat() const { return bitfield(m_control, 27, 2); } // Repeat mode (Manual, Loop infinitely, One-shot) + u32 format() const { return bitfield(m_control, 29, 2); } // Sound Format (PCM8, PCM16, ADPCM, PSG/Noise when exists) + bool busy() const { return bitfield(m_control, 31); } // Busy flag + + // calculated values + s32 lvol() const { return (pan() == 0x7f) ? 0 : 128 - pan(); } // calculated left volume + s32 rvol() const { return (pan() == 0x7f) ? 128 : pan(); } // calculated right volume + + // calculated address + u32 addr() const { return (m_sourceaddr & ~3) + (m_cur_bitaddr >> 3) + (m_cur_state == STATE_POST_LOOP ? ((m_loopstart + m_cur_addr) << 2) : (m_cur_addr << 2)); } + + void keyon(); + void keyoff(); + void fetch(); + void advance(); + + // interfaces + nds_sound_t &m_host; // host device + + // configuration + bool m_psg = false; // PSG Enable + bool m_noise = false; // Noise Enable + + // registers + u32 m_control = 0; // Control + u32 m_sourceaddr = 0; // Source Address + u16 m_freq = 0; // Frequency + u16 m_loopstart = 0; // Loop Start + u32 m_length = 0; // Length + + // internal states + bool m_playing = false; // playing flag + s32 m_adpcm_out = 0; // current ADPCM sample value + s32 m_adpcm_index = 0; // current ADPCM step + s32 m_prev_adpcm_out = 0; // previous ADPCM sample value + s32 m_prev_adpcm_index = 0; // previous ADPCM step + u32 m_cur_addr = 0; // current address + s32 m_cur_state = 0; // current state + s32 m_cur_bitaddr = 0; // bit address + s32 m_delay = 0; // delay + s16 m_sample = 0; // current sample + u32 m_lfsr = 0x7fff; // noise LFSR + s16 m_lfsr_out = 0x7fff; // LFSR output + s32 m_counter = 0x10000; // clock counter + s32 m_output = 0; // current output + s32 m_loutput = 0; // current left output + s32 m_routput = 0; // current right output + }; + + class capture_t + { + public: + capture_t(nds_sound_t &host, channel_t &input, channel_t &output) + : m_host(host) + , m_input(input) + , m_output(output) + + , m_control(0) + , m_dstaddr(0) + , m_length(0) + + , m_counter(0x10000) + , m_cur_addr(0) + , m_cur_waddr(0) + , m_cur_bitaddr(0) + , m_enable(0) + + , m_fifo{ + fifo_data_t(), fifo_data_t(), fifo_data_t(), fifo_data_t(), + fifo_data_t(), fifo_data_t(), fifo_data_t(), fifo_data_t() + } + , m_fifo_head(0) + , m_fifo_tail(0) + , m_fifo_empty(true) + , m_fifo_full(false) + { + } + + void reset(); + void update(s32 mix, s32 cycle); + + void control_w(u8 data); + void addrlen_w(u32 offset, u32 data, u32 mask = ~0); + + // getters + u32 control() const { return m_control; } + u32 dstaddr() const { return m_dstaddr; } + + private: + // inline constants + // control bits + bool addmode() const { return bitfield(m_control, 0); } // Add mode (add channel 1/3 output with channel 0/2) + bool get_source() const { return bitfield(m_control, 1); } // Select source (left or right mixer, channel 0/2) + bool repeat() const { return bitfield(m_control, 2); } // repeat flag + bool format() const { return bitfield(m_control, 3); } // store format (PCM16, PCM8) + bool busy() const { return bitfield(m_control, 7); } // busy flag + + // FIFO offset mask + u32 fifo_mask() const { return format() ? 7 : 3; } + + // calculated address + u32 waddr() const { return (m_dstaddr & ~3) + (m_cur_waddr << 2); } + + void capture_on(); + void capture_off(); + bool fifo_write(); + + // interfaces + nds_sound_t &m_host; // host device + channel_t &m_input; // Input channel + channel_t &m_output; // Output channel + + // registers + u8 m_control = 0; // Control + u32 m_dstaddr = 0; // Destination Address + u32 m_length = 0; // Buffer Length + + // internal states + u32 m_counter = 0x10000; // clock counter + u32 m_cur_addr = 0; // current address + u32 m_cur_waddr = 0; // current write address + s32 m_cur_bitaddr = 0; // bit address + bool m_enable = false; // capture enable + + // FIFO + class fifo_data_t + { + public: + fifo_data_t() + : m_data(0) + { + } + + void reset() + { + m_data = 0; + } + + // accessors + void write_byte(const u8 bit, const u8 data) + { + u32 input = u32(data) << bit; + u32 mask = (0xff << bit); + m_data = (m_data & ~mask) | (input & mask); + } + + void write_word(const u8 bit, const u16 data) + { + u32 input = u32(data) << bit; + u32 mask = (0xffff << bit); + m_data = (m_data & ~mask) | (input & mask); + } + + // getters + u32 data() const { return m_data; } + + private: + u32 m_data = 0; + }; + + fifo_data_t m_fifo[8]; // FIFO (8 word, for 16 sample delay) + u32 m_fifo_head = 0; // FIFO head + u32 m_fifo_tail = 0; // FIFO tail + bool m_fifo_empty = true; // FIFO empty flag + bool m_fifo_full = false; // FIFO full flag + }; + + nds_sound_intf &m_intf; // memory interface + + channel_t m_channel[16]; // 16 channels + capture_t m_capture[2]; // 2 capture channels + + inline u8 mvol() const { return bitfield(m_control, 0, 7); } // master volume + inline u8 lout_from() const { return bitfield(m_control, 8, 2); } // left output source (mixer, channel 1, channel 3, channel 1+3) + inline u8 rout_from() const { return bitfield(m_control, 10, 2); } // right output source (mixer, channel 1, channel 3, channel 1+3) + inline bool mix_ch1() const { return bitfield(m_control, 12); } // mix/bypass channel 1 + inline bool mix_ch3() const { return bitfield(m_control, 13); } // mix/bypass channel 3 + inline bool enable() const { return bitfield(m_control, 15); } // global enable + + u32 m_control = 0; // global control + u32 m_bias = 0; // output bias + s32 m_loutput = 0; // left output + s32 m_routput = 0; // right output + }; +}; // namespace nds_sound_emu + +#endif // NDS_SOUND_EMU_H \ No newline at end of file diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index bb8a21d3f..17340f551 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -35,6 +35,7 @@ extern "C" { #include "../../extern/adpcm/ymb_codec.h" #include "../../extern/adpcm/ymz_codec.h" } +#include "../../extern/adpcm-xq/adpcm-lib.h" #include "brrUtils.h" DivSampleHistory::~DivSampleHistory() { @@ -279,6 +280,9 @@ int DivSample::getSampleOffset(int offset, int length, DivSampleDepth depth) { case DIV_SAMPLE_DEPTH_C219: off=offset; break; + case DIV_SAMPLE_DEPTH_IMA_ADPCM: + off=(offset+1)/2; + break; case DIV_SAMPLE_DEPTH_16BIT: off=offset*2; break; @@ -338,6 +342,10 @@ int DivSample::getSampleOffset(int offset, int length, DivSampleDepth depth) { off=offset; len=length; break; + case DIV_SAMPLE_DEPTH_IMA_ADPCM: + off=(offset+1)/2; + len=(length+1)/2; + break; case DIV_SAMPLE_DEPTH_16BIT: off=offset*2; len=length*2; @@ -396,6 +404,9 @@ int DivSample::getEndPosition(DivSampleDepth depth) { case DIV_SAMPLE_DEPTH_C219: off=lengthC219; break; + case DIV_SAMPLE_DEPTH_IMA_ADPCM: + off=lengthIMA; + break; case DIV_SAMPLE_DEPTH_16BIT: off=length16; break; @@ -587,6 +598,12 @@ bool DivSample::initInternal(DivSampleDepth d, int count) { dataC219=new unsigned char[(count+4095)&(~0xfff)]; memset(dataC219,0,(count+4095)&(~0xfff)); break; + case DIV_SAMPLE_DEPTH_IMA_ADPCM: // IMA ADPCM + if (dataIMA!=NULL) delete[] dataIMA; + lengthIMA=4+((count+1)/2); + dataIMA=new unsigned char[lengthIMA]; + memset(dataIMA,0,lengthIMA); + break; case DIV_SAMPLE_DEPTH_16BIT: // 16-bit if (data16!=NULL) delete[] data16; length16=count*2; @@ -1271,6 +1288,9 @@ void DivSample::render(unsigned int formatMask) { if (dataC219[i]&0x80) data16[i]=-data16[i]; } break; + case DIV_SAMPLE_DEPTH_IMA_ADPCM: // IMA ADPCM + if (adpcm_decode_block(data16,dataIMA,lengthIMA,1)==0) logE("oh crap!"); + break; default: return; } @@ -1442,6 +1462,23 @@ void DivSample::render(unsigned int formatMask) { dataC219[i]=x|(negate?0x80:0); } } + if (NOT_IN_FORMAT(DIV_SAMPLE_DEPTH_IMA_ADPCM)) { // IMA ADPCM + if (!initInternal(DIV_SAMPLE_DEPTH_IMA_ADPCM,samples)) return; + int delta[2]; + delta[0]=0; + delta[1]=0; + + void* codec=adpcm_create_context(1,4,NOISE_SHAPING_OFF,delta); + if (codec==NULL) { + logE("oh no IMA encoder could not be created!"); + } else { + size_t whyPointer=0; + adpcm_encode_block(codec,dataIMA,&whyPointer,data16,samples); + if (whyPointer!=lengthIMA) logW("IMA length mismatch! %d -> %d!=%d",(int)samples,(int)whyPointer,(int)lengthIMA); + + adpcm_free_context(codec); + } + } } void* DivSample::getCurBuf() { @@ -1470,6 +1507,8 @@ void* DivSample::getCurBuf() { return dataMuLaw; case DIV_SAMPLE_DEPTH_C219: return dataC219; + case DIV_SAMPLE_DEPTH_IMA_ADPCM: + return dataIMA; case DIV_SAMPLE_DEPTH_16BIT: return data16; default: @@ -1504,6 +1543,8 @@ unsigned int DivSample::getCurBufLen() { return lengthMuLaw; case DIV_SAMPLE_DEPTH_C219: return lengthC219; + case DIV_SAMPLE_DEPTH_IMA_ADPCM: + return lengthIMA; case DIV_SAMPLE_DEPTH_16BIT: return length16; default: @@ -1616,4 +1657,5 @@ DivSample::~DivSample() { if (dataVOX) delete[] dataVOX; if (dataMuLaw) delete[] dataMuLaw; if (dataC219) delete[] dataC219; + if (dataIMA) delete[] dataIMA; } diff --git a/src/engine/sample.h b/src/engine/sample.h index 63d219259..cd4175e9d 100644 --- a/src/engine/sample.h +++ b/src/engine/sample.h @@ -46,6 +46,7 @@ enum DivSampleDepth: unsigned char { DIV_SAMPLE_DEPTH_VOX=10, DIV_SAMPLE_DEPTH_MULAW=11, DIV_SAMPLE_DEPTH_C219=12, + DIV_SAMPLE_DEPTH_IMA_ADPCM=13, DIV_SAMPLE_DEPTH_16BIT=16, DIV_SAMPLE_DEPTH_MAX // boundary for sample depth }; @@ -114,6 +115,7 @@ struct DivSample { // - 10: VOX ADPCM // - 11: 8-bit µ-law PCM // - 12: C219 "µ-law" PCM + // - 13: IMA ADPCM // - 16: 16-bit PCM DivSampleDepth depth; bool loop, brrEmphasis, dither; @@ -139,8 +141,9 @@ struct DivSample { unsigned char* dataVOX; // 10 unsigned char* dataMuLaw; // 11 unsigned char* dataC219; // 12 + unsigned char* dataIMA; // 13 - unsigned int length8, length16, length1, lengthDPCM, lengthZ, lengthQSoundA, lengthA, lengthB, lengthK, lengthBRR, lengthVOX, lengthMuLaw, lengthC219; + unsigned int length8, length16, length1, lengthDPCM, lengthZ, lengthQSoundA, lengthA, lengthB, lengthK, lengthBRR, lengthVOX, lengthMuLaw, lengthC219, lengthIMA; unsigned int samples; @@ -349,6 +352,7 @@ struct DivSample { dataVOX(NULL), dataMuLaw(NULL), dataC219(NULL), + dataIMA(NULL), length8(0), length16(0), length1(0), @@ -362,6 +366,7 @@ struct DivSample { lengthVOX(0), lengthMuLaw(0), lengthC219(0), + lengthIMA(0), samples(0) { for (int i=0; i