From 3fb5c2764564a71af31a040c3076f79b2a0fa3db Mon Sep 17 00:00:00 2001 From: Eknous-P Date: Thu, 31 Aug 2023 16:00:12 +0400 Subject: [PATCH 01/58] mappy demo --- demos/arcade/last_day_of_summer_mappy.fur | Bin 0 -> 2885 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 demos/arcade/last_day_of_summer_mappy.fur diff --git a/demos/arcade/last_day_of_summer_mappy.fur b/demos/arcade/last_day_of_summer_mappy.fur new file mode 100644 index 0000000000000000000000000000000000000000..bf794a44b0d32fb7016253b5532b6a5a56e78090 GIT binary patch literal 2885 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 jxdGJ Date: Fri, 1 Sep 2023 16:43:08 -0500 Subject: [PATCH 02/58] update settings.md --- doc/2-interface/settings.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/doc/2-interface/settings.md b/doc/2-interface/settings.md index 5c984d39c..34b5c8fab 100644 --- a/doc/2-interface/settings.md +++ b/doc/2-interface/settings.md @@ -66,17 +66,31 @@ settings are saved when clicking the **OK** or **Apply** buttons at the bottom o ### Output -- **Backend**: selects SDL or JACK for audio output. - - only appears on Linux, or MacOS compiled with JACK support -- **Driver**: select a different SDL audio driver if you're having problems with the default one. +- **Backend**: selects a different backend for audio output. + - SDL: the default one. + - JACK: the JACK Audio Connection Kit (low-latency audio server). only appears on Linux, or MacOS compiled with JACK support. + - PortAudio: this may or may not perform better than the SDL backend. +- **Driver**: select a different audio driver if you're having problems with the default one. + - only appears when Backend is SDL. - **Device**: audio device for playback. -- **Sample rate** + - if using PortAudio backend, devices will be prefixed with the audio API that PortAudio is going to use: + - Windows WASAPI: a modern audio API available on Windows Vista and later, featuring an (optional) Exclusive Mode. be noted that your buffer size setting may be ignored. + - Windows WDM-KS: low-latency, direct to hardware output mechanism. may not work all the time and prevents your audio device from being used for anything else! + - Windows DirectSound: this is the worst choice. best to move on. + - MME: an old audio API. doesn't have Exclusive Mode. + - Core Audio: the only choice in macOS. + - ALSA: low-level audio output on Linux. may prevent other applications from using your audio device. +- **Sample rate**: audio output rate. + - a lower rate decreases quality and isn't really beneficial. + - if using PortAudio backend, be careful about this value. - **Outputs**: number of audio outputs created, up to 16. - only appears when Backend is JACK. -- **Channels**: number of output channels to use. +- **Channels**: mono, stereo or something. - **Buffer size**: size of buffer in both samples and milliseconds. - setting this to a low value may cause stuttering/glitches in playback (known as "underruns" or "xruns"). - setting this to a high value increases latency. +- **Exclusive mode**: enables Exclusive Mode, which may offer latency improvements. + - only available on WASAPI devices in the PortAudio backend! - **Low-latency mode (experimental!)**: reduces latency by running the engine faster than the tick rate. useful for live playback/jam mode. - only enable if your buffer size is small (10ms or less). - **Force mono audio**: use if you're unable to hear stereo audio (e.g. single speaker or hearing loss in one ear). @@ -88,6 +102,7 @@ settings are saved when clicking the **OK** or **Apply** buttons at the bottom o - **Quality**: selects quality of resampling. low quality reduces CPU load by a small amount. - **Software clipping**: clips output to nominal range (-1.0 to 1.0) before passing it to the audio device. - this avoids activating Windows' built-in limiter. + - this option shall be enabled when using PortAudio backend with a DirectSound device. ### Metronome From c21f880e3e29876a5d5fb1236a11f626950443e3 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 1 Sep 2023 18:33:08 -0500 Subject: [PATCH 03/58] GUI: update credits --- src/gui/about.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 0c9834795..d1ab6b983 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -79,6 +79,7 @@ const char* aboutLine[]={ "Burnt Fishy", "CaptainMalware", "Clingojam", + "Crisps", "DeMOSic", "DevEd", "Dippy", From 1c171ed7bd017e9846aca9f2378fbd20579afb3a Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 1 Sep 2023 18:33:32 -0500 Subject: [PATCH 04/58] GUI: de-duplicate file dialog filters untested. may not work... --- src/gui/fileDialog.cpp | 39 +++++++++++++++++++++++++++++++++++++-- src/gui/fileDialog.h | 7 +++++-- src/gui/gui.cpp | 41 +---------------------------------------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/gui/fileDialog.cpp b/src/gui/fileDialog.cpp index 17d30a40f..e4dd8d3cd 100644 --- a/src/gui/fileDialog.cpp +++ b/src/gui/fileDialog.cpp @@ -76,7 +76,38 @@ void _nfdThread(const NFDState state, std::atomic* ok, std::vector } #endif -bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback, bool allowMultiple) { +void FurnaceGUIFileDialog::convertFilterList(std::vector& filter) { + memset(noSysFilter,0,4096); + + String result; + + for (size_t i=0; (i+1) filter, String path, double dpiScale, FileDialogSelectCallback clickCallback, bool allowMultiple) { if (opened) return false; saving=false; curPath=path; @@ -149,6 +180,8 @@ bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, c } #endif + convertFilterList(filter); + ImGuiFileDialog::Instance()->singleClickSel=mobileUI; ImGuiFileDialog::Instance()->DpiScale=dpiScale; ImGuiFileDialog::Instance()->mobileMode=mobileUI; @@ -159,7 +192,7 @@ bool FurnaceGUIFileDialog::openLoad(String header, std::vector filter, c return true; } -bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale) { +bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, String path, double dpiScale) { if (opened) return false; #ifdef ANDROID @@ -233,6 +266,8 @@ bool FurnaceGUIFileDialog::openSave(String header, std::vector filter, c } else { hasError=false; + convertFilterList(filter); + ImGuiFileDialog::Instance()->singleClickSel=false; ImGuiFileDialog::Instance()->DpiScale=dpiScale; ImGuiFileDialog::Instance()->mobileMode=mobileUI; diff --git a/src/gui/fileDialog.h b/src/gui/fileDialog.h index b4a6d46e6..371fa7c21 100644 --- a/src/gui/fileDialog.h +++ b/src/gui/fileDialog.h @@ -31,6 +31,7 @@ class FurnaceGUIFileDialog { bool opened; bool saving; bool hasError; + char noSysFilter[4096]; String curPath; std::vector fileName; #ifdef USE_NFD @@ -46,10 +47,12 @@ class FurnaceGUIFileDialog { pfd::open_file* dialogO; pfd::save_file* dialogS; #endif + + void convertFilterList(std::vector& filter); public: bool mobileUI; - bool openLoad(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback=NULL, bool allowMultiple=false); - bool openSave(String header, std::vector filter, const char* noSysFilter, String path, double dpiScale); + bool openLoad(String header, std::vector filter, String path, double dpiScale, FileDialogSelectCallback clickCallback=NULL, bool allowMultiple=false); + bool openSave(String header, std::vector filter, String path, double dpiScale); bool accepted(); void close(); bool render(const ImVec2& min, const ImVec2& max); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 530b402b8..60e0c650f 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1567,7 +1567,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Open File", {"compatible files", "*.fur *.dmf *.mod *.fc13 *.fc14 *.smod *.fc", "all files", "*"}, - "compatible files{.fur,.dmf,.mod,.fc13,.fc14,.smod,.fc},.*", workingDirSong, dpiScale ); @@ -1580,7 +1579,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Restore Backup", {"Furnace song", "*.fur"}, - "Furnace song{.fur}", backupPath+String(DIR_SEPARATOR_STR), dpiScale ); @@ -1590,7 +1588,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save File", {"Furnace song", "*.fur"}, - "Furnace song{.fur}", workingDirSong, dpiScale ); @@ -1600,7 +1597,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save File", {"DefleMask 1.1.3 module", "*.dmf"}, - "DefleMask 1.1.3 module{.dmf}", workingDirSong, dpiScale ); @@ -1610,7 +1606,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save File", {"DefleMask 1.0/legacy module", "*.dmf"}, - "DefleMask 1.0/legacy module{.dmf}", workingDirSong, dpiScale ); @@ -1627,8 +1622,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirIns)) workingDirIns=getHomeDir(); hasOpened=fileDialog->openLoad( "Load Instrument", - // TODO supply loadable formats in a dynamic, scalable, "DRY" way. - // thank the author of IGFD for making things impossible {"all compatible files", "*.fui *.dmp *.tfi *.vgi *.s3i *.sbi *.opli *.opni *.y12 *.bnk *.ff *.gyb *.opm *.wopl *.wopn", "Furnace instrument", "*.fui", "DefleMask preset", "*.dmp", @@ -1646,7 +1639,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Wohlstand WOPL bank", "*.wopl", "Wohlstand WOPN bank", "*.wopn", "all files", "*"}, - "all compatible files{.fui,.dmp,.tfi,.vgi,.s3i,.sbi,.opli,.opni,.y12,.bnk,.ff,.gyb,.opm,.wopl,.wopn},.*", workingDirIns, dpiScale, [this](const char* path) { @@ -1681,7 +1673,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Instrument", {"Furnace instrument", "*.fui"}, - "Furnace instrument{.fui}", workingDirIns, dpiScale ); @@ -1691,7 +1682,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Instrument", {"DefleMask preset", "*.dmp"}, - "DefleMask preset{.dmp}", workingDirIns, dpiScale ); @@ -1703,7 +1693,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Load Wavetable", {"compatible files", "*.fuw *.dmw", "all files", "*"}, - "compatible files{.fuw,.dmw},.*", workingDirWave, dpiScale, NULL, // TODO @@ -1715,7 +1704,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Wavetable", {"Furnace wavetable", ".fuw"}, - "Furnace wavetable{.fuw}", workingDirWave, dpiScale ); @@ -1725,7 +1713,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Wavetable", {"DefleMask wavetable", ".dmw"}, - "DefleMask wavetable{.dmw}", workingDirWave, dpiScale ); @@ -1735,7 +1722,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Wavetable", {"raw data", ".raw"}, - "raw data{.raw}", workingDirWave, dpiScale ); @@ -1747,7 +1733,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Load Sample", {"compatible files", "*.wav *.dmc *.brr", "all files", "*"}, - "compatible files{.wav,.dmc,.brr},.*", workingDirSample, dpiScale, NULL, // TODO @@ -1760,7 +1745,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Load Raw Sample", {"all files", "*"}, - ".*", workingDirSample, dpiScale ); @@ -1770,7 +1754,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Sample", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirSample, dpiScale ); @@ -1778,9 +1761,8 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { case GUI_FILE_SAMPLE_SAVE_RAW: if (!dirExists(workingDirSample)) workingDirSample=getHomeDir(); hasOpened=fileDialog->openSave( - "Load Raw Sample", + "Save Raw Sample", {"all files", "*"}, - ".*", workingDirSample, dpiScale ); @@ -1790,7 +1772,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Audio", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirAudioExport, dpiScale ); @@ -1800,7 +1781,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Audio", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirAudioExport, dpiScale ); @@ -1810,7 +1790,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Audio", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirAudioExport, dpiScale ); @@ -1820,7 +1799,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export VGM", {"VGM file", "*.vgm"}, - "VGM file{.vgm}", workingDirVGMExport, dpiScale ); @@ -1830,7 +1808,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export ZSM", {"ZSM file", "*.zsm"}, - "ZSM file{.zsm}", workingDirZSMExport, dpiScale ); @@ -1840,7 +1817,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Command Stream", {"text file", "*.txt"}, - "text file{.txt}", workingDirROMExport, dpiScale ); @@ -1850,7 +1826,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Command Stream", {"binary file", "*.bin"}, - "binary file{.bin}", workingDirROMExport, dpiScale ); @@ -1863,7 +1838,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Font", {"compatible files", "*.ttf *.otf *.ttc"}, - "compatible files{.ttf,.otf,.ttc}", workingDirFont, dpiScale ); @@ -1873,7 +1847,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Font", {"compatible files", "*.ttf *.otf *.ttc"}, - "compatible files{.ttf,.otf,.ttc}", workingDirFont, dpiScale ); @@ -1883,7 +1856,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Font", {"compatible files", "*.ttf *.otf *.ttc"}, - "compatible files{.ttf,.otf,.ttc}", workingDirFont, dpiScale ); @@ -1893,7 +1865,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Color File", {"configuration files", "*.cfgc"}, - "configuration files{.cfgc}", workingDirColors, dpiScale ); @@ -1903,7 +1874,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Keybind File", {"configuration files", "*.cfgk"}, - "configuration files{.cfgk}", workingDirKeybinds, dpiScale ); @@ -1913,7 +1883,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Layout File", {".ini files", "*.ini"}, - ".ini files{.ini}", workingDirKeybinds, dpiScale ); @@ -1923,7 +1892,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Colors", {"configuration files", "*.cfgc"}, - "configuration files{.cfgc}", workingDirColors, dpiScale ); @@ -1933,7 +1901,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Keybinds", {"configuration files", "*.cfgk"}, - "configuration files{.cfgk}", workingDirKeybinds, dpiScale ); @@ -1943,7 +1910,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Layout", {".ini files", "*.ini"}, - ".ini files{.ini}", workingDirKeybinds, dpiScale ); @@ -1956,7 +1922,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Load ROM", {"compatible files", "*.rom *.bin", "all files", "*"}, - "compatible files{.rom,.bin},.*", workingDirROM, dpiScale ); @@ -1967,7 +1932,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Play Command Stream", {"command stream", "*.bin", "all files", "*"}, - "command stream{.bin},.*", workingDirROM, dpiScale ); @@ -1979,7 +1943,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { {"compatible files", "*.fur *.dmf *.mod", "another option", "*.wav *.ttf", "all files", "*"}, - "compatible files{.fur,.dmf,.mod},another option{.wav,.ttf},.*", workingDirTest, dpiScale, [](const char* path) { @@ -1998,7 +1961,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { {"compatible files", "*.fur *.dmf *.mod", "another option", "*.wav *.ttf", "all files", "*"}, - "compatible files{.fur,.dmf,.mod},another option{.wav,.ttf},.*", workingDirTest, dpiScale, [](const char* path) { @@ -2017,7 +1979,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Save Test", {"Furnace song", "*.fur", "DefleMask module", "*.dmf"}, - "Furnace song{.fur},DefleMask module{.dmf}", workingDirTest, dpiScale ); From 716d42ee6d5f9e82865380b53328eac8c2a46256 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 1 Sep 2023 19:59:43 -0500 Subject: [PATCH 05/58] IGFD: fix .* filter with label --- extern/igfd/ImGuiFileDialog.cpp | 3 ++- extern/igfd/ImGuiFileDialog.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/extern/igfd/ImGuiFileDialog.cpp b/extern/igfd/ImGuiFileDialog.cpp index b5152d7c6..82207a587 100644 --- a/extern/igfd/ImGuiFileDialog.cpp +++ b/extern/igfd/ImGuiFileDialog.cpp @@ -720,6 +720,7 @@ namespace IGFD auto arr = IGFD::Utils::SplitStringToVector(fs, ',', false); for (auto a : arr) { + infos.firstFilter=a; infos.collectionfilters.emplace(a); } } @@ -1048,7 +1049,7 @@ namespace IGFD // check if current file extention is covered by current filter // we do that here, for avoid doing that during filelist display // for better fps - if (prSelectedFilter.exist(vTag) || prSelectedFilter.filter == ".*") + if (prSelectedFilter.exist(vTag) || prSelectedFilter.firstFilter == ".*") { return true; } diff --git a/extern/igfd/ImGuiFileDialog.h b/extern/igfd/ImGuiFileDialog.h index a2420918f..cecf885a3 100644 --- a/extern/igfd/ImGuiFileDialog.h +++ b/extern/igfd/ImGuiFileDialog.h @@ -745,6 +745,7 @@ namespace IGFD { public: std::string filter; + std::string firstFilter; std::set collectionfilters; public: From ef23b88ad3b0fdd36c572e5eb97864c296413c68 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 2 Sep 2023 03:58:11 -0500 Subject: [PATCH 06/58] NES: fix chan osc (noise, NSFplay) --- src/engine/platform/nes.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 657a084e0..92f7edfa8 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -145,7 +145,7 @@ void DivPlatformNES::acquire_NSFPlay(short** buf, size_t len) { oscBuf[0]->data[oscBuf[0]->needle++]=nes1_NP->out[0]<<11; oscBuf[1]->data[oscBuf[1]->needle++]=nes1_NP->out[1]<<11; oscBuf[2]->data[oscBuf[2]->needle++]=nes2_NP->out[0]<<11; - oscBuf[3]->data[oscBuf[3]->needle++]=nes2_NP->out[1]<<12; + oscBuf[3]->data[oscBuf[3]->needle++]=nes2_NP->out[1]<<11; oscBuf[4]->data[oscBuf[4]->needle++]=nes2_NP->out[2]<<8; } } From b26a76f343a138bed9931c692d15e2909b428bbf Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 2 Sep 2023 22:46:46 -0500 Subject: [PATCH 07/58] . --- instruments/FM/bass/(GEN) Thiel Bass.dmp | Bin 51 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 instruments/FM/bass/(GEN) Thiel Bass.dmp diff --git a/instruments/FM/bass/(GEN) Thiel Bass.dmp b/instruments/FM/bass/(GEN) Thiel Bass.dmp deleted file mode 100644 index 17817d34e90c5fec8a0ab9dc10e6b669e8a53b53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51 kcmd<)WMp7xVqy`MXJFuGU|?ooV3C6}8F}GM78W=Y03y}_HUIzs From 20ed22d6e804eddc4e5ecbfb1cc9f69b26091b82 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 2 Sep 2023 23:07:09 -0500 Subject: [PATCH 08/58] Revert "." This reverts commit b26a76f343a138bed9931c692d15e2909b428bbf. --- instruments/FM/bass/(GEN) Thiel Bass.dmp | Bin 0 -> 51 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 instruments/FM/bass/(GEN) Thiel Bass.dmp diff --git a/instruments/FM/bass/(GEN) Thiel Bass.dmp b/instruments/FM/bass/(GEN) Thiel Bass.dmp new file mode 100644 index 0000000000000000000000000000000000000000..17817d34e90c5fec8a0ab9dc10e6b669e8a53b53 GIT binary patch literal 51 kcmd<)WMp7xVqy`MXJFuGU|?ooV3C6}8F}GM78W=Y03y}_HUIzs literal 0 HcmV?d00001 From 19d0ed617a224ceaeda0a5ae970087f676d2a377 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 2 Sep 2023 23:57:55 -0500 Subject: [PATCH 09/58] what? --- src/gui/chanOsc.cpp | 58 ++++++++++++++++++++++++------------------ src/gui/gui.cpp | 3 ++- src/gui/gui.h | 6 ++++- src/gui/sampleEdit.cpp | 1 + 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 4371d6fd3..9b51d0f3f 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -417,8 +417,10 @@ void FurnaceGUI::drawChanOsc() { if (fft->plan==NULL) { logD("creating FFT plan for channel %d",ch); fft->inBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); - fft->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)); + fft->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)*2); + fft->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)*2); fft->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft->inBuf,fft->outBuf,FFTW_ESTIMATE); + fft->planI=fftw_plan_dft_c2r_1d(FURNACE_FFT_SIZE,fft->outBuf,fft->corrBuf,FFTW_ESTIMATE); } int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); @@ -447,38 +449,44 @@ void FurnaceGUI::drawChanOsc() { waveform[i]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f)); } } else { - float minLevel=1.0f; - float maxLevel=-1.0f; - float dcOff=0.0f; + //float minLevel=1.0f; + //float maxLevel=-1.0f; + //float dcOff=0.0f; unsigned short needlePos=buf->needle; //unsigned short needlePosOrig=needlePos; for (int i=0; iinBuf[i]=(double)buf->data[(unsigned short)(needlePos-displaySize*2+((i*displaySize*2)/FURNACE_FFT_SIZE))]/32768.0; + fft->inBuf[i]*=sin(M_PI*(double)i/(double)FURNACE_FFT_SIZE); } fftw_execute(fft->plan); - - // find origin frequency - int point=1; - double candAmp=0.0; - for (unsigned short i=1; i<512; i++) { - fftw_complex& f=fft->outBuf[i]; - // AMPLITUDE - double amp=sqrt(pow(f[0],2.0)+pow(f[1],2.0))/pow((double)i,0.8); - if (amp>candAmp) { - point=i; - candAmp=amp; - } - } - // PHASE - fftw_complex& candPoint=fft->outBuf[point]; - double phase=((double)(displaySize*2)/(double)point)*(0.5+(atan2(candPoint[1],candPoint[0])/(M_PI*2))); + for (int i=0; ioutBuf[i][0]-=fft->outBuf[0][0]; + //fft->outBuf[i][1]-=fft->outBuf[0][1]; + fft->outBuf[i][0]/=FURNACE_FFT_SIZE; + fft->outBuf[i][1]/=FURNACE_FFT_SIZE; + fft->outBuf[i][0]=fft->outBuf[i][0]*fft->outBuf[i][0]; + fft->outBuf[i][1]=fft->outBuf[i][1]*fft->outBuf[i][1]; + } + memset(&fft->outBuf[FURNACE_FFT_SIZE],0,sizeof(fftw_complex)*FURNACE_FFT_SIZE); + fftw_execute(fft->planI); if (chanOscWaveCorr) { - needlePos-=phase; + //needlePos-=phase; } - chanOscPitch[ch]=(float)point/32.0f; + //chanOscPitch[ch]=(float)point/32.0f; + for (unsigned short i=0; icorrBuf[(i*FURNACE_FFT_SIZE)/precision]-fft->corrBuf[FURNACE_FFT_SIZE-1-((i*FURNACE_FFT_SIZE)/precision)]; + if (i>=precision/2) { + y=fft->inBuf[((i-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; + } + + waveform[i]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } + + /* needlePos-=displaySize; for (unsigned short i=0; idata[(unsigned short)(needlePos+(i*displaySize/precision))]/32768.0f; @@ -494,10 +502,10 @@ void FurnaceGUI::drawChanOsc() { if (y>0.5f) y=0.5f; y*=chanOscAmplify; waveform[i]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - } + }*/ - //String cPhase=fmt::sprintf("%d cphase: %f\nvol: %f\nmin: %.2f\nmax: %.2f\ndcOff: %.2f\nneedles:\n- %d\n- %d\n- %d (%s)",point,phase,chanOscVol[ch],minLevel,maxLevel,dcOff,needlePosOrig,needlePos,(needlePos+displaySize),((needlePos+displaySize)>=needlePosOrig)?"WARN":"OK"); - //dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); + String cPhase=fmt::sprintf("\n%f",fft->corrBuf[0]); + dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); } ImU32 color=ImGui::GetColorU32(chanOscColor); if (chanOscUseGrad) { diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 60e0c650f..8a9521277 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -6273,7 +6273,8 @@ bool FurnaceGUI::init() { pianoView=e->getConfInt("pianoView",pianoView); pianoInputPadMode=e->getConfInt("pianoInputPadMode",pianoInputPadMode); - chanOscCols=e->getConfInt("chanOscCols",3); + //chanOscCols=e->getConfInt("chanOscCols",3); + chanOscCols=1; chanOscAutoColsType=e->getConfInt("chanOscAutoColsType",0); chanOscColorX=e->getConfInt("chanOscColorX",GUI_OSCREF_CENTER); chanOscColorY=e->getConfInt("chanOscColorY",GUI_OSCREF_CENTER); diff --git a/src/gui/gui.h b/src/gui/gui.h index 4f90a7bd9..c3b5448d4 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2060,14 +2060,18 @@ class FurnaceGUI { double inBufPosFrac; unsigned short needle; fftw_complex* outBuf; + double* corrBuf; fftw_plan plan; + fftw_plan planI; ChanOscStatus(): inBuf(NULL), inBufPos(0), inBufPosFrac(0.0f), needle(0), outBuf(NULL), - plan(NULL) {} + corrBuf(NULL), + plan(NULL), + planI(NULL) {} } chanOscChan[DIV_MAX_CHANS]; // visualizer diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 2ca22f14e..bdfba982f 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -1197,6 +1197,7 @@ void FurnaceGUI::drawSampleEdit() { sameLineMaybe(ImGui::CalcTextSize("Zoom").x+150.0f*dpiScale+ImGui::CalcTextSize("100%").x); double zoomPercent=100.0/sampleZoom; bool checkZoomLimit=false; + ImGui::AlignTextToFramePadding(); ImGui::Text("Zoom"); ImGui::SameLine(); ImGui::SetNextItemWidth(150.0f*dpiScale); From f6db75fae1bc62c474c99bd5085e60d7f70e9df0 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 3 Sep 2023 04:22:00 -0500 Subject: [PATCH 10/58] GUI: massive chan osc improvements --- src/gui/chanOsc.cpp | 173 ++++++++++++++++++++++++++++++++------------ src/gui/gui.cpp | 4 +- src/gui/gui.h | 2 +- 3 files changed, 130 insertions(+), 49 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 9b51d0f3f..7e7e8efe7 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -23,6 +23,7 @@ #include "imgui.h" #include "imgui_internal.h" #include "misc/cpp/imgui_stdlib.h" +#include #define FURNACE_FFT_SIZE 4096 #define FURNACE_FFT_RATE 80.0 @@ -417,8 +418,8 @@ void FurnaceGUI::drawChanOsc() { if (fft->plan==NULL) { logD("creating FFT plan for channel %d",ch); fft->inBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); - fft->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)*2); - fft->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)*2); + fft->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)); + fft->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); fft->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft->inBuf,fft->outBuf,FFTW_ESTIMATE); fft->planI=fftw_plan_dft_c2r_1d(FURNACE_FFT_SIZE,fft->outBuf,fft->corrBuf,FFTW_ESTIMATE); } @@ -444,67 +445,145 @@ void FurnaceGUI::drawChanOsc() { ImGui::ItemSize(size,style.FramePadding.y); if (ImGui::ItemAdd(rect,ImGui::GetID("chOscDisplay"))) { if (!e->isRunning()) { - for (unsigned short i=0; ineedle; //unsigned short needlePosOrig=needlePos; - for (int i=0; iinBuf[i]=(double)buf->data[(unsigned short)(needlePos-displaySize*2+((i*displaySize*2)/FURNACE_FFT_SIZE))]/32768.0; - fft->inBuf[i]*=sin(M_PI*(double)i/(double)FURNACE_FFT_SIZE); + + // first FFT + for (int j=0; j<(FURNACE_FFT_SIZE); j++) { + fft->inBuf[j]=(double)buf->data[(unsigned short)(needlePos-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; + fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); } + //memset(&fft->inBuf[FURNACE_FFT_SIZE>>1],0,sizeof(double)*(FURNACE_FFT_SIZE>>1)); fftw_execute(fft->plan); - for (int i=0; ioutBuf[i][0]-=fft->outBuf[0][0]; - //fft->outBuf[i][1]-=fft->outBuf[0][1]; - fft->outBuf[i][0]/=FURNACE_FFT_SIZE; - fft->outBuf[i][1]/=FURNACE_FFT_SIZE; - fft->outBuf[i][0]=fft->outBuf[i][0]*fft->outBuf[i][0]; - fft->outBuf[i][1]=fft->outBuf[i][1]*fft->outBuf[i][1]; + // auto-correlation and second FFT + for (int j=0; joutBuf[j][0]-=fft->outBuf[0][0]; + //fft->outBuf[j][1]-=fft->outBuf[0][1]; + fft->outBuf[j][0]/=FURNACE_FFT_SIZE; + fft->outBuf[j][1]/=FURNACE_FFT_SIZE; + fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; + fft->outBuf[j][1]=0; } - memset(&fft->outBuf[FURNACE_FFT_SIZE],0,sizeof(fftw_complex)*FURNACE_FFT_SIZE); + fft->outBuf[0][0]=0; + fft->outBuf[0][1]=0; + fft->outBuf[1][0]=0; + fft->outBuf[1][1]=0; + //memset(&fft->outBuf[FURNACE_FFT_SIZE],0,sizeof(fftw_complex)*FURNACE_FFT_SIZE); fftw_execute(fft->planI); + + // high-pass + /*for (int j=1; j<(FURNACE_FFT_SIZE>>1); j++) { + fft->corrBuf[j]-=fft->corrBuf[j-1]; + }*/ + + // find size of period + double waveLen=FURNACE_FFT_SIZE; + double waveLenCandL=DBL_MAX; + double waveLenCandH=DBL_MIN; + int waveLenBottom=0; + /* + int waveLenTop=0; + double waveLenCandL2=DBL_MAX;*/ + // find lowest point + for (int j=(FURNACE_FFT_SIZE>>1); j>2; j--) { + if (fft->corrBuf[j]corrBuf[j]; + waveLenBottom=j; + } + } + + // find highest point + for (int j=(FURNACE_FFT_SIZE>>1); j>waveLenBottom; j--) { + if (fft->corrBuf[j]>waveLenCandH) { + waveLenCandH=fft->corrBuf[j]; + waveLen=j; + } + } + /* + // find next lowest point + for (int j=(FURNACE_FFT_SIZE>>1); j>waveLenTop; j--) { + if (fft->corrBuf[j]corrBuf[j]; + waveLen=j-waveLenTop; + } + }*/ + waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; + + // DFT of one period (x_1) + std::complex dft(0,0); + for (int j=0; jdata[(unsigned short)(needlePos-waveLen+j)]/32768.0)*std::exp((-std::complex(0.0,1.0)*2.0*M_PI/(double)waveLen)*(double)j); + } + + // calculate and lock into phase + double phase=(0.5+(atan2(dft.imag(),dft.real())/(2.0*M_PI))); + //phase-=sin(4.0*phase*M_PI)/21.0; + + double maxavg=0.0; + for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { + if (fabs(fft->corrBuf[j]>maxavg)) { + maxavg=fabs(fft->corrBuf[j]); + } + } + if (maxavg>0.0000001) maxavg=0.5/maxavg; if (chanOscWaveCorr) { - //needlePos-=phase; + needlePos-=phase*waveLen; + needlePos-=displaySize/waveLen; } //chanOscPitch[ch]=(float)point/32.0f; - for (unsigned short i=0; icorrBuf[(i*FURNACE_FFT_SIZE)/precision]-fft->corrBuf[FURNACE_FFT_SIZE-1-((i*FURNACE_FFT_SIZE)/precision)]; - if (i>=precision/2) { - y=fft->inBuf[((i-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; - } - - waveform[i]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - } - - /* needlePos-=displaySize; - for (unsigned short i=0; idata[(unsigned short)(needlePos+(i*displaySize/precision))]/32768.0f; + for (unsigned short j=0; jdata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; if (minLevel>y) minLevel=y; if (maxLeveldata[(unsigned short)(needlePos+(i*displaySize/precision))]/32768.0f; + for (unsigned short j=0; jdata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; y-=dcOff; if (y<-0.5f) y=-0.5f; if (y>0.5f) y=0.5f; y*=chanOscAmplify; - waveform[i]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - }*/ + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } - String cPhase=fmt::sprintf("\n%f",fft->corrBuf[0]); + // FFT debug code! + if (debugFFT) { + for (unsigned short j=0; jcorrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; + if (j>=precision/2) { + y=fft->inBuf[((j-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; + } + + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } + } + + String cPhase=fmt::sprintf("\n%.1f - %.2f (%.2f %.2f)",waveLen,phase,dft.imag(),dft.real()); dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); } ImU32 color=ImGui::GetColorU32(chanOscColor); @@ -525,9 +604,9 @@ void FurnaceGUI::drawChanOsc() { String text; bool inFormat=false; - for (char i: chanOscTextFormat) { + for (char j: chanOscTextFormat) { if (inFormat) { - switch (i) { + switch (j) { case 'c': text+=e->getChannelName(ch); break; @@ -568,7 +647,7 @@ void FurnaceGUI::drawChanOsc() { break; } case 'p': { - text+=FurnaceGUI::getSystemPartNumber(e->sysOfChan[ch], e->song.systemFlags[e->dispatchOfChan[ch]]); + text+=FurnaceGUI::getSystemPartNumber(e->sysOfChan[ch],e->song.systemFlags[e->dispatchOfChan[ch]]); break; } case 'S': { @@ -609,19 +688,18 @@ void FurnaceGUI::drawChanOsc() { break; default: text+='%'; - text+=i; + text+=j; break; } inFormat=false; } else { - if (i=='%') { + if (j=='%') { inFormat=true; } else { - text+=i; + text+=j; } } } - dl->AddText(ImLerp(inRect.Min,inRect.Max,ImVec2(0.0f,0.0f)),ImGui::GetColorU32(chanOscTextColor),text.c_str()); } @@ -634,6 +712,9 @@ void FurnaceGUI::drawChanOsc() { if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { chanOscOptions=!chanOscOptions; } + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + debugFFT=!debugFFT; + } } ImGui::PopStyleVar(); } diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 8a9521277..7673a19ca 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -6273,8 +6273,7 @@ bool FurnaceGUI::init() { pianoView=e->getConfInt("pianoView",pianoView); pianoInputPadMode=e->getConfInt("pianoInputPadMode",pianoInputPadMode); - //chanOscCols=e->getConfInt("chanOscCols",3); - chanOscCols=1; + chanOscCols=e->getConfInt("chanOscCols",3); chanOscAutoColsType=e->getConfInt("chanOscAutoColsType",0); chanOscColorX=e->getConfInt("chanOscColorX",GUI_OSCREF_CENTER); chanOscColorY=e->getConfInt("chanOscColorY",GUI_OSCREF_CENTER); @@ -6912,6 +6911,7 @@ FurnaceGUI::FurnaceGUI(): killGraphics(false), audioEngineChanged(false), settingsChanged(false), + debugFFT(false), vgmExportVersion(0x171), vgmExportTrailingTicks(-1), drawHalt(10), diff --git a/src/gui/gui.h b/src/gui/gui.h index c3b5448d4..b28918fda 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1334,7 +1334,7 @@ class FurnaceGUI { bool displayPendingIns, pendingInsSingle, displayPendingRawSample, snesFilterHex, modTableHex, displayEditString; bool mobileEdit; bool killGraphics; - bool audioEngineChanged, settingsChanged; + bool audioEngineChanged, settingsChanged, debugFFT; bool willExport[DIV_MAX_CHIPS]; int vgmExportVersion; int vgmExportTrailingTicks; From 7ea5f2de07267767efa0696500f93f61dec24c8b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 3 Sep 2023 04:22:17 -0500 Subject: [PATCH 11/58] remove some debug info --- src/gui/chanOsc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 7e7e8efe7..e14b90547 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -583,8 +583,8 @@ void FurnaceGUI::drawChanOsc() { } } - String cPhase=fmt::sprintf("\n%.1f - %.2f (%.2f %.2f)",waveLen,phase,dft.imag(),dft.real()); - dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); + /*String cPhase=fmt::sprintf("\n%.1f - %.2f (%.2f %.2f)",waveLen,phase,dft.imag(),dft.real()); + dl->AddText(inRect.Min,0xffffffff,cPhase.c_str());*/ } ImU32 color=ImGui::GetColorU32(chanOscColor); if (chanOscUseGrad) { From 25a83bf1b9c3395bc00449dc833eaec628ab492a Mon Sep 17 00:00:00 2001 From: thacuber2a03 Date: Sun, 3 Sep 2023 14:12:56 -0400 Subject: [PATCH 12/58] add sweatsmile bossfight --- demos/nes/sweatsmile_bossfight.fur | Bin 0 -> 4556 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 demos/nes/sweatsmile_bossfight.fur diff --git a/demos/nes/sweatsmile_bossfight.fur b/demos/nes/sweatsmile_bossfight.fur new file mode 100644 index 0000000000000000000000000000000000000000..7aa2652cb25ac63bec2b3b7da2f3d9b8849d737e GIT binary patch literal 4556 zcmZ`&Ra6rIprunmk(LyNjT#M-GPN|!W9Ga8YG(SoGFDCuq(Eu&t3 zy>s5*J@?%Bxaaz1jTn(bsSPtwWLpsCZ3-lH4fz11WUA(5kG_shpu<;PCTr{n(U{&X3b!8_lX#Zcj# zr=vniclRA-Ej(IWL_SQf7NyHa!^>f{iv>u6GeFvslM^{St-Uxofa)P~p*8bA;?NKr2$Kv$fOKERui4bwzk3 zNFj2n6PJxkCDJf$d@1CK@M)MIV_Fhrb*CpAHMKRc23G>74KE09;XPZg`HGF@1b}@n zqywKHNWJE1Ic)@H!n-G?hc`SS4Jf>f-;0At7{n*(|{~Ul@7Gl4my+Ztk z5~Hy)(^=A~RS@n+Fu=^ANWDW85MqHdhI@>YVv`{JR*FL8iIh%cFrGQV60R)XJ&q=b zDj(mE=nv{uuV^Q8r~Hb{if^aux?m+cJBm1(o7P&KZA_)2)43B(3U(nf$Ngo-*1L>H zi9@GJZ7t`q84kYX5V(55&VsLv7c5NbiIW712wpK-DeIuJX7^aNbvf0jLA)6&0xyUX z+L&?nh7E=9;2&jjD-O}TcT}R8sNM}pu-PIyJq;z~SjF`pu9HJ(O+pnWz0!#J-UvS` zWmM*mdtoSt^D5iSe*5W637$xDLP2KpY=$hW_@zVIBFaD3!&mSw^(f+9Bz5ZvXy1Qj ztov_$%tfVhl(-nI0*FHTFb}N#NI9z>#AJ`>?z7ObD3Q=~)_jVU8I9>QdMd$z=b-p@ zs!Nk3>zRD|noA^`30)@&=aR4n6avRRAh)!@xxt(Lzf@or%gJ5{1VILrbe zOq~Q?A93P|H}K`(M7}&os?q6|CB6CoK#ogxoczt3E){RtHSx61I@!(dn>MgYvpM;w zqIy)b$h6m@K^;ONP-Xw`buSK$Yq>@1Pf^C!Dx15AG+G~+du)D0qAMYk&CHFxFZYE-i- zl@~vL)L9FX6IZ$7!Pju>w5GjDy`j9O<<+2k_RpJASS}@obI;gmMl^2r!LiCsyLs>_ z^ZxUweW&@cmw%-d_w^4x;IEyl($(I0dbs~hplzISyImWPwfChuy3KE{MHwaY7ui9; z4SryCQ?~9}Y<=aKv3P@lj;Ti<8d=`$STARuF6Mh_q06YANR|m-t?lNK&B3H zhQZ3m;XaP(C zW3Y%dd@f?5UwY}e0jiHQgE^t0ec^I!x*>G!?Jee{=c>XiXyg)tt@eM7s6cw zyMIz9bpc^H6U+%}D|9^Jf)~IR>JDnP`k%EY*o?Ke{n*%6BCcLMN6(K{1>1IMZOi(t z|I=}zlr@K8*0mZ5@`~6nZsyA(n zEbQc;A?U#WbX>((d?f8|9y7)Wk)uXN9y~X}{-UHqI0qv(*N=5bWG=DJ2_*^MiS&jtmpHUpKT@+XTA=^&Xcq0;itF|Ftx61wfP|(9goKS|s3G zyU?_Or>cMHMb*uN(bS~K3yCo5Z^QM_b0c4>{k3;AWuKtt`2dKUnkw9+M*Vln&Pym} zz1Qv{>OC`E%@Zk&fh=w0gP(Sd?4hLMoqvUyiw@zvN{V#eG!Pua{VsvCM+TG2wLW3l z&U5(Og0?+O@yS;JMOvSy2ud62GfFDP<6o>3F?VU&&{xlg1~2gUEYKON$b5@^HX81( zU6auL30x}@Oy#8NSYa7sI`N@Kv_cIx%!>P)6dc9RoL@Djue#$y_AOstaPEW(7A;=3 zq$~}$K5gm&TpIR-!R6n8M#9ys>gjWlaF`^a*kdA z|KtQXq^B_?n+V{iZd(~RV%M;e4u>Um;-(tsVyB!V{Cn?THiAdg;~j+E#%SW`OLV1+ z{kqsv*nU!_-LYM}aq_9BiIEIrNG?^@sS~ZnhNeavsRcg^^-6ZPIVXlms%tUF=zO6g zww=wVInD@W1`Ojh*Gt4Zr)IV`ALU`fX-SMqN#MAKg@oW*z4oVx3=Ik`i41x=^kkp& z=Bz#d?f8nM@$@JfQWTmegbH&6(C*6gYar%d;Y>tMtT6cOhU6=xHc(~T)W7P)i5G85xv%L3Kg2DU2$Wzp?|LcA_Iq$CA0rLb7Y(TE0wIagc! zM=AJXxckfj6ZzF4E{N2bK2#KD*+J)#UXTs?KL5oAkPYq&A5HC1(T2r*OtiWW!{)F9 zSCwB%gD9E>hASgx3&ds>%)1x?P>DZI57I+dh2e_mdjJino`*-=Y#<26u8(vn6pejf9%dcMq~}h76bTOvo*W!0!E^`Vf$EXPB9jG%CgNpv6;Ttyl0df#YC$qlPcU5YvhIW}HU`8dC1IC*1%< zOqkmDoBkfH%{;_?O&!2%sK1!_GhveXW0_WwJCXW|=I0vIRj*-;*Sye_UBCXUC(Br) z-55Ylomi&Z3D13m-#5X}0m&ex!Tht%z}I2(wYb%ZH=5FlR`^WJUR{#B1-R3zs^B`# z7xF%lF%F6ky!}>Gb4sW$)I?gs%yhLe-sVqJo*?y1EvC1}y3Z@HSS!bEslu}2ryyq#z=mSu z9fjh<#K-TX#<+3NI!A8zKuNel!d9d*4Fw3k zo79{h(WbamG1(e+AH8xwg=)g?&MP*L@%H`(_P0NJT&DBdEy&HLQ1zSB+;6fMJdqUx zFArJH{_AQx^NK0V$~0=_fM{+#7$b+$4}&4?ck8EWG2y>J3!GZ^DHMeWgVXZc`H!Ht zst(}lu68P|POm@)TT1|tz(;ZV>F&AEz#B2716Tl2nt_-1M9T^9+x!q4zJeNlPoYPK zE1Onv@c7B3N%;}^c4+wv(tJf_*NI^;(GRuPkyA`zmqno@dK^eWqN}&lLy!BKfgUoa zlRRbJBx&m4eteT@fi-kN#AW3o`6Hkmq_K`CU^h95CyPptpNr7c0E4@}GOeAhuJZi?%UOk0(ZPN7T>EwSmikT#(YwM#gFiOGQM0UYjq=*N zsd%NwM(FrdM!jP5QoTG2mGAaxWaPDj8&$C)drZEUdPJja(s|Y2m&mA+8M_D=r6xIH zN)~g2Uk)|J#aR;VIV0C}wa0(+*=W|LqrR?@{)=GuWO^k^lsgJ7nik@&S59f6enr(7 ztvA76L}jFIIOZ5l{acSuih)x|(^ewsXW;Nqd_ebib>`Zh$aoR3m7?n0|0;IdP6c86Q18=#2lFg72y!{wPlm@~8^UXt+mnd15Z#A4tf2Oam>wrj}}d19L<+>N!X352^#} z@CDPDcva7Y1}YjV67#bD+fkFPnP#mmUSzAg*XvRgFkBarQp{6tkH~9Y(@XRE&z}EG zrJA=OHA;?GBJ~Hd+wFyy9jcpIL-weoPAr|2rBjT_v*i75In|qv3oTzK4Js zn>NK*5{m#VQ(rwJQd&N7*L(Y47&yt8A%jOm4EMOxxf*vw^=1l$bgH}Sl($x`LytAS zpQ6BOuOxPMru^RhWo+t$Pf=+f^J&N3FBu($$vbWOy(?l0s;3e^baMY|(cGKFbjN(L zpRU`LCF#M4A;5>LGAeU^33xTW`Z8;w!^7*;S+nYK77XSOec+ahd!{HUMyqKEMP zHo?xx&imO6mN@M)pV2gh%7lIn9ibAaTuEy9TfDdZW oKi_r7z8kuIXMdk5i>(_JL#3@~G@wVIqephuD-XC7^udV#0KCufm;e9( literal 0 HcmV?d00001 From 83c64aa4b4e6f193d33ac5764f30fa9238143f04 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 3 Sep 2023 17:18:31 -0500 Subject: [PATCH 13/58] fix the crash the hell? one double and suddenly it crashes on Android? --- src/gui/chanOsc.cpp | 54 +++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index e14b90547..cfdb3f9e1 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -23,7 +23,6 @@ #include "imgui.h" #include "imgui_internal.h" #include "misc/cpp/imgui_stdlib.h" -#include #define FURNACE_FFT_SIZE 4096 #define FURNACE_FFT_RATE 80.0 @@ -422,6 +421,15 @@ void FurnaceGUI::drawChanOsc() { fft->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); fft->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft->inBuf,fft->outBuf,FFTW_ESTIMATE); fft->planI=fftw_plan_dft_c2r_1d(FURNACE_FFT_SIZE,fft->outBuf,fft->corrBuf,FFTW_ESTIMATE); + if (fft->plan==NULL) { + logE("failed to create plan!"); + } + if (fft->planI==NULL) { + logE("failed to create inverse plan!"); + } + if (fft->inBuf==NULL || fft->outBuf==NULL || fft->corrBuf==NULL) { + logE("failed to create FFT buffers"); + } } int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); @@ -465,20 +473,16 @@ void FurnaceGUI::drawChanOsc() { float maxLevel=-1.0f; float dcOff=0.0f; unsigned short needlePos=buf->needle; - //unsigned short needlePosOrig=needlePos; // first FFT - for (int j=0; j<(FURNACE_FFT_SIZE); j++) { + for (int j=0; jinBuf[j]=(double)buf->data[(unsigned short)(needlePos-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); } - //memset(&fft->inBuf[FURNACE_FFT_SIZE>>1],0,sizeof(double)*(FURNACE_FFT_SIZE>>1)); fftw_execute(fft->plan); // auto-correlation and second FFT for (int j=0; joutBuf[j][0]-=fft->outBuf[0][0]; - //fft->outBuf[j][1]-=fft->outBuf[0][1]; fft->outBuf[j][0]/=FURNACE_FFT_SIZE; fft->outBuf[j][1]/=FURNACE_FFT_SIZE; fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; @@ -488,22 +492,14 @@ void FurnaceGUI::drawChanOsc() { fft->outBuf[0][1]=0; fft->outBuf[1][0]=0; fft->outBuf[1][1]=0; - //memset(&fft->outBuf[FURNACE_FFT_SIZE],0,sizeof(fftw_complex)*FURNACE_FFT_SIZE); fftw_execute(fft->planI); - - // high-pass - /*for (int j=1; j<(FURNACE_FFT_SIZE>>1); j++) { - fft->corrBuf[j]-=fft->corrBuf[j-1]; - }*/ // find size of period - double waveLen=FURNACE_FFT_SIZE; + double waveLen=FURNACE_FFT_SIZE-1; double waveLenCandL=DBL_MAX; double waveLenCandH=DBL_MIN; int waveLenBottom=0; - /* - int waveLenTop=0; - double waveLenCandL2=DBL_MAX;*/ + // find lowest point for (int j=(FURNACE_FFT_SIZE>>1); j>2; j--) { if (fft->corrBuf[j]>1); j>waveLenTop; j--) { - if (fft->corrBuf[j]corrBuf[j]; - waveLen=j-waveLenTop; - } - }*/ waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; // DFT of one period (x_1) - std::complex dft(0,0); + double dft[2]; + dft[0]=0.0; + dft[1]=0.0; for (int j=0; jdata[(unsigned short)(needlePos-waveLen+j)]/32768.0)*std::exp((-std::complex(0.0,1.0)*2.0*M_PI/(double)waveLen)*(double)j); + double one=((double)buf->data[(unsigned short)(needlePos-((int)waveLen)+j)&0xffff]/32768.0); + double two=(double)j*(-2.0*M_PI)/waveLen; + dft[0]+=one*cos(two); + dft[1]+=one*sin(two); } // calculate and lock into phase - double phase=(0.5+(atan2(dft.imag(),dft.real())/(2.0*M_PI))); - //phase-=sin(4.0*phase*M_PI)/21.0; + double phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); double maxavg=0.0; for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { @@ -549,7 +541,7 @@ void FurnaceGUI::drawChanOsc() { if (chanOscWaveCorr) { needlePos-=phase*waveLen; - needlePos-=displaySize/waveLen; + //needlePos-=displaySize/waveLen; } //chanOscPitch[ch]=(float)point/32.0f; @@ -571,7 +563,7 @@ void FurnaceGUI::drawChanOsc() { } // FFT debug code! - if (debugFFT) { + /*if (debugFFT) { for (unsigned short j=0; jcorrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; @@ -581,7 +573,7 @@ void FurnaceGUI::drawChanOsc() { waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); } - } + }*/ /*String cPhase=fmt::sprintf("\n%.1f - %.2f (%.2f %.2f)",waveLen,phase,dft.imag(),dft.real()); dl->AddText(inRect.Min,0xffffffff,cPhase.c_str());*/ From 90980a306239ff41cd83fdb4a4f53bd2494aca11 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 3 Sep 2023 19:08:30 -0500 Subject: [PATCH 14/58] GUI: center chan osc --- src/gui/chanOsc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index cfdb3f9e1..d9c58fbc6 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -541,7 +541,7 @@ void FurnaceGUI::drawChanOsc() { if (chanOscWaveCorr) { needlePos-=phase*waveLen; - //needlePos-=displaySize/waveLen; + needlePos-=(2*waveLen-fmod(displaySize,waveLen*2))*0.5; } //chanOscPitch[ch]=(float)point/32.0f; From 7a78ec1b6092d1aa886a63ba13f8b78e5d55f656 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 3 Sep 2023 20:09:03 -0500 Subject: [PATCH 15/58] GUI: optimize chan osc don't process FFT if not loud enough don't process DFT if we couldn't determine wave length --- src/gui/chanOsc.cpp | 141 ++++++++++++++++++++------------------------ 1 file changed, 65 insertions(+), 76 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index d9c58fbc6..637ae7d58 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -469,81 +469,86 @@ void FurnaceGUI::drawChanOsc() { // if you know how, please tell me // initialization + double phase=0.0; float minLevel=1.0f; float maxLevel=-1.0f; float dcOff=0.0f; unsigned short needlePos=buf->needle; + bool loudEnough=false; // first FFT for (int j=0; jinBuf[j]=(double)buf->data[(unsigned short)(needlePos-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; + if (fft->inBuf[j]>0.001 || fft->inBuf[j]<-0.001) loudEnough=true; fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); } - fftw_execute(fft->plan); - // auto-correlation and second FFT - for (int j=0; joutBuf[j][0]/=FURNACE_FFT_SIZE; - fft->outBuf[j][1]/=FURNACE_FFT_SIZE; - fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; - fft->outBuf[j][1]=0; - } - fft->outBuf[0][0]=0; - fft->outBuf[0][1]=0; - fft->outBuf[1][0]=0; - fft->outBuf[1][1]=0; - fftw_execute(fft->planI); + // only proceed if not quiet + if (loudEnough) { + fftw_execute(fft->plan); - // find size of period - double waveLen=FURNACE_FFT_SIZE-1; - double waveLenCandL=DBL_MAX; - double waveLenCandH=DBL_MIN; - int waveLenBottom=0; + // auto-correlation and second FFT + for (int j=0; joutBuf[j][0]/=FURNACE_FFT_SIZE; + fft->outBuf[j][1]/=FURNACE_FFT_SIZE; + fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; + fft->outBuf[j][1]=0; + } + fft->outBuf[0][0]=0; + fft->outBuf[0][1]=0; + fft->outBuf[1][0]=0; + fft->outBuf[1][1]=0; + fftw_execute(fft->planI); - // find lowest point - for (int j=(FURNACE_FFT_SIZE>>1); j>2; j--) { - if (fft->corrBuf[j]corrBuf[j]; - waveLenBottom=j; + // find size of period + double waveLen=FURNACE_FFT_SIZE-1; + double waveLenCandL=DBL_MAX; + double waveLenCandH=DBL_MIN; + int waveLenBottom=0; + + // find lowest point + for (int j=(FURNACE_FFT_SIZE>>1); j>2; j--) { + if (fft->corrBuf[j]corrBuf[j]; + waveLenBottom=j; + } + } + + // find highest point + for (int j=(FURNACE_FFT_SIZE>>1); j>waveLenBottom; j--) { + if (fft->corrBuf[j]>waveLenCandH) { + waveLenCandH=fft->corrBuf[j]; + waveLen=j; + } + } + + // did we find the period size? + if (waveLen<(FURNACE_FFT_SIZE-32)) { + waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; + + // we got pitch + chanOscPitch[ch]=1.0-pow(waveLen/(double)(FURNACE_FFT_SIZE>>1),2.0); + + // DFT of one period (x_1) + double dft[2]; + dft[0]=0.0; + dft[1]=0.0; + for (int j=0; jdata[(unsigned short)(needlePos-((int)waveLen)+j)&0xffff]/32768.0); + double two=(double)j*(-2.0*M_PI)/waveLen; + dft[0]+=one*cos(two); + dft[1]+=one*sin(two); + } + + // calculate and lock into phase + phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); + + if (chanOscWaveCorr) { + needlePos-=phase*waveLen; + needlePos-=(2*waveLen-fmod(displaySize,waveLen*2))*0.5; + } } } - - // find highest point - for (int j=(FURNACE_FFT_SIZE>>1); j>waveLenBottom; j--) { - if (fft->corrBuf[j]>waveLenCandH) { - waveLenCandH=fft->corrBuf[j]; - waveLen=j; - } - } - waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; - - // DFT of one period (x_1) - double dft[2]; - dft[0]=0.0; - dft[1]=0.0; - for (int j=0; jdata[(unsigned short)(needlePos-((int)waveLen)+j)&0xffff]/32768.0); - double two=(double)j*(-2.0*M_PI)/waveLen; - dft[0]+=one*cos(two); - dft[1]+=one*sin(two); - } - - // calculate and lock into phase - double phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); - - double maxavg=0.0; - for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { - if (fabs(fft->corrBuf[j]>maxavg)) { - maxavg=fabs(fft->corrBuf[j]); - } - } - if (maxavg>0.0000001) maxavg=0.5/maxavg; - - if (chanOscWaveCorr) { - needlePos-=phase*waveLen; - needlePos-=(2*waveLen-fmod(displaySize,waveLen*2))*0.5; - } - //chanOscPitch[ch]=(float)point/32.0f; needlePos-=displaySize; for (unsigned short j=0; jcorrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; - if (j>=precision/2) { - y=fft->inBuf[((j-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; - } - - waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - } - }*/ - - /*String cPhase=fmt::sprintf("\n%.1f - %.2f (%.2f %.2f)",waveLen,phase,dft.imag(),dft.real()); - dl->AddText(inRect.Min,0xffffffff,cPhase.c_str());*/ } ImU32 color=ImGui::GetColorU32(chanOscColor); if (chanOscUseGrad) { From ab7b26a2e79992ce1de0212d51a4bf90f912df43 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 4 Sep 2023 01:18:48 -0500 Subject: [PATCH 16/58] GUI: improve chan osc wave centering --- src/engine/workPool.cpp | 20 +++++++++++++ src/engine/workPool.h | 66 +++++++++++++++++++++++++++++++++++++++++ src/gui/chanOsc.cpp | 8 ++--- 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 src/engine/workPool.cpp create mode 100644 src/engine/workPool.h diff --git a/src/engine/workPool.cpp b/src/engine/workPool.cpp new file mode 100644 index 000000000..6c294a7d6 --- /dev/null +++ b/src/engine/workPool.cpp @@ -0,0 +1,20 @@ +/** + * 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 "workPool.h" \ No newline at end of file diff --git a/src/engine/workPool.h b/src/engine/workPool.h new file mode 100644 index 000000000..9abdfba0e --- /dev/null +++ b/src/engine/workPool.h @@ -0,0 +1,66 @@ +/** + * 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 _WORKPOOL_H +#define _WORKPOOL_H + +#include +#include +#include +#include + +struct DivWorkThread { + std::mutex lock; + std::thread* thread; + std::condition_variable notify; + bool busy, terminate; + + void run(); + DivWorkThread(): + busy(false) {} +}; + +/** + * this class provides an implementation of a "thread pool" for executing tasks in parallel. + * it is highly recommended to use `new` when allocating a DivWorkPool. + */ +class DivWorkPool { + DivWorkThread* workThreads; + public: + /** + * push a new job to this work pool. + * if all work threads are busy, this will block until one is free. + */ + bool push(); + + /** + * check whether this work pool is busy. + */ + bool busy(); + + /** + * wait for all work threads to finish. + */ + bool wait(); + + DivWorkPool(unsigned int threads=0); + ~DivWorkPool(); +}; + +#endif \ No newline at end of file diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 637ae7d58..4f62c0d15 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -533,9 +533,9 @@ void FurnaceGUI::drawChanOsc() { double dft[2]; dft[0]=0.0; dft[1]=0.0; - for (int j=0; jdata[(unsigned short)(needlePos-((int)waveLen)+j)&0xffff]/32768.0); - double two=(double)j*(-2.0*M_PI)/waveLen; + for (int j=needlePos-1-((displaySize+(int)waveLen)>>1), k=0; kdata[j&0xffff]/32768.0); + double two=(double)k*(-2.0*M_PI)/waveLen; dft[0]+=one*cos(two); dft[1]+=one*sin(two); } @@ -545,7 +545,7 @@ void FurnaceGUI::drawChanOsc() { if (chanOscWaveCorr) { needlePos-=phase*waveLen; - needlePos-=(2*waveLen-fmod(displaySize,waveLen*2))*0.5; + //needlePos-=(2*waveLen-fmod(displaySize,waveLen*2))*0.5; } } } From 60df7e26f44ddf28cebdfbd6472ac04c1295c55d Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 4 Sep 2023 04:14:47 -0500 Subject: [PATCH 17/58] GUI: even more chan osc improvements --- src/gui/chanOsc.cpp | 90 ++++++++++++++++++++++++++++++----------- src/gui/debugWindow.cpp | 1 + 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 4f62c0d15..6d59879b8 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -500,14 +500,20 @@ void FurnaceGUI::drawChanOsc() { fft->outBuf[1][1]=0; fftw_execute(fft->planI); + // window + for (int j=0; j<(FURNACE_FFT_SIZE>>1); j++) { + fft->corrBuf[j]*=1.0-((double)j/(double)(FURNACE_FFT_SIZE<<1)); + } + // find size of period double waveLen=FURNACE_FFT_SIZE-1; double waveLenCandL=DBL_MAX; double waveLenCandH=DBL_MIN; int waveLenBottom=0; + int waveLenTop=0; // find lowest point - for (int j=(FURNACE_FFT_SIZE>>1); j>2; j--) { + for (int j=(FURNACE_FFT_SIZE>>2); j>2; j--) { if (fft->corrBuf[j]corrBuf[j]; waveLenBottom=j; @@ -515,19 +521,20 @@ void FurnaceGUI::drawChanOsc() { } // find highest point - for (int j=(FURNACE_FFT_SIZE>>1); j>waveLenBottom; j--) { + for (int j=(FURNACE_FFT_SIZE>>1)-1; j>waveLenBottom; j--) { if (fft->corrBuf[j]>waveLenCandH) { waveLenCandH=fft->corrBuf[j]; waveLen=j; } } + waveLenTop=waveLen; // did we find the period size? if (waveLen<(FURNACE_FFT_SIZE-32)) { - waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; - // we got pitch - chanOscPitch[ch]=1.0-pow(waveLen/(double)(FURNACE_FFT_SIZE>>1),2.0); + chanOscPitch[ch]=pow(1.0-(waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); + + waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; // DFT of one period (x_1) double dft[2]; @@ -548,23 +555,63 @@ void FurnaceGUI::drawChanOsc() { //needlePos-=(2*waveLen-fmod(displaySize,waveLen*2))*0.5; } } + + // FFT debug code! + if (debugFFT) { + double maxavg=0.0; + for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { + if (fabs(fft->corrBuf[j]>maxavg)) { + maxavg=fabs(fft->corrBuf[j]); + } + } + if (maxavg>0.0000001) maxavg=0.5/maxavg; + + for (unsigned short j=0; jcorrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; + if (j>=precision/2) { + y=fft->inBuf[((j-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; + } + + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } + String cPhase=fmt::sprintf("\n%.1f (b: %d t: %d)",waveLen,waveLenBottom,waveLenTop); + dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); + + dl->AddLine( + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,0.0)), + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,1.0)), + 0xffffff00 + ); + dl->AddLine( + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,0.0)), + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,1.0)), + 0xff00ff00 + ); + } + } else { + if (debugFFT) { + dl->AddText(inRect.Min,0xffffffff,"\nquiet"); + } } - needlePos-=displaySize; - for (unsigned short j=0; jdata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; - if (minLevel>y) minLevel=y; - if (maxLeveldata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; - y-=dcOff; - if (y<-0.5f) y=-0.5f; - if (y>0.5f) y=0.5f; - y*=chanOscAmplify; - waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + if (!debugFFT || !loudEnough) { + needlePos-=displaySize; + for (unsigned short j=0; jdata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; + if (minLevel>y) minLevel=y; + if (maxLeveldata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; + y-=dcOff; + if (y<-0.5f) y=-0.5f; + if (y>0.5f) y=0.5f; + y*=chanOscAmplify; + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } } } ImU32 color=ImGui::GetColorU32(chanOscColor); @@ -693,9 +740,6 @@ void FurnaceGUI::drawChanOsc() { if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { chanOscOptions=!chanOscOptions; } - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - debugFFT=!debugFFT; - } } ImGui::PopStyleVar(); } diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index b3c07b071..916204030 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -212,6 +212,7 @@ void FurnaceGUI::drawDebug() { } if (ImGui::TreeNode("Oscilloscope Debug")) { int c=0; + ImGui::Checkbox("FFT debug view",&debugFFT); for (int i=0; isong.systemLen; i++) { DivSystem system=e->song.system[i]; if (e->getChannelCount(system)>0) { From 2ca5856800dd5107a237b184a415cdcf4a4f8fd4 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 4 Sep 2023 04:25:21 -0500 Subject: [PATCH 18/58] a fix --- src/gui/chanOsc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 6d59879b8..7f0949dd5 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -540,7 +540,7 @@ void FurnaceGUI::drawChanOsc() { double dft[2]; dft[0]=0.0; dft[1]=0.0; - for (int j=needlePos-1-((displaySize+(int)waveLen)>>1), k=0; k>1)-(int)waveLen, k=0; kdata[j&0xffff]/32768.0); double two=(double)k*(-2.0*M_PI)/waveLen; dft[0]+=one*cos(two); From ad7b4f61b5d60cdf4375c953e4df0e4550b23090 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 4 Sep 2023 15:02:19 -0500 Subject: [PATCH 19/58] YM2612: fix missing 30xx effect --- src/engine/sysDef.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 37fcbf87f..6eba35f2a 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -454,10 +454,11 @@ void DivEngine::registerSystems() { {0x18, {DIV_CMD_FM_EXTCH, "18xx: Toggle extended channel 3 mode"}}, }); - EffectHandlerMap fmOPN2EffectHandlerMap={ + EffectHandlerMap fmOPN2EffectHandlerMap(fmEffectHandlerMap); + fmOPN2EffectHandlerMap.insert({ {0x17, {DIV_CMD_SAMPLE_MODE, "17xx: Toggle PCM mode (LEGACY)"}}, {0xdf, {DIV_CMD_SAMPLE_DIR, "DFxx: Set sample playback direction (0: normal; 1: reverse)"}}, - }; + }); EffectHandlerMap fmOPLDrumsEffectHandlerMap(fmEffectHandlerMap); fmOPLDrumsEffectHandlerMap.insert({ From 55eeb241cf1817e4833b789488564be15db5ca1f Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 4 Sep 2023 18:35:18 -0500 Subject: [PATCH 20/58] this won't build --- src/gui/chanOsc.cpp | 378 +++++++++++++++++++++++--------------------- src/gui/gui.h | 10 +- 2 files changed, 202 insertions(+), 186 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 7f0949dd5..d520835b0 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -363,10 +363,11 @@ void FurnaceGUI::drawChanOsc() { std::vector oscChans; int chans=e->getTotalChannelCount(); ImGuiWindow* window=ImGui::GetCurrentWindow(); - ImVec2 waveform[512]; ImGuiStyle& style=ImGui::GetStyle(); + ImVec2 waveform[1024]; + // fill buffers for (int i=0; igetOscBuffer(i); if (buf!=NULL && e->curSubSong->chanShow[i]) { @@ -376,6 +377,192 @@ void FurnaceGUI::drawChanOsc() { } } + // process + for (size_t i=0; iready) { + logD("creating FFT plan for channel %d",ch); + fft->inBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); + fft->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)); + fft->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); + fft->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft->inBuf,fft->outBuf,FFTW_ESTIMATE); + fft->planI=fftw_plan_dft_c2r_1d(FURNACE_FFT_SIZE,fft->outBuf,fft->corrBuf,FFTW_ESTIMATE); + if (fft->plan==NULL) { + logE("failed to create plan!"); + } else if (fft->planI==NULL) { + logE("failed to create inverse plan!"); + } else if (fft->inBuf==NULL || fft->outBuf==NULL || fft->corrBuf==NULL) { + logE("failed to create FFT buffers"); + } else { + fft->ready=true; + } + } + + if (fft->ready && e->isRunning()) { + // the STRATEGY + // 1. FFT of windowed signal + // 2. inverse FFT of auto-correlation + // 3. find size of one period + // 4. DFT of the fundamental of ONE PERIOD + // 5. now we can get phase information + // + // I have a feeling this could be simplified to two FFTs or even one... + // if you know how, please tell me + + // initialization + double phase=0.0; + float minLevel=1.0f; + float maxLevel=-1.0f; + float dcOff=0.0f; + bool loudEnough=false; + + fft->needle=buf->needle; + + // first FFT + for (int j=0; jinBuf[j]=(double)buf->data[(unsigned short)(fft->needle-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; + if (fft->inBuf[j]>0.001 || fft->inBuf[j]<-0.001) loudEnough=true; + fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); + } + + // only proceed if not quiet + if (loudEnough) { + fftw_execute(fft->plan); + + // auto-correlation and second FFT + for (int j=0; joutBuf[j][0]/=FURNACE_FFT_SIZE; + fft->outBuf[j][1]/=FURNACE_FFT_SIZE; + fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; + fft->outBuf[j][1]=0; + } + fft->outBuf[0][0]=0; + fft->outBuf[0][1]=0; + fft->outBuf[1][0]=0; + fft->outBuf[1][1]=0; + fftw_execute(fft->planI); + + // window + for (int j=0; j<(FURNACE_FFT_SIZE>>1); j++) { + fft->corrBuf[j]*=1.0-((double)j/(double)(FURNACE_FFT_SIZE<<1)); + } + + // find size of period + double waveLen=FURNACE_FFT_SIZE-1; + double waveLenCandL=DBL_MAX; + double waveLenCandH=DBL_MIN; + int waveLenBottom=0; + int waveLenTop=0; + + // find lowest point + for (int j=(FURNACE_FFT_SIZE>>2); j>2; j--) { + if (fft->corrBuf[j]corrBuf[j]; + waveLenBottom=j; + } + } + + // find highest point + for (int j=(FURNACE_FFT_SIZE>>1)-1; j>waveLenBottom; j--) { + if (fft->corrBuf[j]>waveLenCandH) { + waveLenCandH=fft->corrBuf[j]; + waveLen=j; + } + } + waveLenTop=waveLen; + + // did we find the period size? + if (waveLen<(FURNACE_FFT_SIZE-32)) { + // we got pitch + chanOscPitch[ch]=pow(1.0-(waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); + + waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; + + // DFT of one period (x_1) + double dft[2]; + dft[0]=0.0; + dft[1]=0.0; + for (int j=fft->needle-1-(displaySize>>1)-(int)waveLen, k=0; kdata[j&0xffff]/32768.0); + double two=(double)k*(-2.0*M_PI)/waveLen; + dft[0]+=one*cos(two); + dft[1]+=one*sin(two); + } + + // calculate and lock into phase + phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); + + if (chanOscWaveCorr) { + fft->needle-=phase*waveLen; + } + } + + // FFT debug code! + if (debugFFT) { + double maxavg=0.0; + for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { + if (fabs(fft->corrBuf[j]>maxavg)) { + maxavg=fabs(fft->corrBuf[j]); + } + } + if (maxavg>0.0000001) maxavg=0.5/maxavg; + + for (unsigned short j=0; jcorrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; + if (j>=precision/2) { + y=fft->inBuf[((j-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; + } + + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } + String cPhase=fmt::sprintf("\n%.1f (b: %d t: %d)",waveLen,waveLenBottom,waveLenTop); + dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); + + dl->AddLine( + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,0.0)), + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,1.0)), + 0xffffff00 + ); + dl->AddLine( + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,0.0)), + ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,1.0)), + 0xff00ff00 + ); + } + } else { + if (debugFFT) { + dl->AddText(inRect.Min,0xffffffff,"\nquiet"); + } + } + + if (!debugFFT || !loudEnough) { + fft->needle-=displaySize; + for (unsigned short j=0; jdata[(unsigned short)(fft->needle+(j*displaySize/precision))]/32768.0f; + if (minLevel>y) minLevel=y; + if (maxLeveldata[(unsigned short)(fft->needle+(j*displaySize/precision))]/32768.0f; + y-=dcOff; + if (y<-0.5f) y=-0.5f; + if (y>0.5f) y=0.5f; + y*=chanOscAmplify; + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } + } + } + } + } + // 0: none // 1: sqrt(chans) // 2: sqrt(chans+1) @@ -396,6 +583,7 @@ void FurnaceGUI::drawChanOsc() { int rows=(oscBufs.size()+(chanOscCols-1))/chanOscCols; + // render for (size_t i=0; ireadNeedle=buf->needle; - } - - // check FFT status existence - if (fft->plan==NULL) { - logD("creating FFT plan for channel %d",ch); - fft->inBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); - fft->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)); - fft->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); - fft->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft->inBuf,fft->outBuf,FFTW_ESTIMATE); - fft->planI=fftw_plan_dft_c2r_1d(FURNACE_FFT_SIZE,fft->outBuf,fft->corrBuf,FFTW_ESTIMATE); - if (fft->plan==NULL) { - logE("failed to create plan!"); - } - if (fft->planI==NULL) { - logE("failed to create inverse plan!"); - } - if (fft->inBuf==NULL || fft->outBuf==NULL || fft->corrBuf==NULL) { - logE("failed to create FFT buffers"); - } - } - - int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); - ImVec2 minArea=window->DC.CursorPos; ImVec2 maxArea=ImVec2( minArea.x+size.x, @@ -448,7 +611,13 @@ void FurnaceGUI::drawChanOsc() { int precision=inRect.Max.x-inRect.Min.x; if (precision<1) precision=1; - if (precision>512) precision=512; + if (precision>1024) precision=1024; + + if (centerSettingReset) { + buf->readNeedle=buf->needle; + } + + int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); ImGui::ItemSize(size,style.FramePadding.y); if (ImGui::ItemAdd(rect,ImGui::GetID("chOscDisplay"))) { @@ -458,161 +627,6 @@ void FurnaceGUI::drawChanOsc() { waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f)); } } else { - // the STRATEGY - // 1. FFT of windowed signal - // 2. inverse FFT of auto-correlation - // 3. find size of one period - // 4. DFT of the fundamental of ONE PERIOD - // 5. now we can get phase information - // - // I have a feeling this could be simplified to two FFTs or even one... - // if you know how, please tell me - - // initialization - double phase=0.0; - float minLevel=1.0f; - float maxLevel=-1.0f; - float dcOff=0.0f; - unsigned short needlePos=buf->needle; - bool loudEnough=false; - - // first FFT - for (int j=0; jinBuf[j]=(double)buf->data[(unsigned short)(needlePos-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; - if (fft->inBuf[j]>0.001 || fft->inBuf[j]<-0.001) loudEnough=true; - fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); - } - - // only proceed if not quiet - if (loudEnough) { - fftw_execute(fft->plan); - - // auto-correlation and second FFT - for (int j=0; joutBuf[j][0]/=FURNACE_FFT_SIZE; - fft->outBuf[j][1]/=FURNACE_FFT_SIZE; - fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; - fft->outBuf[j][1]=0; - } - fft->outBuf[0][0]=0; - fft->outBuf[0][1]=0; - fft->outBuf[1][0]=0; - fft->outBuf[1][1]=0; - fftw_execute(fft->planI); - - // window - for (int j=0; j<(FURNACE_FFT_SIZE>>1); j++) { - fft->corrBuf[j]*=1.0-((double)j/(double)(FURNACE_FFT_SIZE<<1)); - } - - // find size of period - double waveLen=FURNACE_FFT_SIZE-1; - double waveLenCandL=DBL_MAX; - double waveLenCandH=DBL_MIN; - int waveLenBottom=0; - int waveLenTop=0; - - // find lowest point - for (int j=(FURNACE_FFT_SIZE>>2); j>2; j--) { - if (fft->corrBuf[j]corrBuf[j]; - waveLenBottom=j; - } - } - - // find highest point - for (int j=(FURNACE_FFT_SIZE>>1)-1; j>waveLenBottom; j--) { - if (fft->corrBuf[j]>waveLenCandH) { - waveLenCandH=fft->corrBuf[j]; - waveLen=j; - } - } - waveLenTop=waveLen; - - // did we find the period size? - if (waveLen<(FURNACE_FFT_SIZE-32)) { - // we got pitch - chanOscPitch[ch]=pow(1.0-(waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); - - waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; - - // DFT of one period (x_1) - double dft[2]; - dft[0]=0.0; - dft[1]=0.0; - for (int j=needlePos-1-(displaySize>>1)-(int)waveLen, k=0; kdata[j&0xffff]/32768.0); - double two=(double)k*(-2.0*M_PI)/waveLen; - dft[0]+=one*cos(two); - dft[1]+=one*sin(two); - } - - // calculate and lock into phase - phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); - - if (chanOscWaveCorr) { - needlePos-=phase*waveLen; - //needlePos-=(2*waveLen-fmod(displaySize,waveLen*2))*0.5; - } - } - - // FFT debug code! - if (debugFFT) { - double maxavg=0.0; - for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { - if (fabs(fft->corrBuf[j]>maxavg)) { - maxavg=fabs(fft->corrBuf[j]); - } - } - if (maxavg>0.0000001) maxavg=0.5/maxavg; - - for (unsigned short j=0; jcorrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; - if (j>=precision/2) { - y=fft->inBuf[((j-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; - } - - waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - } - String cPhase=fmt::sprintf("\n%.1f (b: %d t: %d)",waveLen,waveLenBottom,waveLenTop); - dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); - - dl->AddLine( - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,0.0)), - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,1.0)), - 0xffffff00 - ); - dl->AddLine( - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,0.0)), - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,1.0)), - 0xff00ff00 - ); - } - } else { - if (debugFFT) { - dl->AddText(inRect.Min,0xffffffff,"\nquiet"); - } - } - - if (!debugFFT || !loudEnough) { - needlePos-=displaySize; - for (unsigned short j=0; jdata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; - if (minLevel>y) minLevel=y; - if (maxLeveldata[(unsigned short)(needlePos+(j*displaySize/precision))]/32768.0f; - y-=dcOff; - if (y<-0.5f) y=-0.5f; - if (y>0.5f) y=0.5f; - y*=chanOscAmplify; - waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - } - } } ImU32 color=ImGui::GetColorU32(chanOscColor); if (chanOscUseGrad) { diff --git a/src/gui/gui.h b/src/gui/gui.h index b28918fda..37fcd4a36 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2056,20 +2056,22 @@ class FurnaceGUI { unsigned short lastCorrPos[DIV_MAX_CHANS]; struct ChanOscStatus { double* inBuf; + fftw_complex* outBuf; + double* corrBuf; size_t inBufPos; double inBufPosFrac; unsigned short needle; - fftw_complex* outBuf; - double* corrBuf; + bool ready; fftw_plan plan; fftw_plan planI; ChanOscStatus(): inBuf(NULL), + outBuf(NULL), + corrBuf(NULL), inBufPos(0), inBufPosFrac(0.0f), needle(0), - outBuf(NULL), - corrBuf(NULL), + ready(false), plan(NULL), planI(NULL) {} } chanOscChan[DIV_MAX_CHANS]; From c99899a0026a049336a251503b3446747b4e10bd Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 4 Sep 2023 18:54:33 -0500 Subject: [PATCH 21/58] GUI: re-organize chan osc code prepare for possible multi-threading --- src/gui/chanOsc.cpp | 168 +++++++++++++++++++++++--------------------- src/gui/gui.h | 8 ++- 2 files changed, 93 insertions(+), 83 deletions(-) diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index d520835b0..31b0cc0b4 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -384,6 +384,13 @@ void FurnaceGUI::drawChanOsc() { int ch=oscChans[i]; if (buf!=NULL) { + // prepare + if (centerSettingReset) { + buf->readNeedle=buf->needle; + } + + int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); + // check FFT status existence if (!fft->ready) { logD("creating FFT plan for channel %d",ch); @@ -416,22 +423,18 @@ void FurnaceGUI::drawChanOsc() { // initialization double phase=0.0; - float minLevel=1.0f; - float maxLevel=-1.0f; - float dcOff=0.0f; - bool loudEnough=false; - + fft->loudEnough=false; fft->needle=buf->needle; // first FFT for (int j=0; jinBuf[j]=(double)buf->data[(unsigned short)(fft->needle-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; - if (fft->inBuf[j]>0.001 || fft->inBuf[j]<-0.001) loudEnough=true; + if (fft->inBuf[j]>0.001 || fft->inBuf[j]<-0.001) fft->loudEnough=true; fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); } // only proceed if not quiet - if (loudEnough) { + if (fft->loudEnough) { fftw_execute(fft->plan); // auto-correlation and second FFT @@ -453,43 +456,43 @@ void FurnaceGUI::drawChanOsc() { } // find size of period - double waveLen=FURNACE_FFT_SIZE-1; double waveLenCandL=DBL_MAX; double waveLenCandH=DBL_MIN; - int waveLenBottom=0; - int waveLenTop=0; + fft->waveLen=FURNACE_FFT_SIZE-1; + fft->waveLenBottom=0; + fft->waveLenTop=0; // find lowest point for (int j=(FURNACE_FFT_SIZE>>2); j>2; j--) { if (fft->corrBuf[j]corrBuf[j]; - waveLenBottom=j; + fft->waveLenBottom=j; } } // find highest point - for (int j=(FURNACE_FFT_SIZE>>1)-1; j>waveLenBottom; j--) { + for (int j=(FURNACE_FFT_SIZE>>1)-1; j>fft->waveLenBottom; j--) { if (fft->corrBuf[j]>waveLenCandH) { waveLenCandH=fft->corrBuf[j]; - waveLen=j; + fft->waveLen=j; } } - waveLenTop=waveLen; + fft->waveLenTop=fft->waveLen; // did we find the period size? - if (waveLen<(FURNACE_FFT_SIZE-32)) { + if (fft->waveLen<(FURNACE_FFT_SIZE-32)) { // we got pitch - chanOscPitch[ch]=pow(1.0-(waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); + chanOscPitch[ch]=pow(1.0-(fft->waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); - waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; + fft->waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; // DFT of one period (x_1) double dft[2]; dft[0]=0.0; dft[1]=0.0; - for (int j=fft->needle-1-(displaySize>>1)-(int)waveLen, k=0; kneedle-1-(displaySize>>1)-(int)fft->waveLen, k=0; kwaveLen; j++, k++) { double one=((double)buf->data[j&0xffff]/32768.0); - double two=(double)k*(-2.0*M_PI)/waveLen; + double two=(double)k*(-2.0*M_PI)/fft->waveLen; dft[0]+=one*cos(two); dft[1]+=one*sin(two); } @@ -498,67 +501,12 @@ void FurnaceGUI::drawChanOsc() { phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); if (chanOscWaveCorr) { - fft->needle-=phase*waveLen; + fft->needle-=phase*fft->waveLen; } } - - // FFT debug code! - if (debugFFT) { - double maxavg=0.0; - for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { - if (fabs(fft->corrBuf[j]>maxavg)) { - maxavg=fabs(fft->corrBuf[j]); - } - } - if (maxavg>0.0000001) maxavg=0.5/maxavg; - - for (unsigned short j=0; jcorrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; - if (j>=precision/2) { - y=fft->inBuf[((j-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; - } - - waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - } - String cPhase=fmt::sprintf("\n%.1f (b: %d t: %d)",waveLen,waveLenBottom,waveLenTop); - dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); - - dl->AddLine( - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,0.0)), - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenBottom/(double)FURNACE_FFT_SIZE,1.0)), - 0xffffff00 - ); - dl->AddLine( - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,0.0)), - ImLerp(inRect.Min,inRect.Max,ImVec2((double)waveLenTop/(double)FURNACE_FFT_SIZE,1.0)), - 0xff00ff00 - ); - } - } else { - if (debugFFT) { - dl->AddText(inRect.Min,0xffffffff,"\nquiet"); - } } - if (!debugFFT || !loudEnough) { - fft->needle-=displaySize; - for (unsigned short j=0; jdata[(unsigned short)(fft->needle+(j*displaySize/precision))]/32768.0f; - if (minLevel>y) minLevel=y; - if (maxLeveldata[(unsigned short)(fft->needle+(j*displaySize/precision))]/32768.0f; - y-=dcOff; - if (y<-0.5f) y=-0.5f; - if (y>0.5f) y=0.5f; - y*=chanOscAmplify; - waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); - } - } + fft->needle-=displaySize; } } } @@ -613,12 +561,6 @@ void FurnaceGUI::drawChanOsc() { if (precision<1) precision=1; if (precision>1024) precision=1024; - if (centerSettingReset) { - buf->readNeedle=buf->needle; - } - - int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); - ImGui::ItemSize(size,style.FramePadding.y); if (ImGui::ItemAdd(rect,ImGui::GetID("chOscDisplay"))) { if (!e->isRunning()) { @@ -627,6 +569,68 @@ void FurnaceGUI::drawChanOsc() { waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f)); } } else { + int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); + + float minLevel=1.0f; + float maxLevel=-1.0f; + float dcOff=0.0f; + + if (debugFFT) { + // FFT debug code! + double maxavg=0.0; + for (unsigned short j=0; j<(FURNACE_FFT_SIZE>>1); j++) { + if (fabs(fft->corrBuf[j]>maxavg)) { + maxavg=fabs(fft->corrBuf[j]); + } + } + if (maxavg>0.0000001) maxavg=0.5/maxavg; + + for (unsigned short j=0; j=precision/2) { + y=fft->inBuf[((j-(precision/2))*FURNACE_FFT_SIZE*2)/(precision)]; + } else { + y=fft->corrBuf[(j*FURNACE_FFT_SIZE)/precision]*maxavg; + } + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } + if (fft->loudEnough) { + String cPhase=fmt::sprintf("\n%.1f (b: %d t: %d)",fft->waveLen,fft->waveLenBottom,fft->waveLenTop); + dl->AddText(inRect.Min,0xffffffff,cPhase.c_str()); + + dl->AddLine( + ImLerp(inRect.Min,inRect.Max,ImVec2((double)fft->waveLenBottom/(double)FURNACE_FFT_SIZE,0.0)), + ImLerp(inRect.Min,inRect.Max,ImVec2((double)fft->waveLenBottom/(double)FURNACE_FFT_SIZE,1.0)), + 0xffffff00 + ); + dl->AddLine( + ImLerp(inRect.Min,inRect.Max,ImVec2((double)fft->waveLenTop/(double)FURNACE_FFT_SIZE,0.0)), + ImLerp(inRect.Min,inRect.Max,ImVec2((double)fft->waveLenTop/(double)FURNACE_FFT_SIZE,1.0)), + 0xff00ff00 + ); + } else { + if (debugFFT) { + dl->AddText(inRect.Min,0xffffffff,"\nquiet"); + } + } + } else { + for (unsigned short j=0; jdata[(unsigned short)(fft->needle+(j*displaySize/precision))]/32768.0f; + if (minLevel>y) minLevel=y; + if (maxLeveldata[(unsigned short)(fft->needle+(j*displaySize/precision))]/32768.0f; + y-=dcOff; + if (y<-0.5f) y=-0.5f; + if (y>0.5f) y=0.5f; + y*=chanOscAmplify; + waveform[j]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f-y)); + } + } } ImU32 color=ImGui::GetColorU32(chanOscColor); if (chanOscUseGrad) { diff --git a/src/gui/gui.h b/src/gui/gui.h index 37fcd4a36..afba9099c 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2060,8 +2060,10 @@ class FurnaceGUI { double* corrBuf; size_t inBufPos; double inBufPosFrac; + double waveLen; + int waveLenBottom, waveLenTop; unsigned short needle; - bool ready; + bool ready, loudEnough; fftw_plan plan; fftw_plan planI; ChanOscStatus(): @@ -2070,8 +2072,12 @@ class FurnaceGUI { corrBuf(NULL), inBufPos(0), inBufPosFrac(0.0f), + waveLen(0.0), + waveLenBottom(0), + waveLenTop(0), needle(0), ready(false), + loudEnough(false), plan(NULL), planI(NULL) {} } chanOscChan[DIV_MAX_CHANS]; From c3ab0652b2e41bd38b25f32911407059edc9dfb9 Mon Sep 17 00:00:00 2001 From: Electric Keet Date: Mon, 4 Sep 2023 20:53:22 -0700 Subject: [PATCH 22/58] Brand new glossary page. --- doc/1-intro/README.md | 2 +- doc/1-intro/glossary.md | 132 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 doc/1-intro/glossary.md diff --git a/doc/1-intro/README.md b/doc/1-intro/README.md index e5430504b..f864f8f9d 100644 --- a/doc/1-intro/README.md +++ b/doc/1-intro/README.md @@ -12,7 +12,7 @@ Furnace uses hexadecimal (abbreviated as "hex") numbers frequently. see [this gu ## interface -Furnace uses a music tracker interface. think of a table with music notes written on it. then that table scrolls up and plays the notes. even experienced tracker musicians might benefit from a quick review of [tracker concepts and terms](concepts.md) before using Furnace. +Furnace uses a music tracker interface. think of a table with music notes written on it. then that table scrolls up and plays the notes. even experienced tracker musicians might benefit from a quick review of [tracker concepts and terms](concepts.md) before using Furnace. there's also a [glossary of common terms](glossary.md). due to its nature of being feature-packed, it may be technical and somewhat difficult to get around. therefore we added a basic mode, which hides several advanced features. diff --git a/doc/1-intro/glossary.md b/doc/1-intro/glossary.md new file mode 100644 index 000000000..918eb8a4b --- /dev/null +++ b/doc/1-intro/glossary.md @@ -0,0 +1,132 @@ +# glossary of common terms + +**2-op**, **3-op**, **4-op**...: the number of FM operators used to generate a sound. more operators allow for more complex sounds. + +**ADPCM**: adaptive differential pulse code modulation. this is a variety of DPCM with a more complex method of storing the amplitude differences. + +**ADSR**: attack, decay, sustain, release. these are the four necessary values for a basic volume envelope. + +**algorithm**: the way in which the operators in an FM instrument interact. +- when two operators connect to the same point, their sounds are added together. +- when two operators are connected left to right, the left is the modulator and the right is the carrier sound that is modified. + +**bitbang**: to achieve PCM sound by sending a rapid stream of volume commands to a non-PCM channel. + +**BRR**: a type of lossy ADPCM used by the SNES. it has a fixed compression ratio; groups of 32 bytes (16 samples) are encoded in 9 bytes each. +- usually stored in .BRR files. + +**clipping**: when a sample or playback stream exceeds the maximum or minimum values. this can cause audible distortion. +- this often occurs when a sample is amplified too much. +- it can also occur during playback if too much sound is being added together at once. in some cases the mixer can be used to reduce the volume. if this doesn't work, the clipping is caused within the chip's own mixing, and the only solution is to reduce the volumes of the notes being played. + +**clock rate**: the timing at which a chip operates, expressed as cycles per second (Hz). +- changing this may change aspects of how some chips work, most notably pitch. +- some chips cannot operate at anything other than their designed clock rate. + +**cursor**: the marker of input focus. anything typed will happen at the cursor's location. +- _Furnace:_ this always refers to the pattern view except when in a text entry box. + +**DAC**: digital analog converter. this converts a digital representation of sound into actual output. + +**.DFI**: a DefleMask instrument file. + +**.DFM**: a DefleMask song file. +- _Furnace:_ DFM files may be read, and compatibility flags will be set to make them play as accurately as possible, but there may still be glitches. +- _Furnace:_ DFM files may be saved, but full compatibility isn't guaranteed and many features will be missing. this isn't recommended unless absolutely necessary. + +**.DFW**: a DefleMask wavetable file. + +**DPCM**: differential pulse code modulation. this is a variety of PCM that stores each amplitude as its difference from the previous. + +**duty**: in a pulse wave, this represents the ratio of the "on" part of the wave. +- a square wave is a pulse wave with a duty of 50%. + +**feedback**: in FM instruments, this adds some of an operator's output into itself to create complex harmonics. +- in the algorithm view, an operator with a circle around it is capable of feedback. + +**FM**: frequency modulation. this is a method of generating sound that uses one operator's amplitude to modify another operator's frequency. +- the FM in Yamaha chips is more accurately called _phase modulation,_ which uses a different method of computation to achieve similar results. + +**.FUI**: a Furnace instrument file. + +**.FUR**: a Furnace tracker file. + +**.FUW**: a Furnace wavetable file. + +**hard-pan**: sounds can only be panned all the way to one side or the other, not in-between. + +**Hz**: hertz (pronounced as "hurts"). a unit representing divisions of one second. 1 Hz means once per second; 100 Hz means one hundred times per second. also, _kHz_ (kilohertz, one thousand per second) and _MHz_ (megahertz, one million per second). + +**interpolate** (pattern): to fill in the area between two values with a smooth ramp of values in between. + +**interpolation** (sample): filtering of sample output to remove unintended harmonics and achieve a smoother sound. +- analogous to image antialiasing. +- some sample-based chips can perform interpolation, but most cannot. + +**ladder effect**: an inaccurate yet common term for the DAC distortion that affects the YM2612 chip. + +**LFO**: low frequency oscillator. this is slow, often subsonic oscillator used to alter other sounds. + +**macro**: a sequence of values automatically applied while a note plays. + +**normalize**: to adjust the volume of a sample so it is as loud as possible without adding distortion from clipping. + +**operator**: in FM, a single oscillator that interacts with other oscillators to generate sound. + +**oscillator**: a sine wave or other basic waveform used as sound or to alter sound. + +**PCM**: pulse code modulation. a stream of data that represents sound as a rapid sequence of amplitudes. +- CD tracks and WAV files are PCM. +- FLAC, OGG, and MP3 are compressed PCM. these must be uncompressed outside Furnace before they can be used as samples. + +**period**: the length of a repeating waveform. as frequency rises, the period shortens. + +**phase reset**: to restart a waveform at its initial value. +- for FM instruments, this restarts the volume envelope also. + +**PSG**: programmable sound generator. this refers to chips that produce only simple waveforms and noise. + +**pulse wave**: a waveform with a period consisting of only two amplitudes, high and low. also known as a rectangular wave. +- the ratio of the durations of the high and low parts is known as the duty of the wave. +- a square wave is a pulse wave for which the ratio of high and low are exactly equal, having a duty of 50%. + +**release**: the part of a note that plays after it's no longer held, or the part of a macro the plays after it stops looping. usually applies at key off. + +**resample**: to convert a sample to a different playback rate. +- this is a "lossy" process; it usually loses some amount of audio quality. the results can't be converted back into the original rate without further loss of quality. +- resampling to a lower rate reduces the amount of memory required, but strips away higher frequencies in the sound. +- resampling to a higher rate cannot recover missing frequencies and may add unwanted harmonics along with greater memory requirements. + +**raw**: a sample or wavetable file without a header. when loading such a file, the format must be set properly or it will be a mess. + +**register**: a memory location within a sound chip. "register view" shows all the relevant memory of all chips in use. + +**sample**: a digitally recorded sound. usually stored as some variant of PCM. +- these can take up a lot of room depending on length and sample rate, thus older systems tend to use short, lower quality samples. + +**signed**: a digital representation of a number that may be negative or positive. +- if an imported raw sample sounds recognizable but heavily distorted, it's likely to be unsigned interpreted as signed or vice-versa. + +**software mixing**: mixing multiple channels of sound down to a single stream to be sent to a PCM channel. +- this puts a heavy load on the sound chip or the host system, so it was rarely used in games. +- _Furnace:_ this is used for DualPCM and QuadTone. + +**square wave**: a wave consisting of only two values, high and low, with equal durations within the wave's period. +- this is equivalent to a pulse wave with a duty of 50%. + +**supersaw**: a sound made up of multiple saw waves at slightly different frequencies to achieve a flanged effect. + +**tick rate**: the rate at which the software controlling a sound chip sends commands to it. +- this usually corresponds to the frame rate the system uses for video, approximately 60 for NTSC and 50 for PAL. + +**unsigned**: a digital representation of a number that can only be positive. +- if an imported raw sample sounds recognizable but heavily distorted, it's likely to be signed interpreted as unsigned or vice-versa. + +**.VGM**: a file containing the log of data sent to a soundchip during sound playback. +- saving to a .VGM file may be compared to "converting text to outlines" or similar irreversible processes. the results cannot be loaded back into the tracker. +- different versions of the VGM format have different capabilities, with trade-offs. older versions may lack chips or features; newer versions may not be compatible with some software. +- samples are stored uncompressed. PCM streams (such as DualPCM) can quickly take up a huge amount of space. + +**wavetable**: a very short looping sample. + +**ZSM**: a VGM-like file meant specifically for the Commander X16 computer. From e68c0cbd759edc41cb1f9ea8b132506b2b01ea76 Mon Sep 17 00:00:00 2001 From: Electric Keet Date: Mon, 4 Sep 2023 22:11:20 -0700 Subject: [PATCH 23/58] Corrections. Also, pulling "supersaw" until I've gotten something like consensus on the definition. Amusing, since it's what got me started on this glossary.... --- doc/1-intro/glossary.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/1-intro/glossary.md b/doc/1-intro/glossary.md index 918eb8a4b..d8d0e9acf 100644 --- a/doc/1-intro/glossary.md +++ b/doc/1-intro/glossary.md @@ -28,15 +28,15 @@ **DAC**: digital analog converter. this converts a digital representation of sound into actual output. -**.DFI**: a DefleMask instrument file. - -**.DFM**: a DefleMask song file. +**.DMF**: a DefleMask song file. - _Furnace:_ DFM files may be read, and compatibility flags will be set to make them play as accurately as possible, but there may still be glitches. - _Furnace:_ DFM files may be saved, but full compatibility isn't guaranteed and many features will be missing. this isn't recommended unless absolutely necessary. -**.DFW**: a DefleMask wavetable file. +**.DMP**: a DefleMask instrument file. -**DPCM**: differential pulse code modulation. this is a variety of PCM that stores each amplitude as its difference from the previous. +**.DMW**: a DefleMask wavetable file. + +**DPCM**: differential/delta pulse code modulation. this is a variety of PCM that stores each amplitude as its difference from the previous. **duty**: in a pulse wave, this represents the ratio of the "on" part of the wave. - a square wave is a pulse wave with a duty of 50%. @@ -63,9 +63,9 @@ - analogous to image antialiasing. - some sample-based chips can perform interpolation, but most cannot. -**ladder effect**: an inaccurate yet common term for the DAC distortion that affects the YM2612 chip. +**ladder effect**: an inaccurate yet common term for the DAC distortion that affects some Yamaha FM chips. -**LFO**: low frequency oscillator. this is slow, often subsonic oscillator used to alter other sounds. +**LFO**: low frequency oscillator. this is a slow, often subsonic oscillator used to alter other sounds. **macro**: a sequence of values automatically applied while a note plays. @@ -76,8 +76,8 @@ **oscillator**: a sine wave or other basic waveform used as sound or to alter sound. **PCM**: pulse code modulation. a stream of data that represents sound as a rapid sequence of amplitudes. -- CD tracks and WAV files are PCM. -- FLAC, OGG, and MP3 are compressed PCM. these must be uncompressed outside Furnace before they can be used as samples. +- CD tracks and .WAV files are PCM. +- .OGG and .MP3 are compressed differently from PCM. they must be converted to .WAV outside Furnace before they can be used as samples. **period**: the length of a repeating waveform. as frequency rises, the period shortens. @@ -108,7 +108,7 @@ - if an imported raw sample sounds recognizable but heavily distorted, it's likely to be unsigned interpreted as signed or vice-versa. **software mixing**: mixing multiple channels of sound down to a single stream to be sent to a PCM channel. -- this puts a heavy load on the sound chip or the host system, so it was rarely used in games. +- this puts a heavy load on the CPU of the host system, so it was rarely used in games. - _Furnace:_ this is used for DualPCM and QuadTone. **square wave**: a wave consisting of only two values, high and low, with equal durations within the wave's period. From 1da000b00cff25df6a09afe7b3d708aab695a1be Mon Sep 17 00:00:00 2001 From: tildearrow Date: Tue, 5 Sep 2023 04:38:57 -0500 Subject: [PATCH 24/58] GUI: per-chan osc multi-threading! --- CMakeLists.txt | 1 + src/engine/workPool.cpp | 134 ++++++++++++++++++++++++- src/engine/workPool.h | 43 +++++++- src/gui/chanOsc.cpp | 216 ++++++++++++++++++++++------------------ src/gui/gui.cpp | 8 ++ src/gui/gui.h | 10 +- src/gui/settings.cpp | 30 ++++++ 7 files changed, 337 insertions(+), 105 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d9234f48..8aa144dc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -550,6 +550,7 @@ src/engine/blip_buf.c src/engine/brrUtils.c src/engine/safeReader.cpp src/engine/safeWriter.cpp +src/engine/workPool.cpp src/engine/cmdStream.cpp src/engine/cmdStreamOps.cpp src/engine/config.cpp diff --git a/src/engine/workPool.cpp b/src/engine/workPool.cpp index 6c294a7d6..d7c1f6b22 100644 --- a/src/engine/workPool.cpp +++ b/src/engine/workPool.cpp @@ -17,4 +17,136 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ - #include "workPool.h" \ No newline at end of file +#include "workPool.h" +#include "../ta-log.h" +#include + +void* _workThread(void* inst) { + ((DivWorkThread*)inst)->run(); + return NULL; +} + +void DivWorkThread::run() { + std::unique_lock unique(selfLock); + DivPendingTask task; + + logV("running work thread"); + + while (true) { + lock.lock(); + if (tasks.empty()) { + lock.unlock(); + isBusy=false; + parent->notify.notify_one(); + if (terminate) { + break; + } + notify.wait(unique); + continue; + } else { + task=tasks.front(); + tasks.pop(); + lock.unlock(); + + task.func(task.funcArg); + + parent->busyCount--; + parent->notify.notify_one(); + } + } +} + +bool DivWorkThread::assign(const std::function& what, void* arg) { + lock.lock(); + if (tasks.size()>=30) { + lock.unlock(); + return false; + } + tasks.push(DivPendingTask(what,arg)); + parent->busyCount++; + parent->notify.notify_one(); + isBusy=true; + lock.unlock(); + notify.notify_one(); + return true; +} + +void DivWorkThread::wait() { + if (!isBusy) return; +} + +bool DivWorkThread::busy() { + return isBusy; +} + +void DivWorkThread::finish() { + lock.lock(); + terminate=true; + lock.unlock(); + notify.notify_one(); + thread->join(); +} + +void DivWorkThread::init(DivWorkPool* p) { + parent=p; + thread=new std::thread(_workThread,this); +} + +void DivWorkPool::push(const std::function& what, void* arg) { + //logV("submitting work"); + // if no work threads, just execute + if (!threaded) { + what(arg); + return; + } + + if (pos>=count) pos=0; + + for (unsigned int tryCount=0; tryCount unique(selfLock); + while (busyCount!=0) { + notify.wait_for(unique,std::chrono::milliseconds(100)); + } +} + +DivWorkPool::DivWorkPool(unsigned int threads): + threaded(threads>0), + count(threads), + pos(0), + busyCount(0) { + if (threaded) { + workThreads=new DivWorkThread[threads]; + for (unsigned int i=0; i #include +#include #include #include +#include "fixedQueue.h" + +class DivWorkPool; + +struct DivPendingTask { + std::function func; + void* funcArg; + DivPendingTask(std::function f, void* arg): + func(f), + funcArg(arg) {} + DivPendingTask(): + func(NULL), + funcArg(NULL) {} +}; + struct DivWorkThread { + DivWorkPool* parent; std::mutex lock; + std::mutex selfLock; std::thread* thread; std::condition_variable notify; - bool busy, terminate; + FixedQueue tasks; + std::atomic isBusy; + bool terminate; void run(); + bool assign(const std::function& what, void* arg); + void wait(); + bool busy(); + void finish(); + + void init(DivWorkPool* p); DivWorkThread(): - busy(false) {} + parent(NULL), + isBusy(false), + terminate(false) {} }; /** @@ -41,13 +69,20 @@ struct DivWorkThread { * it is highly recommended to use `new` when allocating a DivWorkPool. */ class DivWorkPool { + bool threaded; + std::mutex selfLock; + unsigned int count; + unsigned int pos; DivWorkThread* workThreads; public: + std::condition_variable notify; + std::atomic busyCount; + /** * push a new job to this work pool. * if all work threads are busy, this will block until one is free. */ - bool push(); + void push(const std::function& what, void* arg); /** * check whether this work pool is busy. @@ -57,7 +92,7 @@ class DivWorkPool { /** * wait for all work threads to finish. */ - bool wait(); + void wait(); DivWorkPool(unsigned int threads=0); ~DivWorkPool(); diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 31b0cc0b4..b01a165be 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -367,6 +367,12 @@ void FurnaceGUI::drawChanOsc() { ImGuiStyle& style=ImGui::GetStyle(); ImVec2 waveform[1024]; + // check work thread + if (chanOscWorkPool==NULL) { + logV("creating chan osc work pool"); + chanOscWorkPool=new DivWorkPool(settings.chanOscThreads); + } + // fill buffers for (int i=0; igetOscBuffer(i); @@ -379,137 +385,144 @@ void FurnaceGUI::drawChanOsc() { // process for (size_t i=0; irelatedBuf=oscBufs[i]; + fft_->relatedCh=oscChans[i]; + + if (fft_->relatedBuf!=NULL) { // prepare if (centerSettingReset) { - buf->readNeedle=buf->needle; + fft_->relatedBuf->readNeedle=fft_->relatedBuf->needle; } - int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); - // check FFT status existence - if (!fft->ready) { - logD("creating FFT plan for channel %d",ch); - fft->inBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); - fft->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)); - fft->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); - fft->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft->inBuf,fft->outBuf,FFTW_ESTIMATE); - fft->planI=fftw_plan_dft_c2r_1d(FURNACE_FFT_SIZE,fft->outBuf,fft->corrBuf,FFTW_ESTIMATE); - if (fft->plan==NULL) { + if (!fft_->ready) { + logD("creating FFT plan for channel %d",fft_->relatedCh); + fft_->inBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); + fft_->outBuf=(fftw_complex*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(fftw_complex)); + fft_->corrBuf=(double*)fftw_malloc(FURNACE_FFT_SIZE*sizeof(double)); + fft_->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft_->inBuf,fft_->outBuf,FFTW_ESTIMATE); + fft_->planI=fftw_plan_dft_c2r_1d(FURNACE_FFT_SIZE,fft_->outBuf,fft_->corrBuf,FFTW_ESTIMATE); + if (fft_->plan==NULL) { logE("failed to create plan!"); - } else if (fft->planI==NULL) { + } else if (fft_->planI==NULL) { logE("failed to create inverse plan!"); - } else if (fft->inBuf==NULL || fft->outBuf==NULL || fft->corrBuf==NULL) { + } else if (fft_->inBuf==NULL || fft_->outBuf==NULL || fft_->corrBuf==NULL) { logE("failed to create FFT buffers"); } else { - fft->ready=true; + fft_->ready=true; } } - if (fft->ready && e->isRunning()) { - // the STRATEGY - // 1. FFT of windowed signal - // 2. inverse FFT of auto-correlation - // 3. find size of one period - // 4. DFT of the fundamental of ONE PERIOD - // 5. now we can get phase information - // - // I have a feeling this could be simplified to two FFTs or even one... - // if you know how, please tell me + if (fft_->ready && e->isRunning()) { + chanOscWorkPool->push([this](void* fft_v) { + ChanOscStatus* fft=(ChanOscStatus*)fft_v; + DivDispatchOscBuffer* buf=fft->relatedBuf; + int ch=fft->relatedCh; - // initialization - double phase=0.0; - fft->loudEnough=false; - fft->needle=buf->needle; + // the STRATEGY + // 1. FFT of windowed signal + // 2. inverse FFT of auto-correlation + // 3. find size of one period + // 4. DFT of the fundamental of ONE PERIOD + // 5. now we can get phase information + // + // I have a feeling this could be simplified to two FFTs or even one... + // if you know how, please tell me - // first FFT - for (int j=0; jinBuf[j]=(double)buf->data[(unsigned short)(fft->needle-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; - if (fft->inBuf[j]>0.001 || fft->inBuf[j]<-0.001) fft->loudEnough=true; - fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); - } + // initialization + double phase=0.0; + int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); + fft->loudEnough=false; + fft->needle=buf->needle; - // only proceed if not quiet - if (fft->loudEnough) { - fftw_execute(fft->plan); - - // auto-correlation and second FFT + // first FFT for (int j=0; joutBuf[j][0]/=FURNACE_FFT_SIZE; - fft->outBuf[j][1]/=FURNACE_FFT_SIZE; - fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; - fft->outBuf[j][1]=0; - } - fft->outBuf[0][0]=0; - fft->outBuf[0][1]=0; - fft->outBuf[1][0]=0; - fft->outBuf[1][1]=0; - fftw_execute(fft->planI); - - // window - for (int j=0; j<(FURNACE_FFT_SIZE>>1); j++) { - fft->corrBuf[j]*=1.0-((double)j/(double)(FURNACE_FFT_SIZE<<1)); + fft->inBuf[j]=(double)buf->data[(unsigned short)(fft->needle-displaySize*2+((j*displaySize*2)/(FURNACE_FFT_SIZE)))]/32768.0; + if (fft->inBuf[j]>0.001 || fft->inBuf[j]<-0.001) fft->loudEnough=true; + fft->inBuf[j]*=0.55-0.45*cos(M_PI*(double)j/(double)(FURNACE_FFT_SIZE>>1)); } - // find size of period - double waveLenCandL=DBL_MAX; - double waveLenCandH=DBL_MIN; - fft->waveLen=FURNACE_FFT_SIZE-1; - fft->waveLenBottom=0; - fft->waveLenTop=0; + // only proceed if not quiet + if (fft->loudEnough) { + fftw_execute(fft->plan); - // find lowest point - for (int j=(FURNACE_FFT_SIZE>>2); j>2; j--) { - if (fft->corrBuf[j]corrBuf[j]; - fft->waveLenBottom=j; + // auto-correlation and second FFT + for (int j=0; joutBuf[j][0]/=FURNACE_FFT_SIZE; + fft->outBuf[j][1]/=FURNACE_FFT_SIZE; + fft->outBuf[j][0]=fft->outBuf[j][0]*fft->outBuf[j][0]+fft->outBuf[j][1]*fft->outBuf[j][1]; + fft->outBuf[j][1]=0; } - } - - // find highest point - for (int j=(FURNACE_FFT_SIZE>>1)-1; j>fft->waveLenBottom; j--) { - if (fft->corrBuf[j]>waveLenCandH) { - waveLenCandH=fft->corrBuf[j]; - fft->waveLen=j; - } - } - fft->waveLenTop=fft->waveLen; + fft->outBuf[0][0]=0; + fft->outBuf[0][1]=0; + fft->outBuf[1][0]=0; + fft->outBuf[1][1]=0; + fftw_execute(fft->planI); - // did we find the period size? - if (fft->waveLen<(FURNACE_FFT_SIZE-32)) { - // we got pitch - chanOscPitch[ch]=pow(1.0-(fft->waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); + // window + for (int j=0; j<(FURNACE_FFT_SIZE>>1); j++) { + fft->corrBuf[j]*=1.0-((double)j/(double)(FURNACE_FFT_SIZE<<1)); + } + + // find size of period + double waveLenCandL=DBL_MAX; + double waveLenCandH=DBL_MIN; + fft->waveLen=FURNACE_FFT_SIZE-1; + fft->waveLenBottom=0; + fft->waveLenTop=0; + + // find lowest point + for (int j=(FURNACE_FFT_SIZE>>2); j>2; j--) { + if (fft->corrBuf[j]corrBuf[j]; + fft->waveLenBottom=j; + } + } - fft->waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; - - // DFT of one period (x_1) - double dft[2]; - dft[0]=0.0; - dft[1]=0.0; - for (int j=fft->needle-1-(displaySize>>1)-(int)fft->waveLen, k=0; kwaveLen; j++, k++) { - double one=((double)buf->data[j&0xffff]/32768.0); - double two=(double)k*(-2.0*M_PI)/fft->waveLen; - dft[0]+=one*cos(two); - dft[1]+=one*sin(two); + // find highest point + for (int j=(FURNACE_FFT_SIZE>>1)-1; j>fft->waveLenBottom; j--) { + if (fft->corrBuf[j]>waveLenCandH) { + waveLenCandH=fft->corrBuf[j]; + fft->waveLen=j; + } } + fft->waveLenTop=fft->waveLen; - // calculate and lock into phase - phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); + // did we find the period size? + if (fft->waveLen<(FURNACE_FFT_SIZE-32)) { + // we got pitch + chanOscPitch[ch]=pow(1.0-(fft->waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); + + fft->waveLen*=(double)displaySize*2.0/(double)FURNACE_FFT_SIZE; - if (chanOscWaveCorr) { - fft->needle-=phase*fft->waveLen; + // DFT of one period (x_1) + double dft[2]; + dft[0]=0.0; + dft[1]=0.0; + for (int j=fft->needle-1-(displaySize>>1)-(int)fft->waveLen, k=0; kwaveLen; j++, k++) { + double one=((double)buf->data[j&0xffff]/32768.0); + double two=(double)k*(-2.0*M_PI)/fft->waveLen; + dft[0]+=one*cos(two); + dft[1]+=one*sin(two); + } + + // calculate and lock into phase + phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); + + if (chanOscWaveCorr) { + fft->needle-=phase*fft->waveLen; + } } } - } - fft->needle-=displaySize; + fft->needle-=displaySize; + },fft_); } } } + chanOscWorkPool->wait(); // 0: none // 1: sqrt(chans) @@ -644,7 +657,12 @@ void FurnaceGUI::drawChanOsc() { } ImGui::PushClipRect(inRect.Min,inRect.Max,false); + ImDrawListFlags prevFlags=dl->Flags; + //if (!settings.oscAntiAlias) { + dl->Flags&=~(ImDrawListFlags_AntiAliasedLines|ImDrawListFlags_AntiAliasedLinesUseTex); + //} dl->AddPolyline(waveform,precision,color,ImDrawFlags_None,dpiScale); + dl->Flags=prevFlags; if (!chanOscTextFormat.empty()) { String text; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 7673a19ca..ed43482ec 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -6685,6 +6685,9 @@ bool FurnaceGUI::init() { } #endif + cpuCores=SDL_GetCPUCount(); + if (cpuCores<1) cpuCores=1; + logI("done!"); return true; } @@ -6857,6 +6860,10 @@ bool FurnaceGUI::finish() { backupTask.get(); } + if (chanOscWorkPool!=NULL) { + delete chanOscWorkPool; + } + return true; } @@ -7281,6 +7288,7 @@ FurnaceGUI::FurnaceGUI(): chanOscTextColor(1.0f,1.0f,1.0f,0.75f), chanOscGrad(64,64), chanOscGradTex(NULL), + chanOscWorkPool(NULL), followLog(true), #ifdef IS_MOBILE pianoOctaves(7), diff --git a/src/gui/gui.h b/src/gui/gui.h index afba9099c..cdf676303 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -21,6 +21,7 @@ #define _FUR_GUI_H #include "../engine/engine.h" +#include "../engine/workPool.h" #include "../engine/waveSynth.h" #include "imgui.h" #include "imgui_impl_sdl2.h" @@ -1346,6 +1347,7 @@ class FurnaceGUI { int mobileEditPage; int wheelCalmDown; int shallDetectScale; + int cpuCores; float mobileMenuPos, autoButtonSize, mobileEditAnim; ImVec2 mobileEditButtonPos, mobileEditButtonSize; const int* curSysSection; @@ -1572,6 +1574,7 @@ class FurnaceGUI { int insIconsStyle; int classicChipOptions; int wasapiEx; + int chanOscThreads; unsigned int maxUndoSteps; String mainFontPath; String headFontPath; @@ -1747,6 +1750,7 @@ class FurnaceGUI { insIconsStyle(1), classicChipOptions(0), wasapiEx(0), + chanOscThreads(0), maxUndoSteps(100), mainFontPath(""), headFontPath(""), @@ -2047,6 +2051,7 @@ class FurnaceGUI { ImVec4 chanOscColor, chanOscTextColor; Gradient2D chanOscGrad; FurnaceGUITexture* chanOscGradTex; + DivWorkPool* chanOscWorkPool; float chanOscLP0[DIV_MAX_CHANS]; float chanOscLP1[DIV_MAX_CHANS]; float chanOscVol[DIV_MAX_CHANS]; @@ -2058,10 +2063,11 @@ class FurnaceGUI { double* inBuf; fftw_complex* outBuf; double* corrBuf; + DivDispatchOscBuffer* relatedBuf; size_t inBufPos; double inBufPosFrac; double waveLen; - int waveLenBottom, waveLenTop; + int waveLenBottom, waveLenTop, relatedCh; unsigned short needle; bool ready, loudEnough; fftw_plan plan; @@ -2070,11 +2076,13 @@ class FurnaceGUI { inBuf(NULL), outBuf(NULL), corrBuf(NULL), + relatedBuf(NULL), inBufPos(0), inBufPosFrac(0.0f), waveLen(0.0), waveLenBottom(0), waveLenTop(0), + relatedCh(0), needle(0), ready(false), loudEnough(false), diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 186905143..31463778f 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -400,6 +400,27 @@ void FurnaceGUI::drawSettings() { ImGui::SetTooltip("may cause issues with high-polling-rate mice when previewing notes."); } + pushWarningColor(settings.chanOscThreads>cpuCores,settings.chanOscThreads>(cpuCores*2)); + if (ImGui::InputInt("Per-channel oscilloscope threads",&settings.chanOscThreads)) { + if (settings.chanOscThreads<0) settings.chanOscThreads=0; + if (settings.chanOscThreads>(cpuCores*3)) settings.chanOscThreads=cpuCores*3; + if (settings.chanOscThreads>256) settings.chanOscThreads=256; + } + if (settings.chanOscThreads>=(cpuCores*3)) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("you're being silly, aren't you? that's enough."); + } + } else if (settings.chanOscThreads>(cpuCores*2)) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("what are you doing? stop!"); + } + } else if (settings.chanOscThreads>cpuCores) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("it is a bad idea to set this number higher than your CPU core count (%d)!",cpuCores); + } + } + popWarningColor(); + // SUBSECTION FILE CONFIG_SUBSECTION("File"); @@ -3262,6 +3283,7 @@ void FurnaceGUI::syncSettings() { settings.insIconsStyle=e->getConfInt("insIconsStyle",1); settings.classicChipOptions=e->getConfInt("classicChipOptions",0); settings.wasapiEx=e->getConfInt("wasapiEx",0); + settings.chanOscThreads=e->getConfInt("chanOscThreads",0); clampSetting(settings.mainFontSize,2,96); clampSetting(settings.headFontSize,2,96); @@ -3410,6 +3432,7 @@ void FurnaceGUI::syncSettings() { clampSetting(settings.insIconsStyle,0,2); clampSetting(settings.classicChipOptions,0,1); clampSetting(settings.wasapiEx,0,1); + clampSetting(settings.chanOscThreads,0,256); if (settings.exportLoops<0.0) settings.exportLoops=0.0; if (settings.exportFadeOut<0.0) settings.exportFadeOut=0.0; @@ -3665,6 +3688,7 @@ void FurnaceGUI::commitSettings() { e->setConf("insIconsStyle",settings.insIconsStyle); e->setConf("classicChipOptions",settings.classicChipOptions); e->setConf("wasapiEx",settings.wasapiEx); + e->setConf("chanOscThreads",settings.chanOscThreads); // colors for (int i=0; i Date: Tue, 5 Sep 2023 04:55:22 -0500 Subject: [PATCH 25/58] reduce intro volume, part 1 --- res/intro.fur | Bin 145892 -> 144865 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/res/intro.fur b/res/intro.fur index 9ea563ed26aa0849471d5b3c4fe20e9a6267d932..bb4f9118366d3ca640a29f0c9958585296417553 100644 GIT binary patch delta 134580 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 tFlMslN+Cr?J}55=N?L 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