diff --git a/.gitignore b/.gitignore index 25bd6d0e0..8b45bb970 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ android/app/.cxx/ CMakeSettings.json CMakePresets.json extern/imgui_patched/examples/ +src/asm/68k/amigatest/*.bin +src/asm/68k/amigatest/player diff --git a/CMakeLists.txt b/CMakeLists.txt index 906bacffc..78a68aa83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -459,7 +459,7 @@ src/engine/platform/sound/ga20/iremga20.cpp src/engine/platform/sound/sm8521.c -src/engine/platform/sound/d65010g031.c +src/engine/platform/sound/d65modified.c src/engine/platform/oplAInterface.cpp src/engine/platform/ym2608Interface.cpp @@ -469,10 +469,12 @@ src/engine/blip_buf.c src/engine/brrUtils.c src/engine/safeReader.cpp src/engine/safeWriter.cpp +src/engine/cmdStream.cpp src/engine/config.cpp src/engine/configEngine.cpp src/engine/dispatchContainer.cpp src/engine/engine.cpp +src/engine/export.cpp src/engine/fileOps.cpp src/engine/fileOpsIns.cpp src/engine/filter.cpp @@ -488,6 +490,7 @@ src/engine/waveSynth.cpp src/engine/vgmOps.cpp src/engine/zsmOps.cpp src/engine/zsm.cpp + src/engine/platform/abstract.cpp src/engine/platform/genesis.cpp src/engine/platform/genesisext.cpp @@ -550,6 +553,9 @@ src/engine/platform/sm8521.cpp src/engine/platform/pv1000.cpp src/engine/platform/pcmdac.cpp src/engine/platform/dummy.cpp + +src/engine/export/abstract.cpp +src/engine/export/amigaValidation.cpp ) if (USE_SNDFILE) @@ -619,6 +625,7 @@ src/gui/editing.cpp src/gui/editControls.cpp src/gui/effectList.cpp src/gui/findReplace.cpp +src/gui/fmPreview.cpp src/gui/gradient.cpp src/gui/grooves.cpp src/gui/insEdit.cpp diff --git a/demos/misc/TerminalZone_SM8521.fur b/demos/misc/TerminalZone_SM8521.fur new file mode 100644 index 000000000..e9176c5f4 Binary files /dev/null and b/demos/misc/TerminalZone_SM8521.fur differ diff --git a/demos/misc/empty_PV-1000.fur b/demos/misc/empty_PV-1000.fur new file mode 100644 index 000000000..d6ec13ce5 Binary files /dev/null and b/demos/misc/empty_PV-1000.fur differ diff --git a/demos/snes/ManbowSMW.fur b/demos/snes/ManbowSMW.fur new file mode 100644 index 000000000..680bb858e Binary files /dev/null and b/demos/snes/ManbowSMW.fur differ diff --git a/extern/igfd/ImGuiFileDialog.cpp b/extern/igfd/ImGuiFileDialog.cpp index 535fb267f..372287d0f 100644 --- a/extern/igfd/ImGuiFileDialog.cpp +++ b/extern/igfd/ImGuiFileDialog.cpp @@ -26,6 +26,7 @@ SOFTWARE. */ #include "ImGuiFileDialog.h" +#include "../../src/ta-log.h" #ifdef __cplusplus @@ -1202,11 +1203,13 @@ namespace IGFD SetDefaultFileName("."); else SetDefaultFileName(""); + logV("IGFD: OpenCurrentPath()"); ScanDir(vFileDialogInternal, GetCurrentPath()); } void IGFD::FileManager::SortFields(const FileDialogInternal& vFileDialogInternal, const SortingFieldEnum& vSortingField, const bool vCanChangeOrder) { + logV("IGFD: SortFields()"); if (vSortingField != SortingFieldEnum::FIELD_NONE) { puHeaderFileName = tableHeaderFileNameString; @@ -1216,10 +1219,17 @@ namespace IGFD #ifdef USE_THUMBNAILS puHeaderFileThumbnails = tableHeaderFileThumbnailsString; #endif // #ifdef USE_THUMBNAILS - } + } else { + logV("IGFD: sorting by NONE!"); + } + + if (prFileList.empty()) { + logV("IGFD: with an empty file list?"); + } if (vSortingField == SortingFieldEnum::FIELD_FILENAME) { + logV("IGFD: sorting by name"); if (vCanChangeOrder && puSortingField == vSortingField) { //printf("Change the sorting\n"); puSortingDirection[0] = true;//!puSortingDirection[0]; @@ -1289,6 +1299,7 @@ namespace IGFD } else if (vSortingField == SortingFieldEnum::FIELD_TYPE) { + logV("IGFD: sorting by type"); if (vCanChangeOrder && puSortingField == vSortingField) puSortingDirection[1] = !puSortingDirection[1]; @@ -1327,6 +1338,7 @@ namespace IGFD } else if (vSortingField == SortingFieldEnum::FIELD_SIZE) { + logV("IGFD: sorting by size"); if (vCanChangeOrder && puSortingField == vSortingField) puSortingDirection[2] = !puSortingDirection[2]; @@ -1367,6 +1379,7 @@ namespace IGFD } else if (vSortingField == SortingFieldEnum::FIELD_DATE) { + logV("IGFD: sorting by date"); if (vCanChangeOrder && puSortingField == vSortingField) puSortingDirection[3] = !puSortingDirection[3]; @@ -1458,6 +1471,8 @@ namespace IGFD puSortingField = vSortingField; } + logV("IGFD: applying filtering on file list"); + ApplyFilteringOnFileList(vFileDialogInternal); } @@ -1518,14 +1533,17 @@ namespace IGFD void IGFD::FileManager::ScanDir(const FileDialogInternal& vFileDialogInternal, const std::string& vPath) { std::string path = vPath; + logV("IGFD: ScanDir(%s)",vPath); if (prCurrentPathDecomposition.empty()) { + logV("IGFD: the current path decomposition is empty. setting."); SetCurrentDir(path); } if (!prCurrentPathDecomposition.empty()) { + logV("IGFD: the current path decomposition is not empty. trying."); #ifdef WIN32 if (path == puFsRoot) path += std::string(1u, PATH_SEP); @@ -1553,6 +1571,7 @@ namespace IGFD #else // dirent struct dirent** files = nullptr; int n = scandir(path.c_str(), &files, nullptr, inAlphaSort); + logV("IGFD: %d entries in directory",n); if (n>0) { int i; @@ -1620,11 +1639,18 @@ namespace IGFD } free(files); - } + } else { + logV("IGFD: it's empty"); + } #endif // USE_STD_FILESYSTEM + logV("IGFD: sorting fields..."); SortFields(vFileDialogInternal, puSortingField, false); - } + } else { + logE("IGFD: current path decomposition is empty!"); + } + + fileListActuallyEmpty=prFileList.empty(); } bool IGFD::FileManager::GetDrives() @@ -2436,7 +2462,7 @@ namespace IGFD void IGFD::FileDialogInternal::ResetForNewDialog() { - + puFileManager.fileListActuallyEmpty=false; } ///////////////////////////////////////////////////////////////////////////////////// @@ -3737,16 +3763,17 @@ namespace IGFD fdFilter.SetDefaultFilterIfNotDefined(); // init list of files - if (fdFile.IsFileListEmpty() && !fdFile.puShowDrives) + if (fdFile.IsFileListEmpty() && !fdFile.puShowDrives && !fdFile.fileListActuallyEmpty) { IGFD::Utils::ReplaceString(fdFile.puDLGDefaultFileName, fdFile.puDLGpath, ""); // local path if (!fdFile.puDLGDefaultFileName.empty()) { fdFile.SetDefaultFileName(fdFile.puDLGDefaultFileName); fdFilter.SetSelectedFilterWithExt(fdFilter.puDLGdefaultExt); - } - else if (fdFile.puDLGDirectoryMode) // directory mode + } else if (fdFile.puDLGDirectoryMode) { // directory mode fdFile.SetDefaultFileName("."); + } + logV("IGFD: fdFile.IsFileListEmpty() and !fdFile.puShowDrives"); fdFile.ScanDir(prFileDialogInternal, fdFile.puDLGpath); } diff --git a/extern/igfd/ImGuiFileDialog.h b/extern/igfd/ImGuiFileDialog.h index 2725223f5..7b2523959 100644 --- a/extern/igfd/ImGuiFileDialog.h +++ b/extern/igfd/ImGuiFileDialog.h @@ -875,6 +875,7 @@ namespace IGFD #endif SortingFieldEnum puSortingField = SortingFieldEnum::FIELD_FILENAME; // detail view sorting column bool puShowDrives = false; // drives are shown (only on os windows) + bool fileListActuallyEmpty = false; std::string puDLGpath; // base path set by user when OpenDialog/OpenModal was called std::string puDLGDefaultFileName; // base default file path name set by user when OpenDialog/OpenModal was called diff --git a/instruments/FM/bass/Finger Bass.fui b/instruments/FM/bass/Finger Bass.fui new file mode 100644 index 000000000..9082043cd Binary files /dev/null and b/instruments/FM/bass/Finger Bass.fui differ diff --git a/instruments/FM/bass/Slap Bass.fui b/instruments/FM/bass/Slap Bass.fui new file mode 100644 index 000000000..246116265 Binary files /dev/null and b/instruments/FM/bass/Slap Bass.fui differ diff --git a/instruments/FM/drums/4-7 Snare.fui b/instruments/FM/drums/4-7 Snare.fui new file mode 100644 index 000000000..be625df03 Binary files /dev/null and b/instruments/FM/drums/4-7 Snare.fui differ diff --git a/instruments/FM/drums/Single Clap.fui b/instruments/FM/drums/Single Clap.fui new file mode 100644 index 000000000..fba0c307b Binary files /dev/null and b/instruments/FM/drums/Single Clap.fui differ diff --git a/instruments/FM/strings/Slow Strings.fui b/instruments/FM/strings/Slow Strings.fui new file mode 100644 index 000000000..4f1b57770 Binary files /dev/null and b/instruments/FM/strings/Slow Strings.fui differ diff --git a/src/asm/68k/amigatest/Makefile b/src/asm/68k/amigatest/Makefile new file mode 100644 index 000000000..967d5d501 --- /dev/null +++ b/src/asm/68k/amigatest/Makefile @@ -0,0 +1,4 @@ +all: player + +player: player.s sample.bin seq.bin wave.bin + vasmm68k_mot -Fhunkexe -kick1hunks -nosym -o player player.s diff --git a/src/asm/68k/amigatest/README.md b/src/asm/68k/amigatest/README.md new file mode 100644 index 000000000..300928a80 --- /dev/null +++ b/src/asm/68k/amigatest/README.md @@ -0,0 +1,56 @@ +# Amiga verification export format + +"ROM" export format exclusively for verifying whether the Amiga emulation in Furnace is correct. + +do not assume this is the actual ROM export! it is nothing more than a register dump kind of thing... + +# process + +enable the setting in Furnace to unlock the export option by adding this to furnace.cfg: + +``` +iCannotWait=1 +``` + +go to file > export Amiga validation data... + +put sample.bin, sbook.bin, seq.bin, wave.bin and wbook.bin in this directory. + +type `make`. you need vasm (68000 with Mot syntax) in order for it to work. +alternatively, type: + +``` +vasmm68k_mot -Fhunkexe -kick1hunks -nosym -o player player.s +``` + +run `player` on Amiga. it should play the exported song. + +# notes + +may not work correctly if you have slow/fast memory! +sequence and wave data should reside in fast memory but I haven't figured out how to... + +# sequence format + +## 00-0F: per-channel (00, 10, 20, 30) + +- 00 xxxxxx yyyy: set loc/len + - x: loc + - y: len +- 01 xxxxxx yyyy: initialize wavetable (xxxx: pos; yy: length) +- 02 xx: set loc/len from sample book +- 03 xx: initialize wavetable from wave book +- 04 xxxx: initialize wavetable from wave book (short) +- 06 xxxx: set period +- 08 xx: set volume +- 0a xxxx: set data + +## F0-FF: global + +- f0: do nothing +- f1: next tick +- f2 xx: wait +- f3 xxxx: wait +- f6 xxxx: write to DMACON +- fe xxxx: write to ADKCON +- ff: end of song diff --git a/src/asm/68k/amigatest/player.s b/src/asm/68k/amigatest/player.s new file mode 100644 index 000000000..94a2c05e5 --- /dev/null +++ b/src/asm/68k/amigatest/player.s @@ -0,0 +1,386 @@ +; Furnace validation player code +; this is NOT the ROM export you're looking for! + +VPOSR = $dff004 +VHPOSR = $dff006 +COLOR00 = $dff180 + +chipBase=$dff000 + +DMACONR = $02 +POTGOR = $16 +DMACON = $96 +ADKCON = $9e +AUDBASE = $a0 +AUD0LCH = $a0 +AUD0LCL = $a2 +AUD0LEN = $a4 +AUD0PER = $a6 +AUD0VOL = $a8 +AUD0DAT = $aa +AUD1VOL = $b8 +AUD2VOL = $c8 +AUD3VOL = $d8 + +code_c +init: + move.b #2,$bfe001 + lea chipBase,a0 + move.w #15,DMACON(a0) +waitCon: + move.w DMACONR(a0),d0 + andi.w #15,d0 + bne waitCon + + move.w #$8200,DMACON(a0) + move.w #$40,AUD0VOL(a0) + move.w #$40,AUD1VOL(a0) + move.w #$40,AUD2VOL(a0) + move.w #$40,AUD3VOL(a0) + lea seqAddr(pc),a0 + lea sequence(pc),a1 + move.l a1,(a0) +main: + bsr waitVBlank + + move.w #$000,d4 + move.w d4,COLOR00 + + lea chipBase,a0 + btst.b #2,POTGOR(a0) + bne next + + lea state(pc),a0 + move.w #1,2(a0) + +next: + bsr nextTick + + lea state(pc),a0 + tst.w 2(a0) + beq main +finish: + lea chipBase,a0 + move.w #15,DMACON(a0) + clr.l d0 + rts + +waitVBlank: + move.l (VPOSR),d0 + and.l #$1ff00,d0 + cmp.l #$bc00,d0 + bne waitVBlank +waitVBlank2: + move.l (VPOSR),d0 + and.l #$1ff00,d0 + cmp.l #$bd00,d0 + bne waitVBlank2 + rts + +nextTick: + lea state(pc),a4 + move.w (a4),d0 + subi.w #1,d0 + bmi nextTick0 + move.w d0,(a4) + rts +nextTick0: + move.l seqAddr(pc),a2 +nextTick1: + ; get next command + clr.w d0 + move.b (a2)+,d0 + move.w #$0ff,d4 + move.w d4,COLOR00 + +testSpecial: + cmp.w #$f0,d0 + bmi testChannel + +testF1: + ; f1 - next tick + cmp.b #$f1,d0 + bne testF2 + ; end of tick + move.w #0,(a4) + bra endTick +testF2: + ; f2 - wait (char) + cmp.b #$f2,d0 + bne testF3 + move.b (a2)+,d0 + andi.w #$ff,d0 + move.w d0,(a4) + bra endTick +testF3: + ; f3 - wait (short) + cmp.b #$f3,d0 + bne testF6 + clr.w d2 + move.b (a2)+,d2 + lsl.w #8,d2 + or.b (a2)+,d2 + move.w d2,(a4) + bra endTick +testF6: + ; f6 - write DMACON + cmp.b #$f6,d0 + bne testFE + move.w #$f00,d4 + move.w d4,COLOR00 + clr.w d2 + move.b (a2)+,d2 + lsl.w #8,d2 + or.b (a2)+,d2 + move.w d2,chipBase+DMACON + ; wait for DMACON to be done + move.b (VHPOSR),d0 +dmaConWait: + cmp.b (VHPOSR),d0 + beq dmaConWait + ; wait for DMACON to be done -2 + move.b (VHPOSR),d0 +dmaConWait1: + cmp.b (VHPOSR),d0 + beq dmaConWait1 + bra nextTick1 +testFE: + ; fe - write ADKCON + cmp.b #$fe,d0 + bne testFF + clr.w d2 + move.b (a2)+,d2 + lsl.w #8,d2 + or.b (a2)+,d2 + move.w d2,chipBase+ADKCON + bra nextTick1 +testFF: + ; ff - end of song + cmp.b #$ff,d0 + bne testOther +theEnd: + lea sequence(pc),a2 +testOther: + ; something else + bra nextTick1 + +testChannel: + cmp.b #$40,d0 + bge invalidCmd + ; process channel + move.b d0,d1 + andi.b #15,d0 + ; check for 0 + bne chanNotZero +sampleWrite: + ; write loc/len + move.w #$f0f,d4 + move.w d4,COLOR00 + clr.l d2 + move.b (a2)+,d2 + lsl.l #8,d2 + or.b (a2)+,d2 + lsl.l #8,d2 + or.b (a2)+,d2 + lea sampleData,a0 + add.l a0,d2 + lea chipBase,a0 + move.b d1,d0 + andi.l #$ff,d0 + addi.l #$a0,d0 + adda.l d0,a0 + move.l d2,(a0)+ ; location + clr.w d2 + move.b (a2)+,d2 + lsl.w #8,d2 + or.b (a2)+,d2 + move.w d2,(a0) ; length + bra nextTick1 +chanNotZero: + ; check for 8 (VOL) + cmp.b #8,d0 + bne chanSampleBook + ; write volume + clr.w d2 + move.b (a2)+,d2 + bra chanWrite +chanSampleBook: + ; check for 2 (loc/len from book) + cmp.b #2,d0 + bne chanWaveChange + + move.w #$f0f,d4 + move.w d4,COLOR00 + clr.l d3 + move.b (a2)+,d3 + bsr getSampleBook + + ; write loc/len + lea sampleData,a0 + add.l a0,d2 + lea chipBase,a0 + move.b d1,d0 + andi.l #$f0,d0 + addi.l #$a0,d0 + adda.l d0,a0 + move.l d2,(a0)+ ; location + move.w d3,(a0) ; length + + bra nextTick1 +chanWaveChange: + ; check for 1 (wave change) + cmp.b #1,d0 + bne chanWaveBookB + ; copy wave + clr.l d2 + move.b (a2)+,d2 + lsl.l #8,d2 + or.b (a2)+,d2 + lsl.l #8,d2 + or.b (a2)+,d2 + add.l #wavetable,d2 + move.l d2,a0 + + lea sampleData,a1 + andi.l #$30,d1 + lsl.l #4,d1 + adda.l d1,a1 + + clr.l d2 + move.b (a2)+,d2 + lsl.l #8,d2 + or.b (a2)+,d2 + + bsr copyWave + bra nextTick1 +chanWaveBookB: + ; check for 3 (wave change from book) + cmp.b #3,d0 + bne chanWaveBookW + + clr.l d3 + move.b (a2)+,d3 + bsr getWaveBook + + lea sampleData,a1 + andi.l #$30,d1 + lsl.l #4,d1 + adda.l d1,a1 + + bsr copyWave + bra nextTick1 +chanWaveBookW: + ; check for 4 (wave change from book short) + cmp.b #4,d0 + bne chanOther + + clr.l d3 + move.b (a2)+,d3 + lsl.l #8,d3 + or.b (a2)+,d3 + bsr getWaveBook + + lea sampleData,a1 + andi.l #$30,d1 + lsl.l #4,d1 + adda.l d1,a1 + + bsr copyWave + bra nextTick1 +chanOther: + ; get value and write + clr.w d2 + move.b (a2)+,d2 + lsl.w #8,d2 + or.b (a2)+,d2 +chanWrite: + move.w #$ff0,d4 + move.w d4,COLOR00 + lea chipBase,a0 + or.b d1,d0 + addi.b #AUDBASE,d0 + andi.l #$ff,d0 + adda.l d0,a0 + move.w d2,(a0) +invalidCmd: + bra nextTick1 + +endTick: + lea seqAddr(pc),a3 + move.l a2,(a3) + move.w #$000,d4 + move.w d4,COLOR00 + rts + +; a0: source. a1: destination. d2: length. +copyWave: + ; don't copy a zero-length wave + tst.l d2 + beq noCopy +copyWaveLoop: + move.w (a0)+,(a1)+ + subq.l #2,d2 + bne copyWaveLoop +noCopy: + rts + +; put wave book entry in a0/d2. d3: index (modified). +getWaveBook: + lea waveBook(pc),a3 + lsl.l #2,d3 + move.l (a3,d3),d2 + move.l d2,d3 + rol.l #8,d2 + andi.l #$ff,d2 + bne getWaveBook2 + move.w #$100,d2 +getWaveBook2: + andi.l #$ffffff,d3 + add.l #wavetable,d3 + move.l d3,a0 + rts + +; get sample book entry in d2/d3. d3: index. +getSampleBook: + lea sampleBook(pc),a3 + lsl.l #3,d3 + adda.l d3,a3 + move.l (a3)+,d2 + move.l (a3)+,d3 + andi.l #$ffffff,d2 + andi.l #$ffff,d3 + rts + +data_c + cnop 0,4 + +curColor: + dc.w 0 + +state: + dc.w 0 ; ticks + dc.w 0 ; quit + cnop 0,4 + +seqAddr: + dc.l 0 + cnop 0,4 + +sampleBook: + incbin "sbook.bin" + cnop 0,4 + +waveBook: + incbin "wbook.bin" + cnop 0,4 + +sequence: + incbin "seq.bin" + cnop 0,4 + +sampleData: + incbin "sample.bin" + cnop 0,4 + +wavetable: + incbin "wave.bin" diff --git a/src/engine/cmdStream.cpp b/src/engine/cmdStream.cpp new file mode 100644 index 000000000..4ef2c5e2b --- /dev/null +++ b/src/engine/cmdStream.cpp @@ -0,0 +1,301 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 "cmdStream.h" +#include "engine.h" +#include "../ta-log.h" + +void DivCSPlayer::cleanup() { + delete b; +} + +bool DivCSPlayer::tick() { + bool ticked=false; + for (int i=0; igetTotalChannelCount(); i++) { + bool sendVolume=false; + if (chan[i].readPos==0) continue; + + ticked=true; + + chan[i].waitTicks--; + while (chan[i].waitTicks<=0) { + stream.seek(chan[i].readPos,SEEK_SET); + unsigned char next=stream.readC(); + unsigned char command=0; + + if (next<0xb3) { // note + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,next-60)); + } else if (next>=0xd0 && next<=0xdf) { + command=fastCmds[next&15]; + } else if (next>=0xe0 && next<=0xef) { // preset delay + chan[i].waitTicks=fastDelays[next&15]; + } else switch (next) { + case 0xb4: // note on null + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL)); + break; + case 0xb5: // note off + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF,i)); + break; + case 0xb6: // note off env + e->dispatchCmd(DivCommand(DIV_CMD_NOTE_OFF_ENV,i)); + break; + case 0xb7: // env release + e->dispatchCmd(DivCommand(DIV_CMD_ENV_RELEASE,i)); + break; + case 0xb8: case 0xbe: case 0xc0: case 0xc2: + case 0xc3: case 0xc4: case 0xc5: case 0xc6: + case 0xc7: case 0xc8: case 0xc9: case 0xca: + command=next-0xb4; + break; + case 0xf7: + command=stream.readC(); + break; + case 0xf8: + logE("TODO: CALL"); + break; + case 0xf9: + logE("TODO: RET"); + break; + case 0xfa: + logE("TODO: JMP"); + break; + case 0xfb: + logE("TODO: RATE"); + break; + case 0xfc: + chan[i].waitTicks=(unsigned short)stream.readS(); + break; + case 0xfd: + chan[i].waitTicks=(unsigned char)stream.readC(); + break; + case 0xfe: + chan[i].waitTicks=1; + break; + case 0xff: + chan[i].readPos=0; + break; + } + + if (chan[i].readPos==0) break; + + if (command) { + int arg0=0; + int arg1=0; + switch (command) { + case DIV_CMD_INSTRUMENT: + case DIV_CMD_HINT_VIBRATO_RANGE: + case DIV_CMD_HINT_VIBRATO_SHAPE: + case DIV_CMD_HINT_PITCH: + case DIV_CMD_HINT_VOLUME: + arg0=(unsigned char)stream.readC(); + break; + case DIV_CMD_PANNING: + case DIV_CMD_HINT_VIBRATO: + case DIV_CMD_HINT_ARPEGGIO: + case DIV_CMD_HINT_PORTA: + arg0=(unsigned char)stream.readC(); + arg1=(unsigned char)stream.readC(); + break; + case DIV_CMD_PRE_PORTA: + arg0=(unsigned char)stream.readC(); + arg1=(arg0&0x40)?1:0; + arg0=(arg0&0x80)?1:0; + break; + case DIV_CMD_HINT_VOL_SLIDE: + arg0=(short)stream.readS(); + break; + case DIV_CMD_SAMPLE_MODE: + case DIV_CMD_SAMPLE_FREQ: + case DIV_CMD_SAMPLE_BANK: + case DIV_CMD_SAMPLE_POS: + case DIV_CMD_SAMPLE_DIR: + case DIV_CMD_FM_HARD_RESET: + case DIV_CMD_FM_LFO: + case DIV_CMD_FM_LFO_WAVE: + case DIV_CMD_FM_FB: + case DIV_CMD_FM_EXTCH: + case DIV_CMD_FM_AM_DEPTH: + case DIV_CMD_FM_PM_DEPTH: + case DIV_CMD_STD_NOISE_FREQ: + case DIV_CMD_STD_NOISE_MODE: + case DIV_CMD_WAVE: + case DIV_CMD_GB_SWEEP_TIME: + case DIV_CMD_GB_SWEEP_DIR: + case DIV_CMD_PCE_LFO_MODE: + case DIV_CMD_PCE_LFO_SPEED: + case DIV_CMD_NES_DMC: + case DIV_CMD_C64_CUTOFF: + case DIV_CMD_C64_RESONANCE: + case DIV_CMD_C64_FILTER_MODE: + case DIV_CMD_C64_RESET_TIME: + case DIV_CMD_C64_RESET_MASK: + case DIV_CMD_C64_FILTER_RESET: + case DIV_CMD_C64_DUTY_RESET: + case DIV_CMD_C64_EXTENDED: + case DIV_CMD_AY_ENVELOPE_SET: + case DIV_CMD_AY_ENVELOPE_LOW: + case DIV_CMD_AY_ENVELOPE_HIGH: + case DIV_CMD_AY_ENVELOPE_SLIDE: + case DIV_CMD_AY_NOISE_MASK_AND: + case DIV_CMD_AY_NOISE_MASK_OR: + case DIV_CMD_AY_AUTO_ENVELOPE: + case DIV_CMD_FDS_MOD_DEPTH: + case DIV_CMD_FDS_MOD_HIGH: + case DIV_CMD_FDS_MOD_LOW: + case DIV_CMD_FDS_MOD_POS: + case DIV_CMD_FDS_MOD_WAVE: + case DIV_CMD_SAA_ENVELOPE: + case DIV_CMD_AMIGA_FILTER: + case DIV_CMD_AMIGA_AM: + case DIV_CMD_AMIGA_PM: + case DIV_CMD_MACRO_OFF: + case DIV_CMD_MACRO_ON: + arg0=(unsigned char)stream.readC(); + break; + case DIV_CMD_FM_TL: + case DIV_CMD_FM_AM: + case DIV_CMD_FM_AR: + case DIV_CMD_FM_DR: + case DIV_CMD_FM_SL: + case DIV_CMD_FM_D2R: + case DIV_CMD_FM_RR: + case DIV_CMD_FM_DT: + case DIV_CMD_FM_DT2: + case DIV_CMD_FM_RS: + case DIV_CMD_FM_KSR: + case DIV_CMD_FM_VIB: + case DIV_CMD_FM_SUS: + case DIV_CMD_FM_WS: + case DIV_CMD_FM_SSG: + case DIV_CMD_FM_REV: + case DIV_CMD_FM_EG_SHIFT: + case DIV_CMD_FM_MULT: + case DIV_CMD_FM_FINE: + case DIV_CMD_AY_IO_WRITE: + case DIV_CMD_AY_AUTO_PWM: + case DIV_CMD_SURROUND_PANNING: + arg0=(unsigned char)stream.readC(); + arg1=(unsigned char)stream.readC(); + break; + case DIV_CMD_C64_FINE_DUTY: + case DIV_CMD_C64_FINE_CUTOFF: + case DIV_CMD_LYNX_LFSR_LOAD: + arg0=(unsigned short)stream.readS(); + break; + case DIV_CMD_FM_FIXFREQ: + arg0=(unsigned short)stream.readS(); + arg1=arg0&0x7ff; + arg0>>=12; + break; + case DIV_CMD_NES_SWEEP: + arg0=(unsigned char)stream.readC(); + arg1=arg0&0x77; + arg0=(arg0&8)?1:0; + break; + } + + switch (command) { + case DIV_CMD_HINT_VOLUME: + chan[i].volume=arg0<<8; + sendVolume=true; + break; + case DIV_CMD_HINT_VOL_SLIDE: + chan[i].volSpeed=arg0; + break; + default: // dispatch it + e->dispatchCmd(DivCommand((DivDispatchCmds)command,i,arg0,arg1)); + break; + } + } + + chan[i].readPos=stream.tell(); + } + + if (sendVolume || chan[i].volSpeed!=0) { + chan[i].volume+=chan[i].volSpeed; + if (chan[i].volume<0) { + chan[i].volume=0; + } + if (chan[i].volume>chan[i].volMax) { + chan[i].volume=chan[i].volMax; + } + + e->dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + } + } + + return ticked; +} + +bool DivCSPlayer::init() { + unsigned char magic[4]; + stream.seek(0,SEEK_SET); + stream.read(magic,4); + + if (memcmp(magic,"FCS",4)!=0) return false; + + unsigned int chans=stream.readI(); + + for (unsigned int i=0; i=DIV_MAX_CHANS) { + stream.readI(); + continue; + } + if ((int)i>=e->getTotalChannelCount()) { + stream.readI(); + continue; + } + chan[i].readPos=stream.readI(); + } + + stream.read(fastDelays,16); + stream.read(fastCmds,16); + + // initialize state + for (int i=0; igetTotalChannelCount(); i++) { + chan[i].volMax=(e->getDispatch(e->dispatchOfChan[i])->dispatch(DivCommand(DIV_CMD_GET_VOLMAX,e->dispatchChanOfChan[i]))<<8)|0xff; + chan[i].volume=chan[i].volMax; + } + + return true; +} + +// DivEngine + +bool DivEngine::playStream(unsigned char* f, size_t length) { + BUSY_BEGIN; + cmdStreamInt=new DivCSPlayer(this,f,length); + if (!cmdStreamInt->init()) { + logE("not a command stream!"); + lastError="not a command stream"; + delete[] f; + delete cmdStreamInt; + cmdStreamInt=NULL; + BUSY_END; + return false; + } + + if (!playing) { + reset(); + freelance=true; + playing=true; + } + BUSY_END; + return true; +} diff --git a/src/engine/cmdStream.h b/src/engine/cmdStream.h new file mode 100644 index 000000000..e12b0599e --- /dev/null +++ b/src/engine/cmdStream.h @@ -0,0 +1,59 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 _CMD_STREAM_H +#define _CMD_STREAM_H + +#include "defines.h" +#include "safeReader.h" + +class DivEngine; + +struct DivCSChannelState { + unsigned int readPos; + int waitTicks; + + int volume, volMax, volSpeed; + + DivCSChannelState(): + readPos(0), + waitTicks(0), + volume(0x7f00), + volMax(0), + volSpeed(0) {} +}; + +class DivCSPlayer { + DivEngine* e; + unsigned char* b; + SafeReader stream; + DivCSChannelState chan[DIV_MAX_CHANS]; + unsigned char fastDelays[16]; + unsigned char fastCmds[16]; + public: + void cleanup(); + bool tick(); + bool init(); + DivCSPlayer(DivEngine* en, unsigned char* buf, size_t len): + e(en), + b(buf), + stream(buf,len) {} +}; + +#endif diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index 6cb1b4e99..1d5932b69 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -280,19 +280,31 @@ struct DivRegWrite { * - 0xffffffff: reset */ unsigned int addr; - unsigned short val; - DivRegWrite(unsigned int a, unsigned short v): + unsigned int val; + DivRegWrite(unsigned int a, unsigned int v): addr(a), val(v) {} }; struct DivDelayedWrite { int time; DivRegWrite write; - DivDelayedWrite(int t, unsigned int a, unsigned short v): + DivDelayedWrite(int t, unsigned int a, unsigned int v): time(t), write(a,v) {} }; +struct DivSamplePos { + int sample, pos, freq; + DivSamplePos(int s, int p, int f): + sample(s), + pos(p), + freq(f) {} + DivSamplePos(): + sample(-1), + pos(0), + freq(0) {} +}; + struct DivDispatchOscBuffer { bool follow; unsigned int rate; @@ -371,18 +383,29 @@ class DivDispatch { /** * get the state of a channel. + * @param chan the channel. * @return a pointer, or NULL. */ virtual void* getChanState(int chan); /** - * get the DivMacroInt of a chanmel. + * get the DivMacroInt of a channel. + * @param chan the channel. * @return a pointer, or NULL. */ virtual DivMacroInt* getChanMacroInt(int chan); + /** + * get currently playing sample (and its position). + * @param chan the channel. + * @return a DivSamplePos. if sample is -1 then nothing is playing or the + * channel doesn't play samples. + */ + virtual DivSamplePos getSamplePos(int chan); + /** * get an oscilloscope buffer for a channel. + * @param chan the channel. * @return a pointer to a DivDispatchOscBuffer, or NULL if not supported. */ virtual DivDispatchOscBuffer* getOscBuffer(int chan); diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 04469b63f..a3669a184 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -1588,7 +1588,7 @@ void DivEngine::checkAssetDir(std::vector& dir, size_t entries) { // get unsorted directory DivAssetDir* unsortedDir=NULL; for (DivAssetDir& i: dir) { - if (i.name=="Unsorted") { + if (i.name.empty()) { unsortedDir=&i; break; } @@ -1596,7 +1596,7 @@ void DivEngine::checkAssetDir(std::vector& dir, size_t entries) { // create unsorted directory if it doesn't exist if (unsortedDir==NULL) { - dir.push_back(DivAssetDir("Unsorted")); + dir.push_back(DivAssetDir("")); unsortedDir=&(*dir.rbegin()); } @@ -2119,6 +2119,11 @@ DivMacroInt* DivEngine::getMacroInt(int chan) { return disCont[dispatchOfChan[chan]].dispatch->getChanMacroInt(dispatchChanOfChan[chan]); } +DivSamplePos DivEngine::getSamplePos(int chan) { + if (chan<0 || chan>=chans) return DivSamplePos(); + return disCont[dispatchOfChan[chan]].dispatch->getSamplePos(dispatchChanOfChan[chan]); +} + DivDispatchOscBuffer* DivEngine::getOscBuffer(int chan) { if (chan<0 || chan>=chans) return NULL; return disCont[dispatchOfChan[chan]].dispatch->getOscBuffer(dispatchChanOfChan[chan]); @@ -2498,6 +2503,10 @@ void DivEngine::recalcChans() { if (isInsTypePossible[i]) possibleInsTypes.push_back((DivInstrumentType)i); } + checkAssetDir(song.insDir,song.ins.size()); + checkAssetDir(song.waveDir,song.wave.size()); + checkAssetDir(song.sampleDir,song.sample.size()); + hasLoadedSomething=true; } @@ -2576,6 +2585,10 @@ int DivEngine::divToFileRate(int drate) { return 4; } +void DivEngine::testFunction() { + logI("it works!"); +} + int DivEngine::getEffectiveSampleRate(int rate) { if (rate<1) return 0; switch (song.system[0]) { @@ -4358,6 +4371,7 @@ bool DivEngine::initAudioBackend() { lowLatency=getConfInt("lowLatency",0); metroVol=(float)(getConfInt("metroVol",100))/100.0f; midiOutClock=getConfInt("midiOutClock",0); + midiOutProgramChange = getConfInt("midiOutProgramChange",0); midiOutMode=getConfInt("midiOutMode",DIV_MIDI_MODE_NOTE); if (metroVol<0.0f) metroVol=0.0f; if (metroVol>2.0f) metroVol=2.0f; diff --git a/src/engine/engine.h b/src/engine/engine.h index f1e5e91f8..0f5c40901 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -23,8 +23,10 @@ #include "instrument.h" #include "song.h" #include "dispatch.h" +#include "export.h" #include "dataErrors.h" #include "safeWriter.h" +#include "cmdStream.h" #include "../audio/taAudio.h" #include "blip_buf.h" #include @@ -47,11 +49,17 @@ #define BUSY_BEGIN_SOFT softLocked=true; isBusy.lock(); #define BUSY_END isBusy.unlock(); softLocked=false; +#define EXTERN_BUSY_BEGIN e->softLocked=false; e->isBusy.lock(); +#define EXTERN_BUSY_BEGIN_SOFT e->softLocked=true; e->isBusy.lock(); +#define EXTERN_BUSY_END e->isBusy.unlock(); e->softLocked=false; + #define DIV_VERSION "dev145" #define DIV_ENGINE_VERSION 145 // for imports #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 +#define DIV_VERSION_S3M 0xff03 +#define DIV_VERSION_FTM 0xff04 // "Namco C163" #define DIV_C163_DEFAULT_NAME "Namco 163" @@ -359,6 +367,7 @@ class DivEngine { bool systemsRegistered; bool hasLoadedSomething; bool midiOutClock; + bool midiOutProgramChange; int midiOutMode; int softLockCount; int subticks, ticks, curRow, curOrder, prevRow, prevOrder, remainingLoops, totalLoops, lastLoopPos, exportLoopCount, nextSpeed, elapsedBars, elapsedBeats, curSpeed; @@ -397,6 +406,8 @@ class DivEngine { static DivSystem sysFileMapFur[DIV_MAX_CHIP_DEFS]; static DivSystem sysFileMapDMF[DIV_MAX_CHIP_DEFS]; + DivCSPlayer* cmdStreamInt; + struct SamplePreview { double rate; int sample; @@ -441,7 +452,6 @@ class DivEngine { // MIDI stuff std::function midiCallback=[](const TAMidiMessage&) -> int {return -2;}; - int dispatchCmd(DivCommand c); void processRow(int i, bool afterDelay); void nextOrder(); void nextRow(); @@ -454,9 +464,12 @@ class DivEngine { void reset(); void playSub(bool preserveDrift, int goalRow=0); + void testFunction(); + bool loadDMF(unsigned char* file, size_t len); bool loadFur(unsigned char* file, size_t len); bool loadMod(unsigned char* file, size_t len); + bool loadS3M(unsigned char* file, size_t len); bool loadFTM(unsigned char* file, size_t len); bool loadFC(unsigned char* file, size_t len); @@ -493,6 +506,10 @@ class DivEngine { // check whether an asset directory is complete void checkAssetDir(std::vector& dir, size_t entries); + // add every export method here + friend class DivROMExport; + friend class DivExportAmigaValidation; + public: DivSong song; DivOrders* curOrders; @@ -522,6 +539,8 @@ class DivEngine { void createNewFromDefaults(); // load a file. bool load(unsigned char* f, size_t length); + // play a binary command stream. + bool playStream(unsigned char* f, size_t length); // save as .dmf. SafeWriter* saveDMF(unsigned char version); // save as .fur. @@ -529,7 +548,7 @@ class DivEngine { SafeWriter* saveFur(bool notPrimary=false); // build a ROM file (TODO). // specify system to build ROM for. - SafeWriter* buildROM(int sys); + std::vector buildROM(DivROMExportOptions sys); // dump to VGM. // set trailingTicks to: // - 0 to add one tick of trailing @@ -552,6 +571,9 @@ class DivEngine { // notify wavetable change void notifyWaveChange(int wave); + // dispatch a command + int dispatchCmd(DivCommand c); + // get system IDs static DivSystem systemFromFileFur(unsigned char val); static unsigned char systemToFileFur(DivSystem val); @@ -901,6 +923,9 @@ class DivEngine { // get macro interpreter DivMacroInt* getMacroInt(int chan); + // get sample position + DivSamplePos getSamplePos(int chan); + // get osc buffer DivDispatchOscBuffer* getOscBuffer(int chan); @@ -1094,6 +1119,7 @@ class DivEngine { systemsRegistered(false), hasLoadedSomething(false), midiOutClock(false), + midiOutProgramChange(false), midiOutMode(DIV_MIDI_MODE_NOTE), softLockCount(0), subticks(0), @@ -1133,6 +1159,7 @@ class DivEngine { audioEngine(DIV_AUDIO_NULL), exportMode(DIV_EXPORT_MODE_ONE), exportFadeOut(0.0), + cmdStreamInt(NULL), midiBaseChan(0), midiPoly(true), midiAgeCounter(0), diff --git a/src/engine/export.cpp b/src/engine/export.cpp new file mode 100644 index 000000000..141736d9e --- /dev/null +++ b/src/engine/export.cpp @@ -0,0 +1,37 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 "engine.h" + +#include "export/amigaValidation.h" + +std::vector DivEngine::buildROM(DivROMExportOptions sys) { + DivROMExport* exporter=NULL; + switch (sys) { + case DIV_ROM_AMIGA_VALIDATION: + exporter=new DivExportAmigaValidation; + break; + default: + exporter=new DivROMExport; + break; + } + std::vector ret=exporter->go(this); + delete exporter; + return ret; +} diff --git a/src/engine/export.h b/src/engine/export.h new file mode 100644 index 000000000..07354fdb3 --- /dev/null +++ b/src/engine/export.h @@ -0,0 +1,75 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 _EXPORT_H +#define _EXPORT_H + +#include "song.h" +#include +#include + +class DivEngine; + +enum DivROMExportOptions { + DIV_ROM_ABSTRACT=0, + DIV_ROM_AMIGA_VALIDATION, + + DIV_ROM_MAX +}; + +struct DivROMExportOutput { + String name; + SafeWriter* data; + + DivROMExportOutput(String n, SafeWriter* d): + name(n), + data(d) {} + DivROMExportOutput(): + name(""), + data(NULL) {} +}; + +class DivROMExport { + public: + virtual std::vector go(DivEngine* e); + virtual ~DivROMExport() {} +}; + +struct DivROMExportDef { + const char* name; + const char* author; + const char* description; + DivSystem requisites[32]; + int requisitesLen; + bool multiOutput; + + DivROMExportDef(const char* n, const char* a, const char* d, std::initializer_list req, bool multiOut): + name(n), + author(a), + description(d), + multiOutput(multiOut) { + requisitesLen=0; + memset(requisites,0,32*sizeof(DivSystem)); + for (DivSystem i: req) { + requisites[requisitesLen++]=i; + } + } +}; + +#endif diff --git a/src/engine/export/abstract.cpp b/src/engine/export/abstract.cpp new file mode 100644 index 000000000..ce07519c0 --- /dev/null +++ b/src/engine/export/abstract.cpp @@ -0,0 +1,26 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 "../export.h" +#include "../../ta-log.h" + +std::vector DivROMExport::go(DivEngine* e) { + logW("what's this? the null ROM export?"); + return std::vector(); +} diff --git a/src/engine/export/amigaValidation.cpp b/src/engine/export/amigaValidation.cpp new file mode 100644 index 000000000..dc836f33e --- /dev/null +++ b/src/engine/export/amigaValidation.cpp @@ -0,0 +1,276 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 "amigaValidation.h" +#include "../engine.h" +#include "../platform/amiga.h" + +struct WaveEntry { + unsigned int pos; + short width; + signed char data[256]; + WaveEntry(): + pos(0), + width(32) { + memset(data,0,256); + } +}; + +struct SampleBookEntry { + unsigned int loc; + unsigned short len; + SampleBookEntry(): + loc(0), + len(0) {} +}; + +std::vector DivExportAmigaValidation::go(DivEngine* e) { + std::vector ret; + std::vector waves; + std::vector sampleBook; + unsigned int wavesDataPtr=0; + WaveEntry curWaveState[4]; + unsigned int sampleBookLoc=0; + + DivPlatformAmiga* amiga=(DivPlatformAmiga*)e->getDispatch(0); + + e->stop(); + e->repeatPattern=false; + e->setOrder(0); + EXTERN_BUSY_BEGIN_SOFT; + + // determine loop point + int loopOrder=0; + int loopRow=0; + int loopEnd=0; + e->walkSong(loopOrder,loopRow,loopEnd); + + e->curOrder=0; + e->freelance=false; + e->playing=false; + e->extValuePresent=false; + e->remainingLoops=-1; + + // play the song ourselves + bool done=false; + + // sample.bin + SafeWriter* sample=new SafeWriter; + sample->init(); + for (int i=0; i<256; i++) { + sample->writeI(0); + } + sample->write(&((const unsigned char*)amiga->getSampleMem(0))[0x400],amiga->getSampleMemUsage(0)-0x400); + if (sample->tell()&1) sample->writeC(0); + + // seq.bin + SafeWriter* seq=new SafeWriter; + seq->init(); + + amiga->toggleRegisterDump(true); + + // write song data + e->playSub(false); + size_t songTick=0; + size_t lastTick=0; + //bool writeLoop=false; + int loopPos=-1; + for (int i=0; ichans; i++) { + e->chan[i].wentThroughNote=false; + e->chan[i].goneThroughNote=false; + } + while (!done) { + if (loopPos==-1) { + if (loopOrder==e->curOrder && loopRow==e->curRow && e->ticks==1) { + //writeLoop=true; + } + } + if (e->nextTick(false,true)) { + done=true; + amiga->getRegisterWrites().clear(); + if (lastTick!=songTick) { + int delta=songTick-lastTick; + if (delta==1) { + seq->writeC(0xf1); + } else if (delta<256) { + seq->writeC(0xf2); + seq->writeC(delta-1); + } else if (delta<32768) { + seq->writeC(0xf3); + seq->writeS_BE(delta-1); + } + lastTick=songTick; + } + break; + } + // check wavetable changes + for (int i=0; i<4; i++) { + if (amiga->chan[i].useWave) { + if ((amiga->chan[i].audLen*2)!=curWaveState[i].width || memcmp(curWaveState[i].data,&(((signed char*)amiga->getSampleMem())[i<<8]),amiga->chan[i].audLen*2)!=0) { + curWaveState[i].width=amiga->chan[i].audLen*2; + memcpy(curWaveState[i].data,&(((signed char*)amiga->getSampleMem())[i<<8]),amiga->chan[i].audLen*2); + + int waveNum=-1; + for (size_t j=0; jwriteC((i<<4)|3); + seq->writeC(waveNum); + } else if (waveNum<65536) { + seq->writeC((i<<4)|4); + seq->writeS_BE(waveNum); + } else{ + seq->writeC((i<<4)|1); + seq->writeC(waves[waveNum].pos>>16); + seq->writeC(waves[waveNum].pos>>8); + seq->writeC(waves[waveNum].pos); + seq->writeS_BE(waves[waveNum].width); + } + } + } + } + + // get register writes + std::vector& writes=amiga->getRegisterWrites(); + for (DivRegWrite& j: writes) { + if (lastTick!=songTick) { + int delta=songTick-lastTick; + if (delta==1) { + seq->writeC(0xf1); + } else if (delta<256) { + seq->writeC(0xf2); + seq->writeC(delta-1); + } else if (delta<32768) { + seq->writeC(0xf3); + seq->writeS_BE(delta-1); + } + lastTick=songTick; + } + if (j.addr>=0x200) { // direct loc/len change + if (j.addr&4) { // len + int sampleBookIndex=-1; + for (size_t i=0; iwriteC((j.addr&3)<<4); + seq->writeC(sampleBookLoc>>16); + seq->writeC(sampleBookLoc>>8); + seq->writeC(sampleBookLoc); + seq->writeS_BE(j.val); + } else { + seq->writeC(((j.addr&3)<<4)|2); + seq->writeC(sampleBookIndex); + } + } else { // loc + sampleBookLoc=j.val; + } + } else if (j.addr<0xa0) { + // don't write INTENA + if ((j.addr&15)!=10) { + seq->writeC(0xf0|(j.addr&15)); + seq->writeS_BE(j.val); + } + } else if ((j.addr&15)!=0 && (j.addr&15)!=2 && (j.addr&15)!=4) { + seq->writeC(j.addr-0xa0); + if ((j.addr&15)==8) { + seq->writeC(j.val); + } else { + seq->writeS_BE(j.val); + } + } + } + writes.clear(); + + songTick++; + } + // end of song + seq->writeC(0xff); + + amiga->toggleRegisterDump(false); + + e->remainingLoops=-1; + e->playing=false; + e->freelance=false; + e->extValuePresent=false; + + EXTERN_BUSY_END; + + // wave.bin + SafeWriter* wave=new SafeWriter; + wave->init(); + for (WaveEntry& i: waves) { + wave->write(i.data,i.width); + } + + // sbook.bin + SafeWriter* sbook=new SafeWriter; + sbook->init(); + for (SampleBookEntry& i: sampleBook) { + // 8 bytes per entry + sbook->writeI_BE(i.loc); + sbook->writeI_BE(i.len); + } + + // wbook.bin + SafeWriter* wbook=new SafeWriter; + wbook->init(); + for (WaveEntry& i: waves) { + wbook->writeC(i.width); + wbook->writeC(i.pos>>16); + wbook->writeC(i.pos>>8); + wbook->writeC(i.pos); + } + + // finish + ret.push_back(DivROMExportOutput("sbook.bin",sbook)); + ret.push_back(DivROMExportOutput("wbook.bin",wbook)); + ret.push_back(DivROMExportOutput("sample.bin",sample)); + ret.push_back(DivROMExportOutput("wave.bin",wave)); + ret.push_back(DivROMExportOutput("seq.bin",seq)); + + return ret; +} diff --git a/src/engine/export/amigaValidation.h b/src/engine/export/amigaValidation.h new file mode 100644 index 000000000..94d5eabbe --- /dev/null +++ b/src/engine/export/amigaValidation.h @@ -0,0 +1,26 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 "../export.h" + +class DivExportAmigaValidation: public DivROMExport { + public: + std::vector go(DivEngine* e); + ~DivExportAmigaValidation() {} +}; diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index c1ce34897..e1ae97b13 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -30,6 +30,7 @@ #define DIV_FTM_MAGIC "FamiTracker Module" #define DIV_FC13_MAGIC "SMOD" #define DIV_FC14_MAGIC "FC14" +#define DIV_S3M_MAGIC "SCRM" struct InflateBlock { unsigned char* buf; @@ -3226,6 +3227,246 @@ void generateFCPresetWave(int index, DivWavetable* wave) { } } +bool DivEngine::loadS3M(unsigned char* file, size_t len) { + struct InvalidHeaderException {}; + bool success=false; + char magic[4]={0,0,0,0}; + SafeReader reader=SafeReader(file,len); + warnings=""; + + unsigned char chanSettings[32]; + unsigned char ord[256]; + unsigned short insPtr[256]; + unsigned short patPtr[256]; + unsigned char chanPan[16]; + unsigned char defVol[256]; + + try { + DivSong ds; + ds.version=DIV_VERSION_S3M; + ds.linearPitch=0; + ds.pitchMacroIsLinear=false; + ds.noSlidesOnFirstTick=true; + ds.rowResetsArpPos=true; + ds.ignoreJumpAtEnd=false; + + // load here + if (!reader.seek(0x2c,SEEK_SET)) { + logE("premature end of file!"); + lastError="incomplete file"; + delete[] file; + return false; + } + reader.read(magic,4); + + if (memcmp(magic,DIV_S3M_MAGIC,4)!=0) { + logW("the magic isn't complete"); + throw EndOfFileException(&reader,reader.tell()); + } + + if (!reader.seek(0,SEEK_SET)) { + logE("premature end of file!"); + lastError="incomplete file"; + delete[] file; + return false; + } + + ds.name=reader.readString(28); + + reader.readC(); // 0x1a + if (reader.readC()!=16) { + logW("type is wrong!"); + } + reader.readS(); // x + + unsigned short ordersLen=reader.readS(); + ds.insLen=reader.readS(); + + if (ds.insLen<0 || ds.insLen>256) { + logE("invalid instrument count!"); + lastError="invalid instrument count!"; + delete[] file; + return false; + } + + unsigned short patCount=reader.readS(); + + unsigned short flags=reader.readS(); + unsigned short version=reader.readS(); + bool signedSamples=(reader.readS()==1); + + if ((flags&64) || version==0x1300) { + ds.noSlidesOnFirstTick=false; + } + + reader.readI(); // "SCRM" + + unsigned char globalVol=reader.readC(); + + ds.subsong[0]->speeds.val[0]=(unsigned char)reader.readC(); + ds.subsong[0]->hz=((double)reader.readC())/2.5; + ds.subsong[0]->customTempo=true; + + unsigned char masterVol=reader.readC(); + + logV("masterVol: %d",masterVol); + logV("signedSamples: %d",signedSamples); + logV("globalVol: %d",globalVol); + + reader.readC(); // UC + bool defaultPan=(((unsigned char)reader.readC())==252); + + reader.readS(); // reserved + reader.readI(); + reader.readI(); // the last 2 bytes is Special. we don't read that. + + reader.read(chanSettings,32); + + logD("reading orders..."); + for (int i=0; i=32) continue; + if ((chanSettings[i]&127)>=16) { + hasFM=true; + } else { + hasPCM=true; + } + + if (hasFM && hasPCM) break; + } + + ds.systemName="PC"; + if (hasPCM) { + ds.system[ds.systemLen]=DIV_SYSTEM_ES5506; + ds.systemVol[ds.systemLen]=1.0f; + ds.systemPan[ds.systemLen]=0; + ds.systemLen++; + } + if (hasFM) { + ds.system[ds.systemLen]=DIV_SYSTEM_OPL2; + ds.systemVol[ds.systemLen]=1.0f; + ds.systemPan[ds.systemLen]=0; + ds.systemLen++; + } + + // load instruments/samples + for (int i=0; itype=DIV_INS_ES5506; + } else if (memcmp(magic,"SCRI",4)==0) { + ins->type=DIV_INS_OPL; + } else { + ins->type=DIV_INS_ES5506; + ds.ins.push_back(ins); + continue; + } + + if (!reader.seek(insPtr[i]*16,SEEK_SET)) { + logE("premature end of file!"); + lastError="incomplete file"; + delete ins; + delete[] file; + return false; + } + + String dosName=reader.readString(13); + + if (ins->type==DIV_INS_ES5506) { + unsigned int memSeg=0; + memSeg=(unsigned char)reader.readC(); + memSeg|=((unsigned short)reader.readS())<<8; + + logV("memSeg: %d",memSeg); + + unsigned int length=reader.readI(); + + DivSample* s=new DivSample; + s->depth=DIV_SAMPLE_DEPTH_8BIT; + s->init(length); + + s->loopStart=reader.readI(); + s->loopEnd=reader.readI(); + defVol[i]=reader.readC(); + + logV("defVol: %d",defVol[i]); + + reader.readC(); // x + } else { + + } + + ds.ins.push_back(ins); + } + + if (active) quitDispatch(); + BUSY_BEGIN_SOFT; + saveLock.lock(); + song.unload(); + song=ds; + changeSong(0); + recalcChans(); + saveLock.unlock(); + BUSY_END; + if (active) { + initDispatch(); + BUSY_BEGIN; + renderSamples(); + reset(); + BUSY_END; + } + success=true; + } catch (EndOfFileException& e) { + //logE("premature end of file!"); + lastError="incomplete file"; + } catch (InvalidHeaderException& e) { + //logE("invalid header!"); + lastError="invalid header!"; + } + return success; +} + bool DivEngine::loadFC(unsigned char* file, size_t len) { struct InvalidHeaderException {}; bool success=false; @@ -3820,12 +4061,58 @@ bool DivEngine::loadFC(unsigned char* file, size_t len) { #define CHECK_BLOCK_VERSION(x) \ if (blockVersion>x) { \ - logE("incompatible block version %d for %s!",blockVersion,blockName); \ - lastError="incompatible block version"; \ - delete[] file; \ - return false; \ + logW("incompatible block version %d for %s!",blockVersion,blockName); \ } +const int ftEffectMap[]={ + -1, // none + 0x0f, + 0x0b, + 0x0d, + 0xff, + -1, // volume? not supported in Furnace yet + 0x03, + 0x03, // unused? + 0x13, + 0x14, + 0x00, + 0x04, + 0x07, + 0xe5, + 0xed, + 0x11, + 0x01, // porta up + 0x02, // porta down + 0x12, + 0x90, // sample offset - not supported yet + 0xe1, + 0xe2, + 0x0a, + 0xec, + 0x0c, + -1, // delayed volume - not supported yet + 0x11, // FDS + 0x12, + 0x13, + 0x20, // DPCM pitch + 0x22, // 5B + 0x24, + 0x23, + 0x21, + -1, // VRC7 "custom patch port" - not supported? + -1, // VRC7 "custom patch write" + -1, // release - not supported yet + 0x09, // select groove + -1, // transpose - not supported + 0x10, // Namco 163 + -1, // FDS vol env - not supported + -1, // FDS auto FM - not supported yet + -1, // phase reset - not supported + -1, // harmonic - not supported +}; + +constexpr int ftEffectMapSize=sizeof(ftEffectMap)/sizeof(int); + bool DivEngine::loadFTM(unsigned char* file, size_t len) { SafeReader reader=SafeReader(file,len); warnings=""; @@ -3837,6 +4124,9 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { unsigned int n163Chans=0; bool hasSequence[256][8]; unsigned char sequenceIndex[256][8]; + unsigned int hilightA=4; + unsigned int hilightB=16; + double customHz=60; memset(hasSequence,0,256*8*sizeof(bool)); memset(sequenceIndex,0,256*8); @@ -3857,6 +4147,14 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { return false; } + for (DivSubSong* i: ds.subsong) { + i->clearData(); + delete i; + } + ds.subsong.clear(); + + ds.linearPitch=0; + while (true) { blockName=reader.readString(3); if (blockName=="END") { @@ -3874,7 +4172,8 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { logD("reading block %s (version %d, %d bytes)",blockName,blockVersion,blockSize); if (blockName=="PARAMS") { - CHECK_BLOCK_VERSION(6); + // versions 7-9 don't change anything? + CHECK_BLOCK_VERSION(9); unsigned int oldSpeedTempo=0; if (blockVersion<=1) { oldSpeedTempo=reader.readI(); @@ -3884,15 +4183,32 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { } tchans=reader.readI(); unsigned int pal=reader.readI(); - unsigned int customHz=reader.readI(); + if (blockVersion>=7) { + // advanced Hz control + int controlType=reader.readI(); + switch (controlType) { + case 1: + customHz=1000000.0/(double)reader.readI(); + break; + default: + reader.readI(); + break; + } + } else { + customHz=reader.readI(); + } unsigned int newVibrato=0; + bool sweepReset=false; unsigned int speedSplitPoint=0; if (blockVersion>=3) { newVibrato=reader.readI(); } - if (blockVersion>=4) { - ds.subsong[0]->hilightA=reader.readI(); - ds.subsong[0]->hilightB=reader.readI(); + if (blockVersion>=9) { + sweepReset=reader.readI(); + } + if (blockVersion>=4 && blockVersion<7) { + hilightA=reader.readI(); + hilightB=reader.readI(); } if (expansions&8) if (blockVersion>=5) { // N163 channels n163Chans=reader.readI(); @@ -3901,20 +4217,24 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { speedSplitPoint=reader.readI(); } + if (blockVersion>=8) { + int fineTuneCents=reader.readC()*100; + fineTuneCents+=reader.readC(); + + ds.tuning=440.0*pow(2.0,(double)fineTuneCents/1200.0); + } + logV("old speed/tempo: %d",oldSpeedTempo); logV("expansions: %x",expansions); logV("channels: %d",tchans); logV("PAL: %d",pal); - logV("custom Hz: %d",customHz); + logV("custom Hz: %f",customHz); logV("new vibrato: %d",newVibrato); logV("N163 channels: %d",n163Chans); - logV("highlight 1: %d",ds.subsong[0]->hilightA); - logV("highlight 2: %d",ds.subsong[0]->hilightB); + logV("highlight 1: %d",hilightA); + logV("highlight 2: %d",hilightB); logV("split point: %d",speedSplitPoint); - - if (customHz!=0) { - ds.subsong[0]->hz=customHz; - } + logV("sweep reset: %d",sweepReset); // initialize channels int systemID=0; @@ -3959,28 +4279,46 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { CHECK_BLOCK_VERSION(1); ds.name=reader.readString(32); ds.author=reader.readString(32); - ds.copyright=reader.readString(32); + ds.category=reader.readString(32); + ds.systemName="NES"; } else if (blockName=="HEADER") { - CHECK_BLOCK_VERSION(3); + CHECK_BLOCK_VERSION(4); unsigned char totalSongs=reader.readC(); logV("%d songs:",totalSongs+1); for (int i=0; i<=totalSongs; i++) { String subSongName=reader.readString(); + ds.subsong.push_back(new DivSubSong); + ds.subsong[i]->name=subSongName; + ds.subsong[i]->hilightA=hilightA; + ds.subsong[i]->hilightB=hilightB; + if (customHz!=0) { + ds.subsong[i]->hz=customHz; + } logV("- %s",subSongName); } for (unsigned int i=0; ipat[i].effectCols=effectCols+1; - } + ds.subsong[j]->pat[i].effectCols=effectCols+1; logV("- song %d has %d effect columns",j,effectCols); } } + + if (blockVersion>=4) { + for (int i=0; i<=totalSongs; i++) { + ds.subsong[i]->hilightA=(unsigned char)reader.readC(); + ds.subsong[i]->hilightB=(unsigned char)reader.readC(); + } + } } else if (blockName=="INSTRUMENTS") { CHECK_BLOCK_VERSION(6); + + reader.seek(blockSize,SEEK_CUR); + + /* ds.insLen=reader.readI(); if (ds.insLen<0 || ds.insLen>256) { logE("too many instruments/out of range!"); @@ -4140,21 +4478,131 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { ins->name=reader.readString((unsigned int)reader.readI()); logV("- %d: %s",insIndex,ins->name); } + */ } else if (blockName=="SEQUENCES") { CHECK_BLOCK_VERSION(6); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="FRAMES") { CHECK_BLOCK_VERSION(3); + + for (size_t i=0; iordersLen=reader.readI(); + if (blockVersion>=3) { + s->speeds.val[0]=reader.readI(); + } + if (blockVersion>=2) { + s->virtualTempoN=reader.readI(); + s->patLen=reader.readI(); + } + int why=tchans; + if (blockVersion==1) { + why=reader.readI(); + } + logV("reading %d and %d orders",tchans,s->ordersLen); + + for (int j=0; jordersLen; j++) { + for (int k=0; korders.ord[k][j]=o; + } + } + } } else if (blockName=="PATTERNS") { - CHECK_BLOCK_VERSION(5); + CHECK_BLOCK_VERSION(6); + + size_t blockEnd=reader.tell()+blockSize; + + if (blockVersion==1) { + int patLenOld=reader.readI(); + for (DivSubSong* i: ds.subsong) { + i->patLen=patLenOld; + } + } + + // so it appears .ftm doesn't keep track of how many patterns are stored in the file.... + while (reader.tell()=2) subs=reader.readI(); + int ch=reader.readI(); + int patNum=reader.readI(); + int numRows=reader.readI(); + + DivPattern* pat=ds.subsong[subs]->pat[ch].getPattern(patNum,true); + for (int i=0; i=2 && blockVersion<6) { // row index + row=reader.readI(); + } else { + row=reader.readC(); + } + + unsigned char nextNote=reader.readC(); + unsigned char nextOctave=reader.readC(); + if (nextNote==0x0d) { + pat->data[row][0]=100; + } else if (nextNote==0x0e) { + pat->data[row][0]=101; + } else if (nextNote==0x01) { + pat->data[row][0]=12; + pat->data[row][1]=nextOctave-1; + } else if (nextNote==0) { + pat->data[row][0]=0; + } else if (nextNote<0x0d) { + pat->data[row][0]=nextNote-1; + pat->data[row][1]=nextOctave; + } + + unsigned char nextIns=reader.readC(); + if (nextIns<0x40) { + pat->data[row][2]=nextIns; + } else { + pat->data[row][2]=-1; + } + + unsigned char nextVol=reader.readC(); + if (nextVol<0x10) { + pat->data[row][3]=nextVol; + } else { + pat->data[row][3]=-1; + } + + int effectCols=ds.subsong[subs]->pat[ch].effectCols; + if (blockVersion>=6) effectCols=4; + + for (int j=0; jdata[row][4+(j*2)]=-1; + pat->data[row][5+(j*2)]=-1; + } else { + if (nextEffectdata[row][4+(j*2)]=ftEffectMap[nextEffect]; + } else { + pat->data[row][4+(j*2)]=-1; + } + pat->data[row][5+(j*2)]=nextEffectVal; + } + } + } + } } else if (blockName=="DPCM SAMPLES") { CHECK_BLOCK_VERSION(1); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="SEQUENCES_VRC6") { // where are the 5B and FDS sequences? CHECK_BLOCK_VERSION(6); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="SEQUENCES_N163") { CHECK_BLOCK_VERSION(1); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="COMMENTS") { CHECK_BLOCK_VERSION(1); + reader.seek(blockSize,SEEK_CUR); } else { logE("block %s is unknown!",blockName); lastError="unknown block "+blockName; @@ -4169,6 +4617,27 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { return false; } } + + addWarning("FamiTracker import is experimental!"); + + ds.version=DIV_VERSION_FTM; + + if (active) quitDispatch(); + BUSY_BEGIN_SOFT; + saveLock.lock(); + song.unload(); + song=ds; + changeSong(0); + recalcChans(); + saveLock.unlock(); + BUSY_END; + if (active) { + initDispatch(); + BUSY_BEGIN; + renderSamples(); + reset(); + BUSY_END; + } } catch (EndOfFileException& e) { logE("premature end of file!"); lastError="incomplete file"; diff --git a/src/engine/macroInt.cpp b/src/engine/macroInt.cpp index 103d9ee6b..e06f61b57 100644 --- a/src/engine/macroInt.cpp +++ b/src/engine/macroInt.cpp @@ -20,6 +20,7 @@ #include "macroInt.h" #include "instrument.h" #include "engine.h" +#include "../ta-log.h" #define ADSR_LOW source.val[0] #define ADSR_HIGH source.val[1] @@ -52,6 +53,7 @@ void DivMacroStruct::doMacro(DivInstrumentMacro& source, bool released, bool tic } if (masked) { had=false; + has=false; return; } if (delay>0) { @@ -246,8 +248,10 @@ void DivMacroInt::setEngine(DivEngine* eng) { } #define ADD_MACRO(m,s) \ - macroList[macroListLen]=&m; \ - macroSource[macroListLen++]=&s; + if (!m.masked) { \ + macroList[macroListLen]=&m; \ + macroSource[macroListLen++]=&s; \ + } void DivMacroInt::init(DivInstrument* which) { ins=which; diff --git a/src/engine/platform/abstract.cpp b/src/engine/platform/abstract.cpp index 74ec8cddf..82694e003 100644 --- a/src/engine/platform/abstract.cpp +++ b/src/engine/platform/abstract.cpp @@ -37,6 +37,10 @@ DivMacroInt* DivDispatch::getChanMacroInt(int chan) { return NULL; } +DivSamplePos DivDispatch::getSamplePos(int chan) { + return DivSamplePos(); +} + DivDispatchOscBuffer* DivDispatch::getOscBuffer(int chan) { return NULL; } diff --git a/src/engine/platform/amiga.cpp b/src/engine/platform/amiga.cpp index a83921adf..b04eefbeb 100644 --- a/src/engine/platform/amiga.cpp +++ b/src/engine/platform/amiga.cpp @@ -213,6 +213,10 @@ void DivPlatformAmiga::rWrite(unsigned short addr, unsigned short val) { //logV("%.3x = %.4x",addr,val); regPool[addr>>1]=val; + if (!skipRegisterWrites && dumpWrites) { + addWrite(addr,val); + } + switch (addr&0x1fe) { case 0x96: { // DMACON if (val&32768) { @@ -400,6 +404,29 @@ void DivPlatformAmiga::tick(bool sysTick) { chan[i].keyOn=true; } } + } + + unsigned short dmaOff=0; + unsigned short dmaOn=0; + for (int i=0; i<4; i++) { + if (chan[i].keyOn || chan[i].keyOff) { + chWrite(i,6,1); + dmaOff|=1<=0 && chan[i].samplesong.sampleLen) { + DivSample* s=parent->getSample(chan[i].sample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { //DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_AMIGA); chan[i].freq=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); @@ -409,13 +436,16 @@ void DivPlatformAmiga::tick(bool sysTick) { chWrite(i,6,chan[i].freq); if (chan[i].keyOn) { - rWrite(0x96,1<=0 && chan[i].samplesong.sampleLen) { DivSample* s=parent->getSample(chan[i].sample); @@ -432,13 +462,21 @@ void DivPlatformAmiga::tick(bool sysTick) { chWrite(i,0,0); chWrite(i,2,0x400); chWrite(i,4,1); + if (dumpWrites) { + addWrite(0x200+i,0x400); + addWrite(0x204+i,1); + } } else { chWrite(i,0,start>>16); chWrite(i,2,start); chWrite(i,4,len); + if (dumpWrites) { + addWrite(0x200+i,start); + addWrite(0x204+i,len); + } } - rWrite(0x96,0x8000|(1<isLoopable()) { int loopPos=(sampleOff[chan[i].sample]+s->getLoopStartPosition(DIV_SAMPLE_DEPTH_8BIT))&(~1); int loopEnd=(s->getLoopEndPosition(DIV_SAMPLE_DEPTH_8BIT)-s->getLoopStartPosition(DIV_SAMPLE_DEPTH_8BIT))>>1; @@ -455,19 +493,28 @@ void DivPlatformAmiga::tick(bool sysTick) { chWrite(i,0,0); chWrite(i,2,0x400); chWrite(i,4,1); + if (dumpWrites) { + addWrite(0x200+i,0x400); + addWrite(0x204+i,1); + } } } } - - if (chan[i].keyOff) { - rWrite(0x96,1<=4) return DivSamplePos(); + if (chan[ch].sample<0 || chan[ch].sample>=parent->song.sampleLen) return DivSamplePos(); + int audPer=amiga.audPer[ch]; + if (audPer<1) audPer=1; + return DivSamplePos( + chan[ch].sample, + amiga.dmaLoc[ch]-sampleOff[chan[ch].sample], + chipClock/audPer + ); +} + void DivPlatformAmiga::notifyInsChange(int ins) { for (int i=0; i<4; i++) { if (chan[i].ins==ins) { diff --git a/src/engine/platform/amiga.h b/src/engine/platform/amiga.h index 1cc5035c4..1c793296f 100644 --- a/src/engine/platform/amiga.h +++ b/src/engine/platform/amiga.h @@ -116,6 +116,7 @@ class DivPlatformAmiga: public DivDispatch { friend void putDispatchChip(void*,int); friend void putDispatchChan(void*,int,int); + friend class DivExportAmigaValidation; void irq(int ch); void rWrite(unsigned short addr, unsigned short val); @@ -136,6 +137,7 @@ class DivPlatformAmiga: public DivDispatch { int getOutputCount(); bool keyOffAffectsArp(int ch); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); void setFlags(const DivConfig& flags); void notifyInsChange(int ins); void notifyWaveChange(int wave); diff --git a/src/engine/platform/ay.cpp b/src/engine/platform/ay.cpp index 181901137..145ab5622 100644 --- a/src/engine/platform/ay.cpp +++ b/src/engine/platform/ay.cpp @@ -700,6 +700,15 @@ DivMacroInt* DivPlatformAY8910::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformAY8910::getSamplePos(int ch) { + if (ch>=3) return DivSamplePos(); + return DivSamplePos( + chan[ch].dac.sample, + chan[ch].dac.pos, + chan[ch].dac.rate + ); +} + DivDispatchOscBuffer* DivPlatformAY8910::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/ay.h b/src/engine/platform/ay.h index f20a71eac..8f938d0a9 100644 --- a/src/engine/platform/ay.h +++ b/src/engine/platform/ay.h @@ -143,6 +143,7 @@ class DivPlatformAY8910: public DivDispatch { int getOutputCount(); bool keyOffAffectsArp(int ch); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); bool getDCOffRequired(); void notifyInsDeletion(void* ins); void poke(unsigned int addr, unsigned short val); diff --git a/src/engine/platform/ay8930.cpp b/src/engine/platform/ay8930.cpp index 0eb509db4..ba4e8099a 100644 --- a/src/engine/platform/ay8930.cpp +++ b/src/engine/platform/ay8930.cpp @@ -696,6 +696,15 @@ DivMacroInt* DivPlatformAY8930::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformAY8930::getSamplePos(int ch) { + if (ch>=3) return DivSamplePos(); + return DivSamplePos( + chan[ch].dac.sample, + chan[ch].dac.pos, + chan[ch].dac.rate + ); +} + DivDispatchOscBuffer* DivPlatformAY8930::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/ay8930.h b/src/engine/platform/ay8930.h index 3ffba63e7..3b47cf0d1 100644 --- a/src/engine/platform/ay8930.h +++ b/src/engine/platform/ay8930.h @@ -145,6 +145,7 @@ class DivPlatformAY8930: public DivDispatch { int getOutputCount(); bool keyOffAffectsArp(int ch); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); void notifyInsDeletion(void* ins); void poke(unsigned int addr, unsigned short val); void poke(std::vector& wlist); diff --git a/src/engine/platform/ga20.cpp b/src/engine/platform/ga20.cpp index 9da33543e..527e80f1d 100644 --- a/src/engine/platform/ga20.cpp +++ b/src/engine/platform/ga20.cpp @@ -336,6 +336,18 @@ DivMacroInt* DivPlatformGA20::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformGA20::getSamplePos(int ch) { + if (ch>=4) return DivSamplePos(); + if (chan[ch].sample<0 || chan[ch].sample>=parent->song.sampleLen) return DivSamplePos(); + if (!ga20.is_playing(ch)) return DivSamplePos(); + unsigned char f=chan[ch].freq; + return DivSamplePos( + chan[ch].sample, + ga20.get_position(ch)-sampleOffGA20[chan[ch].sample], + chipClock/(4*(0x100-(int)f)) + ); +} + DivDispatchOscBuffer* DivPlatformGA20::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/ga20.h b/src/engine/platform/ga20.h index 9cd6869a2..1e06378f1 100644 --- a/src/engine/platform/ga20.h +++ b/src/engine/platform/ga20.h @@ -78,6 +78,7 @@ class DivPlatformGA20: public DivDispatch, public iremga20_intf { virtual int dispatch(DivCommand c) override; virtual void* getChanState(int chan) override; virtual DivMacroInt* getChanMacroInt(int ch) override; + virtual DivSamplePos getSamplePos(int ch) override; virtual DivDispatchOscBuffer* getOscBuffer(int chan) override; virtual unsigned char* getRegisterPool() override; virtual int getRegisterPoolSize() override; diff --git a/src/engine/platform/genesis.cpp b/src/engine/platform/genesis.cpp index 118ebdebc..089d33e27 100644 --- a/src/engine/platform/genesis.cpp +++ b/src/engine/platform/genesis.cpp @@ -172,7 +172,14 @@ void DivPlatformGenesis::acquire_nuked(short** buf, size_t len) { flushFirst=false; } - OPN2_Clock(&fm,o); os[0]+=o[0]; os[1]+=o[1]; + OPN2_Clock(&fm,o); + if (chipType==2) { + os[0]+=CLAMP(o[0],-8192,8191); + os[1]+=CLAMP(o[1],-8192,8191); + } else { + os[0]+=o[0]; + os[1]+=o[1]; + } //OPN2_Write(&fm,0,0); if (i==5) { if (fm.dacen) { @@ -1202,6 +1209,17 @@ DivMacroInt* DivPlatformGenesis::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformGenesis::getSamplePos(int ch) { + if (!chan[5].dacMode) return DivSamplePos(); + if (ch<5) return DivSamplePos(); + if (ch>5 && !softPCM) return DivSamplePos(); + return DivSamplePos( + chan[ch].dacSample, + chan[ch].dacPos, + chan[ch].dacRate + ); +} + DivDispatchOscBuffer* DivPlatformGenesis::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/genesis.h b/src/engine/platform/genesis.h index 4c3d57137..210fca9d0 100644 --- a/src/engine/platform/genesis.h +++ b/src/engine/platform/genesis.h @@ -105,6 +105,7 @@ class DivPlatformGenesis: public DivPlatformOPN { int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); DivDispatchOscBuffer* getOscBuffer(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 7dec2102d..a8b2cb88d 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -415,6 +415,16 @@ DivMacroInt* DivPlatformLynx::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformLynx::getSamplePos(int ch) { + if (ch>=4) return DivSamplePos(); + if (!chan[ch].pcm) return DivSamplePos(); + return DivSamplePos( + chan[ch].sample, + chan[ch].samplePos, + chan[ch].sampleFreq + ); +} + DivDispatchOscBuffer* DivPlatformLynx::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/lynx.h b/src/engine/platform/lynx.h index 7cde207e4..c68106dec 100644 --- a/src/engine/platform/lynx.h +++ b/src/engine/platform/lynx.h @@ -72,6 +72,7 @@ class DivPlatformLynx: public DivDispatch { int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); DivDispatchOscBuffer* getOscBuffer(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); diff --git a/src/engine/platform/n163.cpp b/src/engine/platform/n163.cpp index afd4898a0..ead44cc1e 100644 --- a/src/engine/platform/n163.cpp +++ b/src/engine/platform/n163.cpp @@ -19,6 +19,7 @@ #include "n163.h" #include "../engine.h" +#include "../../ta-log.h" #include #define rRead(a,v) n163.addr_w(a); n163.data_r(v); @@ -166,6 +167,7 @@ void DivPlatformN163::updateWave(int ch, int wave, int pos, int len) { void DivPlatformN163::updateWaveCh(int ch) { if (ch<=chanMax) { + logV("updateWave with pos %d and len %d",chan[ch].wavePos,chan[ch].waveLen); updateWave(ch,-1,chan[ch].wavePos,chan[ch].waveLen); if (chan[ch].active && !isMuted[ch]) { chan[ch].volumeChanged=true; @@ -337,15 +339,15 @@ int DivPlatformN163::dispatch(DivCommand c) { DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_N163); if (chan[c.chan].insChanged) { chan[c.chan].wave=ins->n163.wave; - chan[c.chan].ws.changeWave1(chan[c.chan].wave); chan[c.chan].wavePos=ins->n163.wavePos; chan[c.chan].waveLen=ins->n163.waveLen; chan[c.chan].waveMode=ins->n163.waveMode; + chan[c.chan].ws.init(NULL,chan[c.chan].waveLen,15,false); + chan[c.chan].ws.changeWave1(chan[c.chan].wave); chan[c.chan].waveChanged=true; if (chan[c.chan].waveMode&0x3 || ins->ws.enabled) { chan[c.chan].waveUpdated=true; } - chan[c.chan].insChanged=false; } if (c.value!=DIV_NOTE_NULL) { chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); @@ -360,6 +362,7 @@ int DivPlatformN163::dispatch(DivCommand c) { } chan[c.chan].macroInit(ins); chan[c.chan].ws.init(ins,chan[c.chan].waveLen,15,chan[c.chan].insChanged); + chan[c.chan].insChanged=false; break; } case DIV_CMD_NOTE_OFF: diff --git a/src/engine/platform/namcowsg.cpp b/src/engine/platform/namcowsg.cpp index 10eb3453c..0fe2ccd34 100644 --- a/src/engine/platform/namcowsg.cpp +++ b/src/engine/platform/namcowsg.cpp @@ -183,6 +183,7 @@ void DivPlatformNamcoWSG::acquire(short** buf, size_t len) { } void DivPlatformNamcoWSG::updateWave(int ch) { + if (romMode) return; if (devType==30) { for (int i=0; i<32; i++) { ((namco_cus30_device*)namco)->namcos1_cus30_w(i+ch*32,chan[ch].ws.output[i]); @@ -291,9 +292,9 @@ void DivPlatformNamcoWSG::tick(bool sysTick) { rWrite(0x1d,(chan[2].freq>>12)&15); rWrite(0x1e,(chan[2].freq>>16)&15); - rWrite(0x05,0); - rWrite(0x0a,1); - rWrite(0x0f,2); + rWrite(0x05,romMode?(chan[0].wave&7):0); + rWrite(0x0a,romMode?(chan[1].wave&7):1); + rWrite(0x0f,romMode?(chan[2].wave&7):2); break; case 15: for (int i=0; i<8; i++) { @@ -304,7 +305,7 @@ void DivPlatformNamcoWSG::tick(bool sysTick) { } rWrite((i<<3)+0x04,chan[i].freq&0xff); rWrite((i<<3)+0x05,(chan[i].freq>>8)&0xff); - rWrite((i<<3)+0x06,((chan[i].freq>>16)&15)|(i<<4)); + rWrite((i<<3)+0x06,((chan[i].freq>>16)&15)|((romMode?(chan[i].wave&7):i)<<4)); } break; case 30: @@ -496,10 +497,11 @@ void DivPlatformNamcoWSG::reset() { if (dumpWrites) { addWrite(0xffffffff,0); } - // TODO: wave memory namco->set_voices(chans); namco->set_stereo((devType==2 || devType==30)); namco->device_start(NULL); + + updateROMWaves(); } int DivPlatformNamcoWSG::getOutputCount() { @@ -510,6 +512,27 @@ bool DivPlatformNamcoWSG::keyOffAffectsArp(int ch) { return true; } +void DivPlatformNamcoWSG::updateROMWaves() { + if (romMode) { + // copy wavetables + for (int i=0; i<8; i++) { + int data=0; + DivWavetable* w=parent->getWave(i); + + for (int j=0; j<32; j++) { + if (w->max<1 || w->len<1) { + data=0; + } else { + data=w->data[j*w->len/32]*15/w->max; + if (data<0) data=0; + if (data>15) data=15; + } + namco->update_namco_waveform(i*32+j,data); + } + } + } +} + void DivPlatformNamcoWSG::notifyWaveChange(int wave) { for (int i=0; irate=rate; } newNoise=flags.getBool("newNoise",true); + romMode=flags.getBool("romMode",true); + if (devType==30) romMode=false; } void DivPlatformNamcoWSG::poke(unsigned int addr, unsigned short val) { diff --git a/src/engine/platform/namcowsg.h b/src/engine/platform/namcowsg.h index dcc52549f..9d418a9d2 100644 --- a/src/engine/platform/namcowsg.h +++ b/src/engine/platform/namcowsg.h @@ -50,8 +50,10 @@ class DivPlatformNamcoWSG: public DivDispatch { namco_audio_device* namco; int devType, chans; bool newNoise; + bool romMode; unsigned char regPool[512]; void updateWave(int ch); + void updateROMWaves(); friend void putDispatchChip(void*,int); friend void putDispatchChan(void*,int,int); public: diff --git a/src/engine/platform/pce.cpp b/src/engine/platform/pce.cpp index 2ed98b36b..95d483264 100644 --- a/src/engine/platform/pce.cpp +++ b/src/engine/platform/pce.cpp @@ -505,6 +505,16 @@ DivMacroInt* DivPlatformPCE::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformPCE::getSamplePos(int ch) { + if (ch>=6) return DivSamplePos(); + if (!chan[ch].pcm) return DivSamplePos(); + return DivSamplePos( + chan[ch].dacSample, + chan[ch].dacPos, + chan[ch].dacRate + ); +} + DivDispatchOscBuffer* DivPlatformPCE::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/pce.h b/src/engine/platform/pce.h index fd4f81320..9b8c610c8 100644 --- a/src/engine/platform/pce.h +++ b/src/engine/platform/pce.h @@ -81,6 +81,7 @@ class DivPlatformPCE: public DivDispatch { int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); DivDispatchOscBuffer* getOscBuffer(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); diff --git a/src/engine/platform/pcmdac.cpp b/src/engine/platform/pcmdac.cpp index 5f1911f25..f7b3af715 100644 --- a/src/engine/platform/pcmdac.cpp +++ b/src/engine/platform/pcmdac.cpp @@ -30,7 +30,7 @@ void DivPlatformPCMDAC::acquire(short** buf, size_t len) { const int depthScale=(15-outDepth); int output=0; for (size_t h=0; hdata[oscBuf->needle++]=0; @@ -171,7 +171,11 @@ void DivPlatformPCMDAC::acquire(short** buf, size_t len) { } } } - output=output*chan[0].vol*chan[0].envVol/16384; + if (isMuted) { + output=0; + } else { + output=output*chan[0].vol*chan[0].envVol/16384; + } oscBuf->data[oscBuf->needle++]=output; if (outStereo) { buf[0][h]=((output*chan[0].panL)>>(depthScale+8))<=1) return DivSamplePos(); + return DivSamplePos( + chan[ch].sample, + chan[ch].audPos, + chan[ch].freq + ); +} + void DivPlatformPCMDAC::notifyInsChange(int ins) { if (chan[0].ins==ins) { chan[0].insChanged=true; diff --git a/src/engine/platform/pcmdac.h b/src/engine/platform/pcmdac.h index e32b7b099..f9435e3e0 100644 --- a/src/engine/platform/pcmdac.h +++ b/src/engine/platform/pcmdac.h @@ -79,6 +79,7 @@ class DivPlatformPCMDAC: public DivDispatch { void muteChannel(int ch, bool mute); int getOutputCount(); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); void setFlags(const DivConfig& flags); void notifyInsChange(int ins); void notifyWaveChange(int wave); diff --git a/src/engine/platform/pv1000.cpp b/src/engine/platform/pv1000.cpp index 823ca871b..d5f54e6a6 100644 --- a/src/engine/platform/pv1000.cpp +++ b/src/engine/platform/pv1000.cpp @@ -40,7 +40,7 @@ void DivPlatformPV1000::acquire(short** buf, size_t len) { for (size_t h=0; hdata[oscBuf[i]->needle++]=(d65010g031.square[i].out<<12); } @@ -263,6 +263,10 @@ void DivPlatformPV1000::poke(std::vector& wlist) { for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); } +bool DivPlatformPV1000::getDCOffRequired() { + return true; +} + int DivPlatformPV1000::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { parent=p; dumpWrites=false; diff --git a/src/engine/platform/pv1000.h b/src/engine/platform/pv1000.h index 953709abe..c32540763 100644 --- a/src/engine/platform/pv1000.h +++ b/src/engine/platform/pv1000.h @@ -21,7 +21,7 @@ #define _PV1000_H #include "../dispatch.h" -#include "sound/d65010g031.h" +#include "sound/d65modified.h" #include class DivPlatformPV1000: public DivDispatch { @@ -55,6 +55,7 @@ class DivPlatformPV1000: public DivDispatch { void poke(unsigned int addr, unsigned short val); void poke(std::vector& wlist); const char** getRegisterSheet(); + bool getDCOffRequired(); int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags); void quit(); ~DivPlatformPV1000(); diff --git a/src/engine/platform/rf5c68.cpp b/src/engine/platform/rf5c68.cpp index 7301bb40c..149098b8c 100644 --- a/src/engine/platform/rf5c68.cpp +++ b/src/engine/platform/rf5c68.cpp @@ -307,6 +307,7 @@ void DivPlatformRF5C68::forceIns() { chan[i].insChanged=true; chan[i].freqChanged=true; chan[i].sample=-1; + chWrite(i,1,isMuted[i]?0:chan[i].panning); } } diff --git a/src/engine/platform/segapcm.cpp b/src/engine/platform/segapcm.cpp index 0b02bb408..a4003a2be 100644 --- a/src/engine/platform/segapcm.cpp +++ b/src/engine/platform/segapcm.cpp @@ -382,6 +382,17 @@ DivMacroInt* DivPlatformSegaPCM::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformSegaPCM::getSamplePos(int ch) { + if (ch>=16) return DivSamplePos(); + if (chan[ch].pcm.sample<0 || chan[ch].pcm.sample>=parent->song.sampleLen) return DivSamplePos(); + if (!pcm.is_playing(ch)) return DivSamplePos(); + return DivSamplePos( + chan[ch].pcm.sample, + pcm.get_addr(ch)-sampleOffSegaPCM[chan[ch].pcm.sample], + 122*(chan[ch].pcm.freq+1) + ); +} + DivDispatchOscBuffer* DivPlatformSegaPCM::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/segapcm.h b/src/engine/platform/segapcm.h index f99a24df3..9e2ad5df1 100644 --- a/src/engine/platform/segapcm.h +++ b/src/engine/platform/segapcm.h @@ -87,6 +87,7 @@ class DivPlatformSegaPCM: public DivDispatch { int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); DivDispatchOscBuffer* getOscBuffer(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); diff --git a/src/engine/platform/snes.cpp b/src/engine/platform/snes.cpp index 7184cdc43..1fb5a12cd 100644 --- a/src/engine/platform/snes.cpp +++ b/src/engine/platform/snes.cpp @@ -681,6 +681,20 @@ DivMacroInt* DivPlatformSNES::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformSNES::getSamplePos(int ch) { + if (ch>=8) return DivSamplePos(); + if (!chan[ch].active) return DivSamplePos(); + if (chan[ch].sample<0 || chan[ch].sample>=parent->song.sampleLen) return DivSamplePos(); + const SPC_DSP::voice_t* v=dsp.get_voice(ch); + // TODO: fix? + if (sampleMem[v->brr_addr&0xffff]==0) return DivSamplePos(); + return DivSamplePos( + chan[ch].sample, + ((v->brr_addr-sampleOff[chan[ch].sample])*16/9)+v->brr_offset, + (chan[ch].freq*125)/16 + ); +} + DivDispatchOscBuffer* DivPlatformSNES::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/snes.h b/src/engine/platform/snes.h index 8783e61c3..3c3426466 100644 --- a/src/engine/platform/snes.h +++ b/src/engine/platform/snes.h @@ -97,6 +97,7 @@ class DivPlatformSNES: public DivDispatch { int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); DivDispatchOscBuffer* getOscBuffer(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); diff --git a/src/engine/platform/sound/d65010g031.c b/src/engine/platform/sound/d65modified.c similarity index 74% rename from src/engine/platform/sound/d65010g031.c rename to src/engine/platform/sound/d65modified.c index dbcd63784..8f53b34c8 100644 --- a/src/engine/platform/sound/d65010g031.c +++ b/src/engine/platform/sound/d65modified.c @@ -34,9 +34,31 @@ freely, subject to the following restrictions: TODO: - needs hardware test +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! +ALTERED VERSION!!! + + +THIS IS **NOT** NOT NOT NOT!!!! THE ORIGINAL SOFTWARE +IT ISN'T +THE MODIFICATIONS THAT WERE MADE ARE: + +1. FIX VOLUMES - APPARENTLY THE SQUARES HAVE DIFFERENT VOLUMES (thanks forple) + */ -#include "d65010g031.h" +#include "d65modified.h" #include static int d65010g031_max(int a, int b) { return (a > b) ? a : b; } @@ -57,12 +79,20 @@ int d65010g031_square_tick(struct d65010g031_square_t *square, const int cycle) return 0; } +// this is the bit I altered +// THIS IS **NOT** THE ORIGINAL SOFTWARE! I am plainly marking it as such! +const int d65Volumes[3]={ + 3840, + 5120, + 8192 +}; + int d65010g031_sound_tick(struct d65010g031_t *d65010g031, const int cycle) { int out = 0; for (int i = 0; i < 3; i++) { - out += d65010g031_square_tick(&d65010g031->square[i], cycle); + out += d65010g031_square_tick(&d65010g031->square[i], cycle)?d65Volumes[i]:-d65Volumes[i]; } return out; } diff --git a/src/engine/platform/sound/d65010g031.h b/src/engine/platform/sound/d65modified.h similarity index 100% rename from src/engine/platform/sound/d65010g031.h rename to src/engine/platform/sound/d65modified.h diff --git a/src/engine/platform/sound/ga20/iremga20.h b/src/engine/platform/sound/ga20/iremga20.h index dc29d86f4..1a27891ec 100644 --- a/src/engine/platform/sound/ga20/iremga20.h +++ b/src/engine/platform/sound/ga20/iremga20.h @@ -39,12 +39,19 @@ public: u8 read(u32 offset); inline void set_mute(const int ch, const bool mute) { m_channel[ch & 3].mute = mute; } + inline unsigned int get_position(const int ch) { + return m_channel[ch&3].pos; + } + inline bool is_playing(const int ch) { + return m_channel[ch&3].play; + } // device-level overrides void device_reset(); // sound stream update overrides void sound_stream_update(short** outputs, int len); + private: struct channel_def diff --git a/src/engine/platform/sound/segapcm.cpp b/src/engine/platform/sound/segapcm.cpp index 4dd58eb26..d469e3a15 100644 --- a/src/engine/platform/sound/segapcm.cpp +++ b/src/engine/platform/sound/segapcm.cpp @@ -134,6 +134,18 @@ uint8_t* segapcm_device::get_ram() { return m_ram; } +unsigned int segapcm_device::get_addr(int ch) { + uint8_t *regs = &m_ram[8*ch]; + int offset = (regs[0x86] & m_bankmask) << m_bankshift; + uint32_t addr = (regs[0x85] << 8) | (regs[0x84]) | offset; + return addr; +} + +bool segapcm_device::is_playing(int ch) { + uint8_t *regs = &m_ram[8*ch]; + return !(regs[0x86]&1); +} + void segapcm_device::mute(int ch, bool doMute) { m_muted[ch&15]=doMute; -} \ No newline at end of file +} diff --git a/src/engine/platform/sound/segapcm.h b/src/engine/platform/sound/segapcm.h index 40ca35a4b..819202cd7 100644 --- a/src/engine/platform/sound/segapcm.h +++ b/src/engine/platform/sound/segapcm.h @@ -34,6 +34,8 @@ public: void write(unsigned int offset, uint8_t data); uint8_t read(unsigned int offset); uint8_t* get_ram(); + unsigned int get_addr(int ch); + bool is_playing(int ch); void mute(int ch, bool doMute); // device-level overrides diff --git a/src/engine/platform/sound/snes/SPC_DSP.h b/src/engine/platform/sound/snes/SPC_DSP.h index 879ee703d..924d9ae2c 100644 --- a/src/engine/platform/sound/snes/SPC_DSP.h +++ b/src/engine/platform/sound/snes/SPC_DSP.h @@ -123,6 +123,9 @@ public: uint8_t t_envx_out; sample_t out[2]; // Furnace addition, for per-channel oscilloscope }; + + // Furnace addition, gets a voice + const voice_t* get_voice(int n); private: enum { brr_block_size = 9 }; @@ -298,6 +301,10 @@ inline void SPC_DSP::get_voice_outputs( sample_t* outs ) } } +inline const SPC_DSP::voice_t* SPC_DSP::get_voice(int n) { + return &m.voices[n]; +} + #if !SPC_NO_COPY_STATE_FUNCS class SPC_State_Copier { diff --git a/src/engine/platform/vic20.cpp b/src/engine/platform/vic20.cpp index 253c4fb76..bd25b5284 100644 --- a/src/engine/platform/vic20.cpp +++ b/src/engine/platform/vic20.cpp @@ -19,6 +19,7 @@ #include "vic20.h" #include "../engine.h" +#include "../../ta-log.h" #include #define rWrite(a,v) {regPool[(a)]=(v)&0xff; vic_sound_machine_store(vic,a,(v)&0xff);} @@ -79,9 +80,7 @@ void DivPlatformVIC20::calcAndWriteOutVol(int ch, int env) { } void DivPlatformVIC20::writeOutVol(int ch) { - if (!isMuted[ch] && chan[ch].active) { - rWrite(14,chan[ch].outVol); - } + rWrite(14,chan[ch].outVol); } void DivPlatformVIC20::tick(bool sysTick) { @@ -99,6 +98,20 @@ void DivPlatformVIC20::tick(bool sysTick) { } chan[i].freqChanged=true; } + if (chan[i].std.duty.had) { + if (chan[i].onOff!=(bool)chan[i].std.duty.val) { + chan[i].onOff=(bool)chan[i].std.duty.val; + if (chan[i].active) { + if (chan[i].onOff) { + chan[i].keyOn=true; + chan[i].keyOff=false; + } else { + chan[i].keyOn=false; + chan[i].keyOff=true; + } + } + } + } if (chan[i].std.wave.had) { if (chan[i].wave!=chan[i].std.wave.val) { chan[i].wave=chan[i].std.wave.val&0x0f; @@ -156,6 +169,7 @@ int DivPlatformVIC20::dispatch(DivCommand c) { chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; } + chan[c.chan].onOff=true; chan[c.chan].active=true; chan[c.chan].keyOn=true; chan[c.chan].macroInit(ins); diff --git a/src/engine/platform/vic20.h b/src/engine/platform/vic20.h index 1b8584a75..5125bd96a 100644 --- a/src/engine/platform/vic20.h +++ b/src/engine/platform/vic20.h @@ -27,10 +27,12 @@ class DivPlatformVIC20: public DivDispatch { struct Channel: public SharedChannel { int wave, waveWriteCycle; + bool onOff; Channel(): SharedChannel(15), wave(0), - waveWriteCycle(-1) {} + waveWriteCycle(-1), + onOff(true) {} }; Channel chan[4]; DivDispatchOscBuffer* oscBuf[4]; diff --git a/src/engine/platform/vrc6.cpp b/src/engine/platform/vrc6.cpp index b08255d36..912a589ea 100644 --- a/src/engine/platform/vrc6.cpp +++ b/src/engine/platform/vrc6.cpp @@ -448,6 +448,16 @@ DivMacroInt* DivPlatformVRC6::getChanMacroInt(int ch) { return &chan[ch].std; } +DivSamplePos DivPlatformVRC6::getSamplePos(int ch) { + if (ch>=2) return DivSamplePos(); + if (!chan[ch].pcm) return DivSamplePos(); + return DivSamplePos( + chan[ch].dacSample, + chan[ch].dacPos, + chan[ch].dacRate + ); +} + DivDispatchOscBuffer* DivPlatformVRC6::getOscBuffer(int ch) { return oscBuf[ch]; } diff --git a/src/engine/platform/vrc6.h b/src/engine/platform/vrc6.h index 5688cb5c6..5a2416101 100644 --- a/src/engine/platform/vrc6.h +++ b/src/engine/platform/vrc6.h @@ -65,6 +65,7 @@ class DivPlatformVRC6: public DivDispatch, public vrcvi_intf { int dispatch(DivCommand c); void* getChanState(int chan); DivMacroInt* getChanMacroInt(int ch); + DivSamplePos getSamplePos(int ch); DivDispatchOscBuffer* getOscBuffer(int chan); unsigned char* getRegisterPool(); int getRegisterPoolSize(); diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 9176a0678..c77afaee5 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -278,7 +278,7 @@ int DivEngine::dispatchCmd(DivCommand c) { cmdStream.push_back(c); } - if (output) if (!skipping && output->midiOut!=NULL) { + if (output) if (!skipping && output->midiOut!=NULL && !isChannelMuted(c.chan)) { if (output->midiOut->isDeviceOpen()) { if (midiOutMode==DIV_MIDI_MODE_NOTE) { int scaledVol=(chan[c.chan].volume*127)/MAX(1,chan[c.chan].volMax); @@ -305,7 +305,7 @@ int DivEngine::dispatchCmd(DivCommand c) { chan[c.chan].curMidiNote=-1; break; case DIV_CMD_INSTRUMENT: - if (chan[c.chan].lastIns!=c.value) { + if (chan[c.chan].lastIns!=c.value && midiOutProgramChange) { output->midiOut->send(TAMidiMessage(0xc0|(c.chan&15),c.value,0)); } break; @@ -1385,6 +1385,15 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) { } } + if (subticks==tickMult && cmdStreamInt) { + if (!cmdStreamInt->tick()) { + cmdStreamInt->cleanup(); + delete cmdStreamInt; + cmdStreamInt=NULL; + } + } + + firstTick=false; if (shallStop) { diff --git a/src/engine/safeReader.cpp b/src/engine/safeReader.cpp index cf2effbec..5ac416e27 100644 --- a/src/engine/safeReader.cpp +++ b/src/engine/safeReader.cpp @@ -236,10 +236,14 @@ String SafeReader::readString(size_t stlen) { #endif size_t curPos=0; if (isEOF()) throw EndOfFileException(this, len); + bool zero=false; while (!isEOF() && curPos>24)&0xff), + (unsigned char)((val>>16)&0xff), + (unsigned char)((val>>8)&0xff), + (unsigned char)(val&0xff) + }; + return write(bytes,4); +} + int SafeWriter::writeL(int64_t val) { return write(&val,8); } diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 80c987e28..fbb718cb5 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -789,7 +789,7 @@ void DivEngine::registerSystems() { {"S1", "S2", "S3"}, {DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE}, {DIV_INS_AY, DIV_INS_AY, DIV_INS_AY}, - {}, + {DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, {}, ayPostEffectHandlerMap ); @@ -870,7 +870,7 @@ void DivEngine::registerSystems() { {"S1", "S2", "S3"}, {DIV_CH_PULSE, DIV_CH_PULSE, DIV_CH_PULSE}, {DIV_INS_AY8930, DIV_INS_AY8930, DIV_INS_AY8930}, - {}, + {DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA}, {}, ay8930PostEffectHandlerMap ); diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index fdcd80720..01833d26f 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -583,8 +583,8 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write logD("writing stream command %x:%x with stream ID %d",write.addr,write.val,streamID); switch (write.addr&0xff) { case 0: // play sample - if (write.valgetWave(num); + logV("changeWave1 (%d)",width); if (width<1) return; for (int i=0; imax<1 || w1->len<1) { diff --git a/src/gui/dataList.cpp b/src/gui/dataList.cpp index 73197a815..2a37c7e8e 100644 --- a/src/gui/dataList.cpp +++ b/src/gui/dataList.cpp @@ -455,12 +455,14 @@ void FurnaceGUI::drawInsList(bool asChild) { if (ImGui::Selectable(name.c_str(),(i==-1)?(curIns<0 || curIns>=e->song.insLen):(curIns==i))) { curIns=i; wavePreviewInit=true; + updateFMPreview=true; } if (wantScrollList && curIns==i) ImGui::SetScrollHereY(); if (settings.insFocusesPattern && patternOpen && ImGui::IsItemActivated()) { nextWindow=GUI_WINDOW_PATTERN; curIns=i; wavePreviewInit=true; + updateFMPreview=true; } if (ImGui::IsItemHovered() && i>=0 && !mobileUI) { ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]); @@ -474,6 +476,7 @@ void FurnaceGUI::drawInsList(bool asChild) { if (i>=0) { if (ImGui::BeginPopupContextItem("InsRightMenu")) { curIns=i; + updateFMPreview=true; ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]); if (ImGui::MenuItem("replace...")) { doAction((curIns>=0 && curIns<(int)e->song.ins.size())?GUI_ACTION_INS_LIST_OPEN_REPLACE:GUI_ACTION_INS_LIST_OPEN); diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index d7179204c..ae1a1dfce 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -58,6 +58,8 @@ void FurnaceGUI::drawDebug() { ImGui::SameLine(); if (ImGui::Button("Pattern Advance")) e->haltWhen(DIV_HALT_PATTERN); + if (ImGui::Button("Play Command Stream")) openFileDialog(GUI_FILE_CMDSTREAM_OPEN); + if (ImGui::Button("Panic")) e->syncReset(); ImGui::SameLine(); if (ImGui::Button("Abort")) { @@ -519,6 +521,14 @@ void FurnaceGUI::drawDebug() { ImGui::InputFloat("maxRr",&maxRr); ImGui::TreePop(); } + if (ImGui::TreeNode("FM Preview")) { + float asFloat[FM_PREVIEW_SIZE]; + for (int i=0; i=(int)e->song.ins.size()) { @@ -134,6 +135,7 @@ void FurnaceGUI::doAction(int what) { } wavePreviewInit=true; wantScrollList=true; + updateFMPreview=true; break; case GUI_ACTION_STEP_UP: if (++editStep>64) editStep=64; @@ -593,6 +595,7 @@ void FurnaceGUI::doAction(int what) { wantScrollList=true; MARK_MODIFIED; wavePreviewInit=true; + updateFMPreview=true; } break; case GUI_ACTION_INS_LIST_DUPLICATE: @@ -606,6 +609,7 @@ void FurnaceGUI::doAction(int what) { wantScrollList=true; MARK_MODIFIED; wavePreviewInit=true; + updateFMPreview=true; } } break; @@ -653,11 +657,13 @@ void FurnaceGUI::doAction(int what) { if (--curIns<0) curIns=0; wantScrollList=true; wavePreviewInit=true; + updateFMPreview=true; break; case GUI_ACTION_INS_LIST_DOWN: if (++curIns>=(int)e->song.ins.size()) curIns=((int)e->song.ins.size())-1; wantScrollList=true; wavePreviewInit=true; + updateFMPreview=true; break; case GUI_ACTION_WAVE_LIST_ADD: @@ -1366,6 +1372,7 @@ void FurnaceGUI::doAction(int what) { nextWindow=GUI_WINDOW_INS_EDIT; MARK_MODIFIED; wavePreviewInit=true; + updateFMPreview=true; } break; } diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp index f6869ee10..98babf574 100644 --- a/src/gui/editControls.cpp +++ b/src/gui/editControls.cpp @@ -19,7 +19,9 @@ #define _USE_MATH_DEFINES #include "gui.h" +#include "../fileutils.h" #include "IconsFontAwesome4.h" +#include "misc/cpp/imgui_stdlib.h" #include // 0: all directions @@ -514,11 +516,31 @@ void FurnaceGUI::drawMobileControls() { openFileDialog(GUI_FILE_EXPORT_VGM); } - ImGui::Button("CmdStream"); + if (ImGui::Button("CmdStream")) { + openFileDialog(GUI_FILE_EXPORT_CMDSTREAM_BINARY); + } + ImGui::SameLine(); + if (ImGui::Button("CmdStream Text")) { + openFileDialog(GUI_FILE_EXPORT_CMDSTREAM); + } ImGui::Separator(); - drawSongInfo(true); + if (ImGui::BeginTabBar("MobileSong")) { + if (ImGui::BeginTabItem("Song Info")) { + drawSongInfo(true); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Subsongs")) { + drawSubSongs(true); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Speed")) { + drawSpeed(true); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } break; } case GUI_SCENE_CHANNELS: @@ -575,6 +597,40 @@ void FurnaceGUI::drawMobileControls() { if (ImGui::Button("Switch to Desktop Mode")) { toggleMobileUI(!mobileUI); } + + int numAmiga=0; + for (int i=0; isong.systemLen; i++) { + if (e->song.system[i]==DIV_SYSTEM_AMIGA) numAmiga++; + } + + if (numAmiga) { + ImGui::Text( + "this is NOT ROM export! only use for making sure the\n" + "Furnace Amiga emulator is working properly by\n" + "comparing it with real Amiga output." + ); + ImGui::Text("Directory"); + ImGui::SameLine(); + ImGui::InputText("##AVDPath",&workingDirROMExport); + if (ImGui::Button("Bake Data")) { + std::vector out=e->buildROM(DIV_ROM_AMIGA_VALIDATION); + if (workingDirROMExport.size()>0) { + if (workingDirROMExport[workingDirROMExport.size()-1]!=DIR_SEPARATOR) workingDirROMExport+=DIR_SEPARATOR_STR; + } + for (DivROMExportOutput& i: out) { + String path=workingDirROMExport+i.name; + FILE* outFile=ps_fopen(path.c_str(),"wb"); + if (outFile!=NULL) { + fwrite(i.data->getFinalBuf(),1,i.data->size(),outFile); + fclose(outFile); + } + i.data->finish(); + delete i.data; + } + showError(fmt::sprintf("Done! Baked %d files.",(int)out.size())); + } + } + break; } } diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 0d8341b28..77d76da24 100644 --- a/src/gui/editing.cpp +++ b/src/gui/editing.cpp @@ -1005,6 +1005,13 @@ void FurnaceGUI::doUndo() { break; } + if (curOrder>=e->curSubSong->ordersLen) { + curOrder=e->curSubSong->ordersLen-1; + oldOrder=curOrder; + oldOrder1=curOrder; + e->setOrder(curOrder); + } + undoHist.pop_back(); } @@ -1058,5 +1065,12 @@ void FurnaceGUI::doRedo() { break; } + if (curOrder>=e->curSubSong->ordersLen) { + curOrder=e->curSubSong->ordersLen-1; + oldOrder=curOrder; + oldOrder1=curOrder; + e->setOrder(curOrder); + } + redoHist.pop_back(); } diff --git a/src/gui/fmPreview.cpp b/src/gui/fmPreview.cpp new file mode 100644 index 000000000..eaf449ed9 --- /dev/null +++ b/src/gui/fmPreview.cpp @@ -0,0 +1,86 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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. + */ + +#define _USE_MATH_DEFINES +#include "gui.h" +#include "../../extern/opn/ym3438.h" + +#define FM_WRITE(addr,val) \ + OPN2_Write((ym3438_t*)fmPreviewOPN,0,(addr)); \ + do { \ + OPN2_Clock((ym3438_t*)fmPreviewOPN,out); \ + } while (((ym3438_t*)fmPreviewOPN)->write_busy); \ + OPN2_Write((ym3438_t*)fmPreviewOPN,1,(val)); \ + do { \ + OPN2_Clock((ym3438_t*)fmPreviewOPN,out); \ + } while (((ym3438_t*)fmPreviewOPN)->write_busy); \ + +const unsigned char dtTableFMP[8]={ + 7,6,5,0,1,2,3,4 +}; + +void FurnaceGUI::renderFMPreview(const DivInstrumentFM& params, int pos) { + if (fmPreviewOPN==NULL) { + fmPreviewOPN=new ym3438_t; + } + short out[2]; + int aOut=0; + bool mult0=false; + + if (pos==0) { + OPN2_Reset((ym3438_t*)fmPreviewOPN); + OPN2_SetChipType((ym3438_t*)fmPreviewOPN,ym3438_mode_opn); + + // set params + for (int i=0; i<4; i++) { + if ((params.op[i].mult&15)==0) { + mult0=true; + break; + } + } + for (int i=0; i<4; i++) { + const DivInstrumentFM::Operator& op=params.op[i]; + unsigned short baseAddr=i*4; + FM_WRITE(baseAddr+0x40,op.tl); + FM_WRITE(baseAddr+0x30,(op.mult&15)|(dtTableFMP[op.dt&7]<<4)); + FM_WRITE(baseAddr+0x50,(op.ar&31)|(op.rs<<6)); + FM_WRITE(baseAddr+0x60,(op.dr&31)|(op.am<<7)); + FM_WRITE(baseAddr+0x70,op.d2r&31); + FM_WRITE(baseAddr+0x80,(op.rr&15)|(op.sl<<4)); + FM_WRITE(baseAddr+0x90,op.ssgEnv&15); + } + FM_WRITE(0xb0,(params.alg&7)|((params.fb&7)<<3)); + FM_WRITE(0xb4,0xc0|(params.fms&7)|((params.ams&3)<<4)); + FM_WRITE(0xa4,mult0?0x1c:0x14); // frequency + FM_WRITE(0xa0,0); + FM_WRITE(0x28,0xf0); // key on + } + + // render + for (int i=0; ich_out[0]; + if (aOut<-32768) aOut=-32768; + if (aOut>32767) aOut=32767; + fmPreview[i]=aOut; + } +} diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 2c96ceeb8..7c23114ad 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1175,6 +1175,7 @@ void FurnaceGUI::valueInput(int num, bool direct, int target) { if (settings.absorbInsInput) { curIns=pat->data[cursor.y][target]; wavePreviewInit=true; + updateFMPreview=true; } makeUndo(GUI_UNDO_PATTERN_EDIT); if (direct) { @@ -1736,9 +1737,18 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirROMExport)) workingDirROMExport=getHomeDir(); hasOpened=fileDialog->openSave( "Export Command Stream", - {"text file", "*.txt", - "binary file", "*.bin"}, - "text file{.txt},binary file{.bin}", + {"text file", "*.txt"}, + "text file{.txt}", + workingDirROMExport, + dpiScale + ); + break; + case GUI_FILE_EXPORT_CMDSTREAM_BINARY: + if (!dirExists(workingDirROMExport)) workingDirROMExport=getHomeDir(); + hasOpened=fileDialog->openSave( + "Export Command Stream", + {"binary file", "*.bin"}, + "binary file{.bin}", workingDirROMExport, dpiScale ); @@ -1839,6 +1849,17 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { dpiScale ); break; + case GUI_FILE_CMDSTREAM_OPEN: + if (!dirExists(workingDirROM)) workingDirROM=getHomeDir(); + hasOpened=fileDialog->openLoad( + "Play Command Stream", + {"command stream", "*.bin", + "all files", "*"}, + "command stream{.bin},.*", + workingDirROM, + dpiScale + ); + break; case GUI_FILE_TEST_OPEN: if (!dirExists(workingDirTest)) workingDirTest=getHomeDir(); hasOpened=fileDialog->openLoad( @@ -2093,6 +2114,64 @@ void FurnaceGUI::pushRecentFile(String path) { } } +int FurnaceGUI::loadStream(String path) { + if (!path.empty()) { + logI("loading stream..."); + FILE* f=ps_fopen(path.c_str(),"rb"); + if (f==NULL) { + perror("error"); + lastError=strerror(errno); + return 1; + } + if (fseek(f,0,SEEK_END)<0) { + perror("size error"); + lastError=fmt::sprintf("on seek: %s",strerror(errno)); + fclose(f); + return 1; + } + ssize_t len=ftell(f); + if (len==(SIZE_MAX>>1)) { + perror("could not get file length"); + lastError=fmt::sprintf("on pre tell: %s",strerror(errno)); + fclose(f); + return 1; + } + if (len<1) { + if (len==0) { + logE("that file is empty!"); + lastError="file is empty"; + } else { + perror("tell error"); + lastError=fmt::sprintf("on tell: %s",strerror(errno)); + } + fclose(f); + return 1; + } + if (fseek(f,0,SEEK_SET)<0) { + perror("size error"); + lastError=fmt::sprintf("on get size: %s",strerror(errno)); + fclose(f); + return 1; + } + unsigned char* file=new unsigned char[len]; + if (fread(file,1,(size_t)len,f)!=(size_t)len) { + perror("read error"); + lastError=fmt::sprintf("on read: %s",strerror(errno)); + fclose(f); + delete[] file; + return 1; + } + fclose(f); + if (!e->playStream(file,(size_t)len)) { + lastError=e->getLastError(); + logE("could not open file!"); + return 1; + } + } + return 0; +} + + void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { e->saveAudio(path.c_str(),exportLoops+1,mode,exportFadeOut); displayExporting=true; @@ -3404,6 +3483,7 @@ bool FurnaceGUI::loop() { curIns=msg.data[0]; if (curIns>=(int)e->song.ins.size()) curIns=e->song.ins.size()-1; wavePreviewInit=true; + updateFMPreview=true; } break; case TA_MIDI_CONTROL: @@ -3668,18 +3748,53 @@ bool FurnaceGUI::loop() { } if (numZSMCompat > 0) { if (ImGui::BeginMenu("export ZSM...")) { - ImGui::Text("Commander X16 Zsound Music File"); - if (ImGui::InputInt("Tick Rate (Hz)",&zsmExportTickRate,1,2)) { - if (zsmExportTickRate<1) zsmExportTickRate=1; - if (zsmExportTickRate>44100) zsmExportTickRate=44100; + ImGui::Text("Commander X16 Zsound Music File"); + if (ImGui::InputInt("Tick Rate (Hz)",&zsmExportTickRate,1,2)) { + if (zsmExportTickRate<1) zsmExportTickRate=1; + if (zsmExportTickRate>44100) zsmExportTickRate=44100; + } + ImGui::Checkbox("loop",&zsmExportLoop); + ImGui::SameLine(); + if (ImGui::Button("Begin Export")) { + openFileDialog(GUI_FILE_EXPORT_ZSM); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); + } + } + int numAmiga=0; + for (int i=0; isong.systemLen; i++) { + if (e->song.system[i]==DIV_SYSTEM_AMIGA) numAmiga++; + } + if (numAmiga && settings.iCannotWait) { + if (ImGui::BeginMenu("export Amiga validation data...")) { + ImGui::Text( + "this is NOT ROM export! only use for making sure the\n" + "Furnace Amiga emulator is working properly by\n" + "comparing it with real Amiga output." + ); + ImGui::Text("Directory"); + ImGui::SameLine(); + ImGui::InputText("##AVDPath",&workingDirROMExport); + if (ImGui::Button("Bake Data")) { + std::vector out=e->buildROM(DIV_ROM_AMIGA_VALIDATION); + if (workingDirROMExport.size()>0) { + if (workingDirROMExport[workingDirROMExport.size()-1]!=DIR_SEPARATOR) workingDirROMExport+=DIR_SEPARATOR_STR; } - ImGui::Checkbox("loop",&zsmExportLoop); - ImGui::SameLine(); - if (ImGui::Button("Begin Export")) { - openFileDialog(GUI_FILE_EXPORT_ZSM); - ImGui::CloseCurrentPopup(); + for (DivROMExportOutput& i: out) { + String path=workingDirROMExport+i.name; + FILE* outFile=ps_fopen(path.c_str(),"wb"); + if (outFile!=NULL) { + fwrite(i.data->getFinalBuf(),1,i.data->size(),outFile); + fclose(outFile); + } + i.data->finish(); + delete i.data; } - ImGui::EndMenu(); + showError(fmt::sprintf("Done! Baked %d files.",(int)out.size())); + ImGui::CloseCurrentPopup(); + } + ImGui::EndMenu(); } } if (ImGui::BeginMenu("export command stream...")) { @@ -3690,7 +3805,10 @@ bool FurnaceGUI::loop() { "technical/development use only!" ); - if (ImGui::Button("export")) { + if (ImGui::Button("export (binary)")) { + openFileDialog(GUI_FILE_EXPORT_CMDSTREAM_BINARY); + } + if (ImGui::Button("export (text)")) { openFileDialog(GUI_FILE_EXPORT_CMDSTREAM); } ImGui::EndMenu(); @@ -4123,6 +4241,7 @@ bool FurnaceGUI::loop() { } else { curIns=prevIns; wavePreviewInit=true; + updateFMPreview=true; } prevIns=-3; } @@ -4167,6 +4286,7 @@ bool FurnaceGUI::loop() { break; case GUI_FILE_EXPORT_ROM: case GUI_FILE_EXPORT_CMDSTREAM: + case GUI_FILE_EXPORT_CMDSTREAM_BINARY: workingDirROMExport=fileDialog->getPath()+DIR_SEPARATOR_STR; break; case GUI_FILE_LOAD_MAIN_FONT: @@ -4190,6 +4310,9 @@ bool FurnaceGUI::loop() { case GUI_FILE_MU5_ROM_OPEN: workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR; break; + case GUI_FILE_CMDSTREAM_OPEN: + workingDirROM=fileDialog->getPath()+DIR_SEPARATOR_STR; + break; case GUI_FILE_TEST_OPEN: case GUI_FILE_TEST_OPEN_MULTI: case GUI_FILE_TEST_SAVE: @@ -4254,9 +4377,10 @@ bool FurnaceGUI::loop() { checkExtension(".zsm"); } if (curFileDialog==GUI_FILE_EXPORT_CMDSTREAM) { - // we can't tell whether the user chose .txt or .bin in the system file picker - const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="text file")?".txt":".bin"; - checkExtensionDual(".txt",".bin",fallbackExt); + checkExtension(".txt"); + } + if (curFileDialog==GUI_FILE_EXPORT_CMDSTREAM_BINARY) { + checkExtension(".bin"); } if (curFileDialog==GUI_FILE_EXPORT_COLORS) { checkExtension(".cfgc"); @@ -4598,15 +4722,9 @@ bool FurnaceGUI::loop() { case GUI_FILE_EXPORT_ROM: showError("Coming soon!"); break; - case GUI_FILE_EXPORT_CMDSTREAM: { - String lowerCase=fileName; - for (char& i: lowerCase) { - if (i>='A' && i<='Z') i+='a'-'A'; - } - bool isBinary=true; - if ((lowerCase.size()<4 || lowerCase.rfind(".bin")!=lowerCase.size()-4)) { - isBinary=false; - } + case GUI_FILE_EXPORT_CMDSTREAM: + case GUI_FILE_EXPORT_CMDSTREAM_BINARY: { + bool isBinary=(curFileDialog==GUI_FILE_EXPORT_CMDSTREAM_BINARY); SafeWriter* w=e->saveCommand(isBinary); if (w!=NULL) { @@ -4660,6 +4778,11 @@ bool FurnaceGUI::loop() { case GUI_FILE_MU5_ROM_OPEN: settings.mu5Path=copyOfName; break; + case GUI_FILE_CMDSTREAM_OPEN: + if (loadStream(copyOfName)>0) { + showError(fmt::sprintf("Error while loading file! (%s)",lastError)); + } + break; case GUI_FILE_TEST_OPEN: showWarning(fmt::sprintf("You opened: %s",copyOfName),GUI_WARN_GENERIC); break; @@ -5120,6 +5243,7 @@ bool FurnaceGUI::loop() { if (i!=DIV_INS_AMIGA) e->song.ins[curIns]->amiga.useSample=true; nextWindow=GUI_WINDOW_INS_EDIT; wavePreviewInit=true; + updateFMPreview=true; } MARK_MODIFIED; } @@ -5435,6 +5559,7 @@ bool FurnaceGUI::init() { waveSigned=e->getConfBool("waveSigned",false); waveGenVisible=e->getConfBool("waveGenVisible",false); waveEditStyle=e->getConfInt("waveEditStyle",0); + extraChannelButtons=e->getConfInt("extraChannelButtons",0); lockLayout=e->getConfBool("lockLayout",false); #ifdef IS_MOBILE fullScreen=true; @@ -5849,6 +5974,7 @@ void FurnaceGUI::commitState() { e->setConf("waveSigned",waveSigned); e->setConf("waveGenVisible",waveGenVisible); e->setConf("waveEditStyle",waveEditStyle); + e->setConf("extraChannelButtons",extraChannelButtons); e->setConf("lockLayout",lockLayout); e->setConf("fullScreen",fullScreen); e->setConf("mobileUI",mobileUI); @@ -5979,6 +6105,10 @@ FurnaceGUI::FurnaceGUI(): mobileEditButtonPos(0.7f,0.7f), mobileEditButtonSize(60.0f,60.0f), curSysSection(NULL), + updateFMPreview(true), + fmPreviewOn(false), + fmPreviewPaused(false), + fmPreviewOPN(NULL), pendingRawSampleDepth(8), pendingRawSampleChannels(1), pendingRawSampleUnsigned(false), diff --git a/src/gui/gui.h b/src/gui/gui.h index 5b4cc3221..7dddb53e7 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -68,6 +68,8 @@ #define BIND_FOR(x) getKeyName(actionKeys[x],true).c_str() +#define FM_PREVIEW_SIZE 512 + // TODO: // - add colors for FM envelope and waveform // - maybe add "alternate" color for FM modulators/carriers (a bit difficult) @@ -368,6 +370,7 @@ enum FurnaceGUIFileDialogs { GUI_FILE_EXPORT_VGM, GUI_FILE_EXPORT_ZSM, GUI_FILE_EXPORT_CMDSTREAM, + GUI_FILE_EXPORT_CMDSTREAM_BINARY, GUI_FILE_EXPORT_ROM, GUI_FILE_LOAD_MAIN_FONT, GUI_FILE_LOAD_PAT_FONT, @@ -380,6 +383,7 @@ enum FurnaceGUIFileDialogs { GUI_FILE_YRW801_ROM_OPEN, GUI_FILE_TG100_ROM_OPEN, GUI_FILE_MU5_ROM_OPEN, + GUI_FILE_CMDSTREAM_OPEN, GUI_FILE_TEST_OPEN, GUI_FILE_TEST_OPEN_MULTI, @@ -1192,6 +1196,9 @@ class FurnaceGUI { ImVec2 mobileEditButtonPos, mobileEditButtonSize; const int* curSysSection; DivInstrumentFM opllPreview; + short fmPreview[FM_PREVIEW_SIZE]; + bool updateFMPreview, fmPreviewOn, fmPreviewPaused; + void* fmPreviewOPN; String pendingRawSample; int pendingRawSampleDepth, pendingRawSampleChannels; @@ -1357,6 +1364,7 @@ class FurnaceGUI { int channelFont; int channelTextCenter; int midiOutClock; + int midiOutProgramChange; int midiOutMode; int maxRecentFile; int centerPattern; @@ -1369,6 +1377,8 @@ class FurnaceGUI { int oneDigitEffects; int disableFadeIn; int alwaysPlayIntro; + int iCannotWait; + int orderButtonPos; unsigned int maxUndoSteps; String mainFontPath; String patFontPath; @@ -1497,6 +1507,7 @@ class FurnaceGUI { channelFont(1), channelTextCenter(1), midiOutClock(0), + midiOutProgramChange(0), midiOutMode(1), maxRecentFile(10), centerPattern(0), @@ -1509,6 +1520,8 @@ class FurnaceGUI { oneDigitEffects(0), disableFadeIn(0), alwaysPlayIntro(0), + iCannotWait(0), + orderButtonPos(2), maxUndoSteps(100), mainFontPath(""), patFontPath(""), @@ -1885,6 +1898,8 @@ class FurnaceGUI { void drawGBEnv(unsigned char vol, unsigned char len, unsigned char sLen, bool dir, const ImVec2& size); bool drawSysConf(int chan, DivSystem type, DivConfig& flags, bool modifyOnChange); void kvsConfig(DivInstrument* ins); + void drawFMPreview(const ImVec2& size); + void renderFMPreview(const DivInstrumentFM& params, int pos=0); // these ones offer ctrl-wheel fine value changes. bool CWSliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format=NULL, ImGuiSliderFlags flags=0); @@ -1919,6 +1934,8 @@ class FurnaceGUI { void drawMacroEdit(FurnaceGUIMacroDesc& i, int totalFit, float availableWidth, int index); void drawMacros(std::vector& macros, FurnaceGUIMacroEditState& state); + void drawOrderButtons(); + void actualWaveList(); void actualSampleList(); @@ -1966,7 +1983,7 @@ class FurnaceGUI { void drawNewSong(); void drawLog(); void drawEffectList(); - void drawSubSongs(); + void drawSubSongs(bool asChild=false); void drawFindReplace(); void drawSpoiler(); void drawClock(); @@ -2058,6 +2075,7 @@ class FurnaceGUI { void openFileDialog(FurnaceGUIFileDialogs type); int save(String path, int dmfVersion); int load(String path); + int loadStream(String path); void pushRecentFile(String path); void exportAudio(String path, DivAudioExportModes mode); diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 6237a4392..abac70c1a 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -271,9 +271,9 @@ const char* x1_010EnvBits[8]={ "enable", "oneshot", "split L/R", "HinvR", "VinvR", "HinvL", "VinvL", NULL }; -const char* n163UpdateBits[8]={ +/*const char* n163UpdateBits[8]={ "now", "every waveform changed", NULL -}; +};*/ const char* suControlBits[5]={ "ring mod", "low pass", "high pass", "band pass", NULL @@ -1259,9 +1259,10 @@ void FurnaceGUI::drawGBEnv(unsigned char vol, unsigned char len, unsigned char s #define P(x) if (x) { \ MARK_MODIFIED; \ e->notifyInsChange(curIns); \ + updateFMPreview=true; \ } -#define PARAMETER MARK_MODIFIED; e->notifyInsChange(curIns); +#define PARAMETER MARK_MODIFIED; e->notifyInsChange(curIns); updateFMPreview=true; String genericGuide(float value) { return fmt::sprintf("%d",(int)value); @@ -1279,42 +1280,67 @@ inline bool enBit30(const int val) { void FurnaceGUI::kvsConfig(DivInstrument* ins) { - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("(click to configure TL scaling)"); - } - int opCount=4; - if (ins->type==DIV_INS_OPLL) opCount=2; - if (ins->type==DIV_INS_OPL) opCount=(ins->fm.ops==4)?4:2; - if (ImGui::BeginPopupContextItem("IKVSOpt",ImGuiPopupFlags_MouseButtonLeft)) { - ImGui::Text("operator level changes with volume?"); - if (ImGui::BeginTable("KVSTable",4,ImGuiTableFlags_BordersInner)) { - ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed); - ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch); - for (int i=0; i<4; i++) { - int o=(opCount==4)?orderedOps[i]:i; - if (!(i&1)) ImGui::TableNextRow(); - const char* label="AUTO##OPKVS"; - if (ins->fm.op[o].kvs==0) { - label="NO##OPKVS"; - } else if (ins->fm.op[o].kvs==1) { - label="YES##OPKVS"; - } - ImGui::TableNextColumn(); - ImGui::Text("%d",i+1); - ImGui::TableNextColumn(); - ImGui::PushID(o); - if (ImGui::Button(label,ImVec2(ImGui::GetContentRegionAvail().x,0.0f))) { - if (++ins->fm.op[o].kvs>2) ins->fm.op[o].kvs=0; - PARAMETER; - } - ImGui::PopID(); - } - ImGui::EndTable(); + if (ins->type==DIV_INS_FM && fmPreviewOn) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("left click to restart\nmiddle click to pause\nright click to see algorithm"); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + updateFMPreview=true; + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Middle)) { + fmPreviewPaused=!fmPreviewPaused; + } + } else { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("left click to configure TL scaling\nright click to see FM preview"); } - ImGui::EndPopup(); } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && ins->type==DIV_INS_FM) { + fmPreviewOn=!fmPreviewOn; + } + if (!fmPreviewOn || ins->type!=DIV_INS_FM) { + int opCount=4; + if (ins->type==DIV_INS_OPLL) opCount=2; + if (ins->type==DIV_INS_OPL) opCount=(ins->fm.ops==4)?4:2; + if (ImGui::BeginPopupContextItem("IKVSOpt",ImGuiPopupFlags_MouseButtonLeft)) { + ImGui::Text("operator level changes with volume?"); + if (ImGui::BeginTable("KVSTable",4,ImGuiTableFlags_BordersInner)) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("c2",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c3",ImGuiTableColumnFlags_WidthStretch); + for (int i=0; i<4; i++) { + int o=(opCount==4)?orderedOps[i]:i; + if (!(i&1)) ImGui::TableNextRow(); + const char* label="AUTO##OPKVS"; + if (ins->fm.op[o].kvs==0) { + label="NO##OPKVS"; + } else if (ins->fm.op[o].kvs==1) { + label="YES##OPKVS"; + } + ImGui::TableNextColumn(); + ImGui::Text("%d",i+1); + ImGui::TableNextColumn(); + ImGui::PushID(o); + if (ImGui::Button(label,ImVec2(ImGui::GetContentRegionAvail().x,0.0f))) { + if (++ins->fm.op[o].kvs>2) ins->fm.op[o].kvs=0; + PARAMETER; + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::EndPopup(); + } + } +} + +void FurnaceGUI::drawFMPreview(const ImVec2& size) { + float asFloat[FM_PREVIEW_SIZE]; + for (int i=0; isong.ins[curIns]; + if (updateFMPreview) { + renderFMPreview(ins->fm); + updateFMPreview=false; + } if (settings.insEditColorize) { pushAccentColors(uiColors[GUI_COLOR_INSTR_STD+ins->type],uiColors[GUI_COLOR_INSTR_STD+ins->type],uiColors[GUI_COLOR_INSTR_STD+ins->type],ImVec4(0.0f,0.0f,0.0f,0.0f)); } @@ -2167,6 +2198,7 @@ void FurnaceGUI::drawInsEdit() { curIns=i; ins=e->song.ins[curIns]; wavePreviewInit=true; + updateFMPreview=true; } } ImGui::EndCombo(); @@ -2306,7 +2338,15 @@ void FurnaceGUI::drawInsEdit() { P(CWSliderScalar(FM_NAME(FM_ALG),ImGuiDataType_U8,&ins->fm.alg,&_ZERO,&_SEVEN)); rightClickable P(CWSliderScalar(FM_NAME(FM_AMS),ImGuiDataType_U8,&ins->fm.ams,&_ZERO,&_THREE)); rightClickable ImGui::TableNextColumn(); - drawAlgorithm(ins->fm.alg,FM_ALGS_4OP,ImVec2(ImGui::GetContentRegionAvail().x,48.0*dpiScale)); + if (ins->type==DIV_INS_FM && fmPreviewOn) { + drawFMPreview(ImVec2(ImGui::GetContentRegionAvail().x,48.0*dpiScale)); + if (!fmPreviewPaused) { + renderFMPreview(ins->fm,1); + WAKE_UP; + } + } else { + drawAlgorithm(ins->fm.alg,FM_ALGS_4OP,ImVec2(ImGui::GetContentRegionAvail().x,48.0*dpiScale)); + } kvsConfig(ins); break; case DIV_INS_OPZ: @@ -4262,9 +4302,18 @@ void FurnaceGUI::drawInsEdit() { } popToggleColors(); - P(ImGui::Checkbox("Volume Macro is Cutoff Macro",&ins->c64.volIsCutoff)); - P(ImGui::Checkbox("Absolute Cutoff Macro",&ins->c64.filterIsAbs)); - P(ImGui::Checkbox("Absolute Duty Macro",&ins->c64.dutyIsAbs)); + if (ImGui::Checkbox("Volume Macro is Cutoff Macro",&ins->c64.volIsCutoff)) { + ins->std.volMacro.vZoom=-1; + PARAMETER; + } + if (ImGui::Checkbox("Absolute Cutoff Macro",&ins->c64.filterIsAbs)) { + ins->std.volMacro.vZoom=-1; + PARAMETER; + } + if (ImGui::Checkbox("Absolute Duty Macro",&ins->c64.dutyIsAbs)) { + ins->std.dutyMacro.vZoom=-1; + PARAMETER; + } P(ImGui::Checkbox("Don't test/gate before new note",&ins->c64.noTest)); ImGui::EndTabItem(); } @@ -5116,7 +5165,7 @@ void FurnaceGUI::drawInsEdit() { dutyMax=ins->amiga.useSample?0:255; } if (ins->type==DIV_INS_TIA || ins->type==DIV_INS_AMIGA || ins->type==DIV_INS_SCC || - ins->type==DIV_INS_PET || ins->type==DIV_INS_VIC || ins->type==DIV_INS_SEGAPCM || + ins->type==DIV_INS_PET || ins->type==DIV_INS_SEGAPCM || ins->type==DIV_INS_FM || ins->type==DIV_INS_K007232 || ins->type==DIV_INS_GA20 || ins->type==DIV_INS_SM8521 || ins->type==DIV_INS_PV1000) { dutyMax=0; @@ -5133,6 +5182,10 @@ void FurnaceGUI::drawInsEdit() { dutyLabel="Noise"; dutyMax=1; } + if (ins->type==DIV_INS_VIC) { + dutyLabel="On/Off"; + dutyMax=1; + } if (ins->type==DIV_INS_SWAN) { dutyLabel="Noise"; dutyMax=ins->amiga.useSample?0:8; @@ -5149,10 +5202,10 @@ void FurnaceGUI::drawInsEdit() { dutyLabel="Duty"; dutyMax=63; } - if (ins->type==DIV_INS_N163) { + /*if (ins->type==DIV_INS_N163) { dutyLabel="Waveform pos."; dutyMax=255; - } + }*/ if (ins->type==DIV_INS_VRC6) { dutyLabel="Duty"; dutyMax=ins->amiga.useSample?0:7; @@ -5432,8 +5485,8 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("Envelope",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,saaEnvBits)); } else if (ins->type==DIV_INS_X1_010 && !ins->amiga.useSample) { macroList.push_back(FurnaceGUIMacroDesc("Envelope Mode",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,x1_010EnvBits)); - } else if (ins->type==DIV_INS_N163) { - macroList.push_back(FurnaceGUIMacroDesc("Wave Length",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); + /*} else if (ins->type==DIV_INS_N163) { + macroList.push_back(FurnaceGUIMacroDesc("Wave Length",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER]));*/ } else if (ins->type==DIV_INS_FDS) { macroList.push_back(FurnaceGUIMacroDesc("Mod Depth",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } else if (ins->type==DIV_INS_SU) { @@ -5455,8 +5508,8 @@ void FurnaceGUI::drawInsEdit() { if (ex2Max>0) { if (ins->type==DIV_INS_C64) { macroList.push_back(FurnaceGUIMacroDesc("Resonance",&ins->std.ex2Macro,0,ex2Max,64,uiColors[GUI_COLOR_MACRO_OTHER])); - } else if (ins->type==DIV_INS_N163) { - macroList.push_back(FurnaceGUIMacroDesc("Wave Update",&ins->std.ex2Macro,0,ex2Max,64,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,n163UpdateBits)); + /*} else if (ins->type==DIV_INS_N163) { + macroList.push_back(FurnaceGUIMacroDesc("Wave Update",&ins->std.ex2Macro,0,ex2Max,64,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,n163UpdateBits));*/ } else if (ins->type==DIV_INS_FDS) { macroList.push_back(FurnaceGUIMacroDesc("Mod Speed",&ins->std.ex2Macro,0,ex2Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } else if (ins->type==DIV_INS_SU) { diff --git a/src/gui/orders.cpp b/src/gui/orders.cpp index 16a9c1bbb..dad280d46 100644 --- a/src/gui/orders.cpp +++ b/src/gui/orders.cpp @@ -91,6 +91,122 @@ void FurnaceGUI::drawMobileOrderSel() { ImGui::End(); } +#define NEXT_BUTTON \ + if (++buttonColumn>=buttonColumns) { \ + buttonColumn=0; \ + } else { \ + ImGui::SameLine(); \ + } + +void FurnaceGUI::drawOrderButtons() { + int buttonColumns=(settings.orderButtonPos==0)?8:1; + int buttonColumn=0; + + while (buttonColumns<8 && ((8/buttonColumns)*ImGui::GetFrameHeightWithSpacing())>ImGui::GetContentRegionAvail().y) { + buttonColumns++; + } + + if (ImGui::Button(ICON_FA_PLUS)) { handleUnimportant + // add order row (new) + doAction(GUI_ACTION_ORDERS_ADD); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Add new order"); + } + NEXT_BUTTON; + + if (ImGui::Button(ICON_FA_MINUS)) { handleUnimportant + // remove this order row + doAction(GUI_ACTION_ORDERS_REMOVE); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Remove order"); + } + NEXT_BUTTON; + + if (ImGui::Button(ICON_FA_FILES_O)) { handleUnimportant + // duplicate order row + doAction(GUI_ACTION_ORDERS_DUPLICATE); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + doAction(GUI_ACTION_ORDERS_DEEP_CLONE); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Duplicate order (right-click to deep clone)"); + } + NEXT_BUTTON; + + if (ImGui::Button(ICON_FA_ANGLE_UP)) { handleUnimportant + // move order row up + doAction(GUI_ACTION_ORDERS_MOVE_UP); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Move order up"); + } + NEXT_BUTTON; + + if (ImGui::Button(ICON_FA_ANGLE_DOWN)) { handleUnimportant + // move order row down + doAction(GUI_ACTION_ORDERS_MOVE_DOWN); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Move order down"); + } + NEXT_BUTTON; + + if (ImGui::Button(ICON_FA_ANGLE_DOUBLE_DOWN)) { handleUnimportant + // duplicate order row at end + doAction(GUI_ACTION_ORDERS_DUPLICATE_END); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + doAction(GUI_ACTION_ORDERS_DEEP_CLONE_END); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Duplicate order at end of song (right-click to deep clone)"); + } + NEXT_BUTTON; + + if (ImGui::Button(changeAllOrders?ICON_FA_LINK"##ChangeAll":ICON_FA_CHAIN_BROKEN"##ChangeAll")) { handleUnimportant + // whether to change one or all orders in a row + changeAllOrders=!changeAllOrders; + } + if (ImGui::IsItemHovered()) { + if (changeAllOrders) { + ImGui::SetTooltip("Order change mode: entire row"); + } else { + ImGui::SetTooltip("Order change mode: one"); + } + } + NEXT_BUTTON; + + const char* orderEditModeLabel="?##OrderEditMode"; + if (orderEditMode==3) { + orderEditModeLabel=ICON_FA_ARROWS_V "##OrderEditMode"; + } else if (orderEditMode==2) { + orderEditModeLabel=ICON_FA_ARROWS_H "##OrderEditMode"; + } else if (orderEditMode==1) { + orderEditModeLabel=ICON_FA_I_CURSOR "##OrderEditMode"; + } else { + orderEditModeLabel=ICON_FA_MOUSE_POINTER "##OrderEditMode"; + } + if (ImGui::Button(orderEditModeLabel)) { handleUnimportant + orderEditMode++; + if (orderEditMode>3) orderEditMode=0; + curNibble=false; + } + if (ImGui::IsItemHovered()) { + if (orderEditMode==3) { + ImGui::SetTooltip("Order edit mode: Select and type (scroll vertically)"); + } else if (orderEditMode==2) { + ImGui::SetTooltip("Order edit mode: Select and type (scroll horizontally)"); + } else if (orderEditMode==1) { + ImGui::SetTooltip("Order edit mode: Select and type (don't scroll)"); + } else { + ImGui::SetTooltip("Order edit mode: Click to change"); + } + } +} + void FurnaceGUI::drawOrders() { static char selID[4096]; if (nextWindow==GUI_WINDOW_ORDERS) { @@ -107,242 +223,189 @@ void FurnaceGUI::drawOrders() { } else { //ImGui::SetNextWindowSizeConstraints(ImVec2(440.0f*dpiScale,400.0f*dpiScale),ImVec2(canvasW,canvasH)); } - if (ImGui::Begin("Orders",&ordersOpen,globalWinFlags)) { - float regionX=ImGui::GetContentRegionAvail().x; - ImVec2 prevSpacing=ImGui::GetStyle().ItemSpacing; - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(1.0f*dpiScale,1.0f*dpiScale)); - ImGui::Columns(2,NULL,false); - ImGui::SetColumnWidth(-1,regionX-24.0f*dpiScale); - int displayChans=0; - for (int i=0; igetTotalChannelCount(); i++) { - if (e->curSubSong->chanShow[i]) displayChans++; - } - ImGui::PushFont(patFont); - bool tooSmall=((displayChans+1)>((ImGui::GetContentRegionAvail().x)/(ImGui::CalcTextSize("AA").x+2.0*ImGui::GetStyle().ItemInnerSpacing.x))); - ImGui::PopFont(); - if (ImGui::BeginTable("OrdersTable",1+displayChans,(tooSmall?ImGuiTableFlags_SizingFixedFit:ImGuiTableFlags_SizingStretchSame)|ImGuiTableFlags_ScrollX|ImGuiTableFlags_ScrollY)) { - ImGui::PushFont(patFont); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,prevSpacing); - ImGui::TableSetupScrollFreeze(1,1); - float lineHeight=(ImGui::GetTextLineHeight()+4*dpiScale); - if (e->isPlaying()) { - if (followOrders) { - ImGui::SetScrollY((e->getOrder()+1)*lineHeight-(ImGui::GetContentRegionAvail().y/2)); - } + if (ImGui::Begin("Orders",&ordersOpen,globalWinFlags|ImGuiWindowFlags_NoScrollbar|ImGuiWindowFlags_NoScrollWithMouse)) { + if (ImGui::BeginTable("OrdColumn",(settings.orderButtonPos==0)?1:2,ImGuiTableFlags_BordersInnerV)) { + if (settings.orderButtonPos==2) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthFixed); + } else if (settings.orderButtonPos==1) { + ImGui::TableSetupColumn("c0",ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("c1",ImGuiTableColumnFlags_WidthStretch); } - ImGui::TableNextRow(0,lineHeight); - ImVec2 ra=ImGui::GetContentRegionAvail(); - ImGui::TableNextColumn(); - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]); - for (int i=0; igetTotalChannelCount(); i++) { - if (!e->curSubSong->chanShow[i]) continue; - ImGui::TableNextColumn(); - ImGui::Text("%s",e->getChannelShortName(i)); - } - ImGui::PopStyleColor(); - for (int i=0; icurSubSong->ordersLen; i++) { - ImGui::TableNextRow(0,lineHeight); - if (oldOrder1==i) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,ImGui::GetColorU32(uiColors[GUI_COLOR_ORDER_ACTIVE])); - ImGui::TableNextColumn(); - if ((!followPattern && curOrder==i) || (followPattern && oldOrder1==i)) { - // draw a border - ImDrawList* dl=ImGui::GetWindowDrawList(); - ImVec2 rBegin=ImGui::GetCursorScreenPos(); - rBegin.y-=ImGui::GetStyle().CellPadding.y; - ImVec2 rEnd=ImVec2(rBegin.x+ra.x,rBegin.y+lineHeight); - dl->AddRect(rBegin,rEnd,ImGui::GetColorU32(uiColors[GUI_COLOR_ORDER_SELECTED]),2.0f*dpiScale); - } - ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]); - bool highlightLoop=(i>=loopOrder && i<=loopEnd); - if (highlightLoop) ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,ImGui::GetColorU32(uiColors[GUI_COLOR_SONG_LOOP])); - if (settings.orderRowsBase==1) { - snprintf(selID,4096,"%.2X##O_S%.2x",i,i); - } else { - snprintf(selID,4096,"%d##O_S%.2x",i,i); - } - if (ImGui::Selectable(selID)) { - setOrder(i); - curNibble=false; - orderCursor=-1; - if (orderEditMode==0) { - handleUnimportant; + ImVec2 prevSpacing=ImGui::GetStyle().ItemSpacing; + if (settings.orderButtonPos!=0) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,ImVec2(1.0f*dpiScale,1.0f*dpiScale)); + } + + ImGui::TableNextRow(); + + if (settings.orderButtonPos<2) { + ImGui::TableNextColumn(); + drawOrderButtons(); + } + + if (settings.orderButtonPos==0) { + ImGui::TableNextRow(); + } + + ImGui::TableNextColumn(); + + int displayChans=0; + for (int i=0; igetTotalChannelCount(); i++) { + if (e->curSubSong->chanShow[i]) displayChans++; + } + ImGui::PushFont(patFont); + bool tooSmall=((displayChans+1)>((ImGui::GetContentRegionAvail().x)/(ImGui::CalcTextSize("AA").x+2.0*ImGui::GetStyle().ItemInnerSpacing.x))); + ImGui::PopFont(); + if (ImGui::BeginTable("OrdersTable",1+displayChans,(tooSmall?ImGuiTableFlags_SizingFixedFit:ImGuiTableFlags_SizingStretchSame)|ImGuiTableFlags_ScrollX|ImGuiTableFlags_ScrollY)) { + ImGui::PushFont(patFont); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,prevSpacing); + ImGui::TableSetupScrollFreeze(1,1); + float lineHeight=(ImGui::GetTextLineHeight()+4*dpiScale); + if (e->isPlaying()) { + if (followOrders) { + ImGui::SetScrollY((e->getOrder()+1)*lineHeight-(ImGui::GetContentRegionAvail().y/2)); } } - ImGui::PopStyleColor(); - for (int j=0; jgetTotalChannelCount(); j++) { - if (!e->curSubSong->chanShow[j]) continue; + ImGui::TableNextRow(0,lineHeight); + ImVec2 ra=ImGui::GetContentRegionAvail(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]); + for (int i=0; igetTotalChannelCount(); i++) { + if (!e->curSubSong->chanShow[i]) continue; ImGui::TableNextColumn(); - DivPattern* pat=e->curPat[j].getPattern(e->curOrders->ord[j][i],false); - /*if (!pat->name.empty()) { - snprintf(selID,4096,"%s##O_%.2x_%.2x",pat->name.c_str(),j,i); - } else {*/ - snprintf(selID,4096,"%.2X##O_%.2x_%.2x",e->curOrders->ord[j][i],j,i); - //} - - ImGui::PushStyleColor(ImGuiCol_Text,(curOrder==i || e->curOrders->ord[j][i]==e->curOrders->ord[j][curOrder])?uiColors[GUI_COLOR_ORDER_SIMILAR]:uiColors[GUI_COLOR_ORDER_INACTIVE]); - if (ImGui::Selectable(selID,settings.ordersCursor?(cursor.xCoarse==j && oldOrder1!=i):false)) { - if (curOrder==i) { - if (orderEditMode==0) { - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->lockSave([this,i,j]() { - if (changeAllOrders) { - for (int k=0; kgetTotalChannelCount(); k++) { - if (e->curOrders->ord[k][i]<(unsigned char)(DIV_MAX_PATTERNS-1)) e->curOrders->ord[k][i]++; - } - } else { - if (e->curOrders->ord[j][i]<(unsigned char)(DIV_MAX_PATTERNS-1)) e->curOrders->ord[j][i]++; - } - }); - e->walkSong(loopOrder,loopRow,loopEnd); - makeUndo(GUI_UNDO_CHANGE_ORDER); - } else { - orderCursor=j; - curNibble=false; - } - } else { - setOrder(i); - e->walkSong(loopOrder,loopRow,loopEnd); - if (orderEditMode!=0) { - orderCursor=j; - curNibble=false; - } - } + ImGui::Text("%s",e->getChannelShortName(i)); + } + ImGui::PopStyleColor(); + for (int i=0; icurSubSong->ordersLen; i++) { + ImGui::TableNextRow(0,lineHeight); + if (oldOrder1==i) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,ImGui::GetColorU32(uiColors[GUI_COLOR_ORDER_ACTIVE])); + ImGui::TableNextColumn(); + if ((!followPattern && curOrder==i) || (followPattern && oldOrder1==i)) { + // draw a border + ImDrawList* dl=ImGui::GetWindowDrawList(); + ImVec2 rBegin=ImGui::GetCursorScreenPos(); + rBegin.y-=ImGui::GetStyle().CellPadding.y; + ImVec2 rEnd=ImVec2(rBegin.x+ra.x,rBegin.y+lineHeight); + dl->AddRect(rBegin,rEnd,ImGui::GetColorU32(uiColors[GUI_COLOR_ORDER_SELECTED]),2.0f*dpiScale); + } + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_ORDER_ROW_INDEX]); + bool highlightLoop=(i>=loopOrder && i<=loopEnd); + if (highlightLoop) ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,ImGui::GetColorU32(uiColors[GUI_COLOR_SONG_LOOP])); + if (settings.orderRowsBase==1) { + snprintf(selID,4096,"%.2X##O_S%.2x",i,i); + } else { + snprintf(selID,4096,"%d##O_S%.2x",i,i); + } + if (ImGui::Selectable(selID)) { + setOrder(i); + curNibble=false; + orderCursor=-1; if (orderEditMode==0) { handleUnimportant; } } ImGui::PopStyleColor(); - if (orderEditMode!=0 && curOrder==i && orderCursor==j) { - // draw a border - ImDrawList* dl=ImGui::GetWindowDrawList(); - dl->AddRect(ImGui::GetItemRectMin(),ImGui::GetItemRectMax(),ImGui::GetColorU32(uiColors[GUI_COLOR_TEXT]),2.0f*dpiScale); - } - if (!pat->name.empty() && ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s",pat->name.c_str()); - } - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - if (curOrder==i) { - if (orderEditMode==0) { - prepareUndo(GUI_UNDO_CHANGE_ORDER); - e->lockSave([this,i,j]() { - if (changeAllOrders) { - for (int k=0; kgetTotalChannelCount(); k++) { - if (e->curOrders->ord[k][i]>0) e->curOrders->ord[k][i]--; + for (int j=0; jgetTotalChannelCount(); j++) { + if (!e->curSubSong->chanShow[j]) continue; + ImGui::TableNextColumn(); + DivPattern* pat=e->curPat[j].getPattern(e->curOrders->ord[j][i],false); + /*if (!pat->name.empty()) { + snprintf(selID,4096,"%s##O_%.2x_%.2x",pat->name.c_str(),j,i); + } else {*/ + snprintf(selID,4096,"%.2X##O_%.2x_%.2x",e->curOrders->ord[j][i],j,i); + //} + + ImGui::PushStyleColor(ImGuiCol_Text,(curOrder==i || e->curOrders->ord[j][i]==e->curOrders->ord[j][curOrder])?uiColors[GUI_COLOR_ORDER_SIMILAR]:uiColors[GUI_COLOR_ORDER_INACTIVE]); + if (ImGui::Selectable(selID,settings.ordersCursor?(cursor.xCoarse==j && oldOrder1!=i):false)) { + if (curOrder==i) { + if (orderEditMode==0) { + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->lockSave([this,i,j]() { + if (changeAllOrders) { + for (int k=0; kgetTotalChannelCount(); k++) { + if (e->curOrders->ord[k][i]<(unsigned char)(DIV_MAX_PATTERNS-1)) e->curOrders->ord[k][i]++; + } + } else { + if (e->curOrders->ord[j][i]<(unsigned char)(DIV_MAX_PATTERNS-1)) e->curOrders->ord[j][i]++; } - } else { - if (e->curOrders->ord[j][i]>0) e->curOrders->ord[j][i]--; - } - }); - e->walkSong(loopOrder,loopRow,loopEnd); - makeUndo(GUI_UNDO_CHANGE_ORDER); + }); + e->walkSong(loopOrder,loopRow,loopEnd); + makeUndo(GUI_UNDO_CHANGE_ORDER); + } else { + orderCursor=j; + curNibble=false; + } } else { - orderCursor=j; - curNibble=false; + setOrder(i); + e->walkSong(loopOrder,loopRow,loopEnd); + if (orderEditMode!=0) { + orderCursor=j; + curNibble=false; + } } - } else { - setOrder(i); - e->walkSong(loopOrder,loopRow,loopEnd); - if (orderEditMode!=0) { - orderCursor=j; - curNibble=false; + + if (orderEditMode==0) { + handleUnimportant; + } + } + ImGui::PopStyleColor(); + if (orderEditMode!=0 && curOrder==i && orderCursor==j) { + // draw a border + ImDrawList* dl=ImGui::GetWindowDrawList(); + dl->AddRect(ImGui::GetItemRectMin(),ImGui::GetItemRectMax(),ImGui::GetColorU32(uiColors[GUI_COLOR_TEXT]),2.0f*dpiScale); + } + if (!pat->name.empty() && ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s",pat->name.c_str()); + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + if (curOrder==i) { + if (orderEditMode==0) { + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->lockSave([this,i,j]() { + if (changeAllOrders) { + for (int k=0; kgetTotalChannelCount(); k++) { + if (e->curOrders->ord[k][i]>0) e->curOrders->ord[k][i]--; + } + } else { + if (e->curOrders->ord[j][i]>0) e->curOrders->ord[j][i]--; + } + }); + e->walkSong(loopOrder,loopRow,loopEnd); + makeUndo(GUI_UNDO_CHANGE_ORDER); + } else { + orderCursor=j; + curNibble=false; + } + } else { + setOrder(i); + e->walkSong(loopOrder,loopRow,loopEnd); + if (orderEditMode!=0) { + orderCursor=j; + curNibble=false; + } } } } } + ImGui::PopStyleVar(); + ImGui::PopFont(); + ImGui::EndTable(); } - ImGui::PopStyleVar(); - ImGui::PopFont(); + + if (settings.orderButtonPos==2) { + ImGui::TableNextColumn(); + drawOrderButtons(); + } + + if (settings.orderButtonPos!=0) { + ImGui::PopStyleVar(); + } + ImGui::EndTable(); } - ImGui::NextColumn(); - if (ImGui::Button(ICON_FA_PLUS)) { handleUnimportant - // add order row (new) - doAction(GUI_ACTION_ORDERS_ADD); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Add new order"); - } - if (ImGui::Button(ICON_FA_MINUS)) { handleUnimportant - // remove this order row - doAction(GUI_ACTION_ORDERS_REMOVE); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Remove order"); - } - if (ImGui::Button(ICON_FA_FILES_O)) { handleUnimportant - // duplicate order row - doAction(GUI_ACTION_ORDERS_DUPLICATE); - } - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - doAction(GUI_ACTION_ORDERS_DEEP_CLONE); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Duplicate order (right-click to deep clone)"); - } - if (ImGui::Button(ICON_FA_ANGLE_UP)) { handleUnimportant - // move order row up - doAction(GUI_ACTION_ORDERS_MOVE_UP); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Move order up"); - } - if (ImGui::Button(ICON_FA_ANGLE_DOWN)) { handleUnimportant - // move order row down - doAction(GUI_ACTION_ORDERS_MOVE_DOWN); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Move order down"); - } - if (ImGui::Button(ICON_FA_ANGLE_DOUBLE_DOWN)) { handleUnimportant - // duplicate order row at end - doAction(GUI_ACTION_ORDERS_DUPLICATE_END); - } - if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { - doAction(GUI_ACTION_ORDERS_DEEP_CLONE_END); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Duplicate order at end of song (right-click to deep clone)"); - } - if (ImGui::Button(changeAllOrders?ICON_FA_LINK"##ChangeAll":ICON_FA_CHAIN_BROKEN"##ChangeAll")) { handleUnimportant - // whether to change one or all orders in a row - changeAllOrders=!changeAllOrders; - } - if (ImGui::IsItemHovered()) { - if (changeAllOrders) { - ImGui::SetTooltip("Order change mode: entire row"); - } else { - ImGui::SetTooltip("Order change mode: one"); - } - } - const char* orderEditModeLabel="?##OrderEditMode"; - if (orderEditMode==3) { - orderEditModeLabel=ICON_FA_ARROWS_V "##OrderEditMode"; - } else if (orderEditMode==2) { - orderEditModeLabel=ICON_FA_ARROWS_H "##OrderEditMode"; - } else if (orderEditMode==1) { - orderEditModeLabel=ICON_FA_I_CURSOR "##OrderEditMode"; - } else { - orderEditModeLabel=ICON_FA_MOUSE_POINTER "##OrderEditMode"; - } - if (ImGui::Button(orderEditModeLabel)) { handleUnimportant - orderEditMode++; - if (orderEditMode>3) orderEditMode=0; - curNibble=false; - } - if (ImGui::IsItemHovered()) { - if (orderEditMode==3) { - ImGui::SetTooltip("Order edit mode: Select and type (scroll vertically)"); - } else if (orderEditMode==2) { - ImGui::SetTooltip("Order edit mode: Select and type (scroll horizontally)"); - } else if (orderEditMode==1) { - ImGui::SetTooltip("Order edit mode: Select and type (don't scroll)"); - } else { - ImGui::SetTooltip("Order edit mode: Click to change"); - } - } - ImGui::PopStyleVar(); } if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_ORDERS; oldOrder1=e->getOrder(); diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 115223cc8..3a8fae4b1 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -1367,6 +1367,7 @@ void FurnaceGUI::drawSampleEdit() { } } + dl->PushClipRect(rectMin,rectMax); if (e->isPreviewingSample()) { if (!statusBar2.empty()) { statusBar2+=" | "; @@ -1380,7 +1381,6 @@ void FurnaceGUI::drawSampleEdit() { end^=start; start^=end; } - ImDrawList* dl=ImGui::GetWindowDrawList(); ImVec2 p1=rectMin; p1.x+=(e->getSamplePreviewPos()-samplePos)/sampleZoom; ImVec4 posColor=uiColors[GUI_COLOR_SAMPLE_NEEDLE]; @@ -1390,8 +1390,8 @@ void FurnaceGUI::drawSampleEdit() { posTrail2.w=0.0f; float trailDistance=(e->getSamplePreviewRate()/100.0f)/sampleZoom; - if (p1.xrectMax.x) p1.x=rectMax.x; + //if (p1.xrectMax.x) p1.x=rectMax.x; ImVec2 p2=p1; p2.y=rectMax.y; @@ -1407,6 +1407,46 @@ void FurnaceGUI::drawSampleEdit() { dl->AddLine(p1,p2,ImGui::GetColorU32(posColor)); } + if (e->isRunning()) { + for (int i=0; igetTotalChannelCount(); i++) { + DivSamplePos chanPos=e->getSamplePos(i); + if (chanPos.sample!=curSample) continue; + + int start=sampleSelStart; + int end=sampleSelEnd; + if (start>end) { + start^=end; + end^=start; + start^=end; + } + ImVec2 p1=rectMin; + p1.x+=(chanPos.pos-samplePos)/sampleZoom; + ImVec4 posColor=uiColors[GUI_COLOR_SAMPLE_NEEDLE_PLAYING]; + ImVec4 posTrail1=posColor; + ImVec4 posTrail2=posColor; + posTrail1.w*=0.5f; + posTrail2.w=0.0f; + float trailDistance=((float)chanPos.freq/100.0f)/sampleZoom; + + //if (p1.xrectMax.x) p1.x=rectMax.x; + + ImVec2 p2=p1; + p2.y=rectMax.y; + + dl->AddRectFilledMultiColor( + ImVec2(p1.x-trailDistance,p1.y), + p2, + ImGui::GetColorU32(posTrail2), + ImGui::GetColorU32(posTrail1), + ImGui::GetColorU32(posTrail1), + ImGui::GetColorU32(posTrail2) + ); + dl->AddLine(p1,p2,ImGui::GetColorU32(posColor)); + } + } + dl->PopClipRect(); + if (drawSelection) { int start=sampleSelStart; int end=sampleSelEnd; @@ -1415,7 +1455,6 @@ void FurnaceGUI::drawSampleEdit() { end^=start; start^=end; } - ImDrawList* dl=ImGui::GetWindowDrawList(); ImVec2 p1=rectMin; p1.x+=(start-samplePos)/sampleZoom; diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index e457c99c3..207c8838b 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -1149,6 +1149,11 @@ void FurnaceGUI::drawSettings() { settings.midiOutClock=midiOutClockB; } + bool midiOutProgramChangeB=settings.midiOutProgramChange; + if (ImGui::Checkbox("Send Program Change",&midiOutProgramChangeB)) { + settings.midiOutProgramChange=midiOutProgramChangeB; + } + ImGui::TreePop(); } } @@ -1447,6 +1452,17 @@ void FurnaceGUI::drawSettings() { settings.controlLayout=3; } + ImGui::Text("Position of buttons in Orders:"); + if (ImGui::RadioButton("Top##obp0",settings.orderButtonPos==0)) { + settings.orderButtonPos=0; + } + if (ImGui::RadioButton("Left##obp1",settings.orderButtonPos==1)) { + settings.orderButtonPos=1; + } + if (ImGui::RadioButton("Right##obp2",settings.orderButtonPos==2)) { + settings.orderButtonPos=2; + } + ImGui::Text("FM parameter editor layout:"); if (ImGui::RadioButton("Modern##fml0",settings.fmLayout==0)) { settings.fmLayout=0; @@ -2416,6 +2432,7 @@ void FurnaceGUI::drawSettings() { // "Debug" - toggles mobile UI // "Nice Amiga cover of the song!" - enables hidden systems (YMU759/SoundUnit/Dummy) // "42 63" - enables all instrument types + // "????" - enables stuff if (ImGui::BeginTabItem("Cheat Codes")) { ImVec2 settingsViewSize=ImGui::GetContentRegionAvail(); settingsViewSize.y-=ImGui::GetFrameHeight()+ImGui::GetStyle().WindowPadding.y; @@ -2607,6 +2624,7 @@ void FurnaceGUI::syncSettings() { settings.channelTextCenter=e->getConfInt("channelTextCenter",1); settings.maxRecentFile=e->getConfInt("maxRecentFile",10); settings.midiOutClock=e->getConfInt("midiOutClock",0); + settings.midiOutProgramChange=e->getConfInt("midiOutProgramChange",0); settings.midiOutMode=e->getConfInt("midiOutMode",1); settings.centerPattern=e->getConfInt("centerPattern",0); settings.ordersCursor=e->getConfInt("ordersCursor",1); @@ -2619,6 +2637,8 @@ void FurnaceGUI::syncSettings() { settings.disableFadeIn=e->getConfInt("disableFadeIn",0); settings.alwaysPlayIntro=e->getConfInt("alwaysPlayIntro",0); settings.cursorFollowsOrder=e->getConfInt("cursorFollowsOrder",1); + settings.iCannotWait=e->getConfInt("iCannotWait",0); + settings.orderButtonPos=e->getConfInt("orderButtonPos",2); clampSetting(settings.mainFontSize,2,96); clampSetting(settings.patFontSize,2,96); @@ -2724,6 +2744,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.channelTextCenter,0,1); clampSetting(settings.maxRecentFile,0,30); clampSetting(settings.midiOutClock,0,1); + clampSetting(settings.midiOutProgramChange,0,1); clampSetting(settings.midiOutMode,0,2); clampSetting(settings.centerPattern,0,1); clampSetting(settings.ordersCursor,0,1); @@ -2734,6 +2755,8 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.disableFadeIn,0,1); clampSetting(settings.alwaysPlayIntro,0,3); clampSetting(settings.cursorFollowsOrder,0,1); + clampSetting(settings.iCannotWait,0,1); + clampSetting(settings.orderButtonPos,0,2); if (settings.exportLoops<0.0) settings.exportLoops=0.0; if (settings.exportFadeOut<0.0) settings.exportFadeOut=0.0; @@ -2932,6 +2955,7 @@ void FurnaceGUI::commitSettings() { e->setConf("channelTextCenter",settings.channelTextCenter); e->setConf("maxRecentFile",settings.maxRecentFile); e->setConf("midiOutClock",settings.midiOutClock); + e->setConf("midiOutProgramChange",settings.midiOutProgramChange); e->setConf("midiOutMode",settings.midiOutMode); e->setConf("centerPattern",settings.centerPattern); e->setConf("ordersCursor",settings.ordersCursor); @@ -2943,7 +2967,9 @@ void FurnaceGUI::commitSettings() { e->setConf("oneDigitEffects",settings.oneDigitEffects); e->setConf("disableFadeIn",settings.disableFadeIn); e->setConf("alwaysPlayIntro",settings.alwaysPlayIntro); - e->setConf("cursorFollowsOrder", settings.cursorFollowsOrder); + e->setConf("cursorFollowsOrder",settings.cursorFollowsOrder); + e->setConf("iCannotWait",settings.iCannotWait); + e->setConf("orderButtonPos",settings.orderButtonPos); // colors for (int i=0; icurSubSong->name.empty()) { @@ -107,12 +110,16 @@ void FurnaceGUI::drawSubSongs() { MARK_MODIFIED; } - if (ImGui::GetContentRegionAvail().y>(10.0f*dpiScale)) { + if (!asChild && ImGui::GetContentRegionAvail().y>(10.0f*dpiScale)) { if (ImGui::InputTextMultiline("##SubSongNotes",&e->curSubSong->notes,ImGui::GetContentRegionAvail(),ImGuiInputTextFlags_UndoRedo)) { MARK_MODIFIED; } } } - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SUBSONGS; - ImGui::End(); + if (!asChild && ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_SUBSONGS; + if (asChild) { + ImGui::EndChild(); + } else { + ImGui::End(); + } } diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index 6084ab1f3..d5c104088 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -621,6 +621,9 @@ bool FurnaceGUI::drawSysConf(int chan, DivSystem type, DivConfig& flags, bool mo chipType=3; altered=true; } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("note: AY-3-8914 is not supported by the VGM format!"); + } } ImGui::BeginDisabled(type==DIV_SYSTEM_AY8910 && chipType==2); if (ImGui::Checkbox("Stereo##_AY_STEREO",&stereo)) { @@ -1738,6 +1741,27 @@ bool FurnaceGUI::drawSysConf(int chan, DivSystem type, DivConfig& flags, bool mo } break; } + case DIV_SYSTEM_NAMCO: + case DIV_SYSTEM_NAMCO_15XX: { + bool romMode=flags.getBool("romMode",false); + + ImGui::Text("Waveform storage mode:"); + if (ImGui::RadioButton("RAM",!romMode)) { + romMode=false; + altered=true; + } + if (ImGui::RadioButton("ROM (up to 8 waves)",romMode)) { + romMode=true; + altered=true; + } + + if (altered) { + e->lockSave([&]() { + flags.set("romMode",romMode); + }); + } + break; + } case DIV_SYSTEM_NAMCO_CUS30: { bool newNoise=flags.getBool("newNoise",true); @@ -1772,11 +1796,8 @@ bool FurnaceGUI::drawSysConf(int chan, DivSystem type, DivConfig& flags, bool mo case DIV_SYSTEM_VBOY: case DIV_SYSTEM_GA20: case DIV_SYSTEM_PV1000: - case DIV_SYSTEM_NAMCO: - case DIV_SYSTEM_NAMCO_15XX: - ImGui::Text("nothing to configure"); - break; case DIV_SYSTEM_VERA: + break; case DIV_SYSTEM_YMU759: supportsCustomRate=false; ImGui::Text("nothing to configure");