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/demos/arcade/last_day_of_summer_NamcoC15.fur b/demos/arcade/last_day_of_summer_NamcoC15.fur new file mode 100644 index 000000000..bf794a44b Binary files /dev/null and b/demos/arcade/last_day_of_summer_NamcoC15.fur differ diff --git a/demos/nes/sweatsmile_bossfight.fur b/demos/nes/sweatsmile_bossfight.fur new file mode 100644 index 000000000..7aa2652cb Binary files /dev/null and b/demos/nes/sweatsmile_bossfight.fur differ 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..fe8eacf8d --- /dev/null +++ b/doc/1-intro/glossary.md @@ -0,0 +1,139 @@ +# 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 lossy sample format 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 (1)**: the marker of input focus. anything typed will happen at the cursor's location. + +**cursor (2)**: the pointer controlled by a mouse or similar input. clicking when the cursor(2) is in a valid area will place the cursor(1) there. + +**DAC**: digital analog converter. this converts a digital representation of sound into actual output. + +**.dmf**: DefleMask Module File. +- _Furnace:_ .dmf 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:_ .dmf files may be saved, but full compatibility isn't guaranteed and many features will be missing. this isn't recommended unless absolutely necessary. + +**.dmp**: DefleMask Preset. an instrument file. + +**.dmw**: DefleMask Wavetable. a 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 cycle**: usually called _pulse width._ in a pulse wave, this is the ratio of the high part to the high and low combined. + +**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 module 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. 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**: to fill in the area between two values with a smooth ramp of values in between. +- some sample-based chips can interpolate, filtering out unwanted harmonics. + +**ladder effect**: an inaccurate yet common term for the DAC distortion that affects some Yamaha FM chips. + +**LFO**: low frequency oscillator. a wave with a slow period (often below hearing range) used to alter other sounds. + +**macro**: a sequence of values automatically applied while a note plays. + +**noise bass**: the technique of using a PSG's periodic noise generator with a very short period to create low-frequency sounds. + +**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. + +**period**: the length of a repeating waveform. as frequency rises, the period shortens. + +**periodic noise**: an approximation of random noise generated algorithmically. +- the period is the number of values generated until the algorithm repeats itself. + +**phase reset**: to restart a waveform at its initial value. +- for FM instruments, this restarts the volume envelope also. + +**PSG**: programmable sound generator. any sound chip is a PSG, though the term is often used to specifically refer 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. + +**pulse width**: sometimes called _duty cycle._ in a pulse wave, this is the ratio of the high part to the high and low combined. + +**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** (1): 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. + +**sample** (2): a single value taken from a digitally recorded sound. a sample(1) is made up of samples(2). + +**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 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. +- 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 chorusing effect. + +**tick rate**: the number of times per second that the sound engine moves forward. all notes and effects are quantized to this rate. +- 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**: Video Game Music. a file containing the log of data sent to a sound chip 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. + +**waveform**: a very short period of repeating sound. +- the most basic waveform is a sine wave. others include triangle, pulse, saw, and the like. + +**wavetable** (1): a very short looping sample. + +**wavetable** (2): an ordered group of wavetables(1) used in sequence within a single instrument. + +**.zsm**: ZSound Music. a VGM-like file meant specifically for the Commander X16 computer. 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 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: diff --git a/res/intro.fur b/res/intro.fur index 9ea563ed2..bb4f91183 100644 Binary files a/res/intro.fur and b/res/intro.fur differ diff --git a/src/check/calc_checksum.c b/src/check/calc_checksum.c new file mode 100644 index 000000000..8e13eb6dd --- /dev/null +++ b/src/check/calc_checksum.c @@ -0,0 +1,24 @@ +#include + +int main(int argc, char** argv) { + if (argc<2) { + printf("usage: %s text\n",argv[0]); + return 1; + } + + unsigned int checker=0x11111111; + unsigned int checker1=0; + int index=0; + + for (char* i=argv[1]; *i; i++) { + checker^=((unsigned int)(*i))<>1|(((checker)^(checker>>2)^(checker>>3)^(checker>>5))&1)<<31); + checker1<<=1; + index=(index+1)&31; + } + + printf("%.8x %x\n",checker,checker1); + + return 0; +} diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 8dbe1f26d..86eebfa31 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -23,6 +23,7 @@ #include "engine.h" #include "instrument.h" #include "safeReader.h" +#include "workPool.h" #include "../ta-log.h" #include "../fileutils.h" #ifdef HAVE_SDL2 @@ -2016,6 +2017,10 @@ bool DivEngine::isPreviewingSample() { return (sPreview.sample>=0 && sPreview.sample<(int)song.sample.size()); } +int DivEngine::getSamplePreviewSample() { + return sPreview.sample; +} + int DivEngine::getSamplePreviewPos() { return sPreview.pos; } @@ -3119,6 +3124,10 @@ bool DivEngine::switchMaster(bool full) { quitDispatch(); initDispatch(); } + if (renderPool!=NULL) { + delete renderPool; + renderPool=NULL; + } if (initAudioBackend()) { for (int i=0; i2.0f) metroVol=2.0f; + renderPoolThreads=getConfInt("renderPoolThreads",0); if (lowLatency) logI("using low latency mode."); diff --git a/src/engine/engine.h b/src/engine/engine.h index 0c755f8f9..f94cb3e1f 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -39,6 +39,8 @@ #include #include +class DivWorkPool; + #define addWarning(x) \ if (warnings.empty()) { \ warnings+=x; \ @@ -195,6 +197,10 @@ struct DivDispatchContainer { bool lowQuality, dcOffCompensation; double rateMemory; + // used in multi-thread + int cycles; + unsigned int size; + void setRates(double gotRate); void setQuality(bool lowQual); void grow(size_t size); @@ -213,7 +219,9 @@ struct DivDispatchContainer { lastAvail(0), lowQuality(false), dcOffCompensation(false), - rateMemory(0.0) { + rateMemory(0.0), + cycles(0), + size(0) { memset(bb,0,DIV_MAX_OUTPUTS*sizeof(blip_buffer_t*)); memset(temp,0,DIV_MAX_OUTPUTS*sizeof(int)); memset(prevSample,0,DIV_MAX_OUTPUTS*sizeof(int)); @@ -485,6 +493,9 @@ class DivEngine { size_t totalProcessed; + unsigned int renderPoolThreads; + DivWorkPool* renderPool; + // MIDI stuff std::function midiCallback=[](const TAMidiMessage&) -> int {return -2;}; @@ -714,6 +725,7 @@ class DivEngine { // sample preview query bool isPreviewingSample(); + int getSamplePreviewSample(); int getSamplePreviewPos(); double getSamplePreviewRate(); @@ -1259,6 +1271,8 @@ class DivEngine { metroAmp(0.0f), metroVol(1.0f), totalProcessed(0), + renderPoolThreads(0), + renderPool(NULL), curOrders(NULL), curPat(NULL), tempIns(NULL), diff --git a/src/engine/platform/amiga.cpp b/src/engine/platform/amiga.cpp index c97624c54..a4e279dad 100644 --- a/src/engine/platform/amiga.cpp +++ b/src/engine/platform/amiga.cpp @@ -80,7 +80,7 @@ const char** DivPlatformAmiga::getRegisterSheet() { } void DivPlatformAmiga::acquire(short** buf, size_t len) { - static int outL, outR, output; + thread_local int outL, outR, output; for (size_t h=0; hdebug_engine(); diff --git a/src/engine/platform/genesis.cpp b/src/engine/platform/genesis.cpp index 1564fa0cc..4973a323d 100644 --- a/src/engine/platform/genesis.cpp +++ b/src/engine/platform/genesis.cpp @@ -132,8 +132,8 @@ void DivPlatformGenesis::processDAC(int iRate) { } void DivPlatformGenesis::acquire_nuked(short** buf, size_t len) { - static short o[2]; - static int os[2]; + thread_local short o[2]; + thread_local int os[2]; for (size_t h=0; hdebug_engine(); 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; } } diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp index c8c5c944c..44e9b3b14 100644 --- a/src/engine/platform/opl.cpp +++ b/src/engine/platform/opl.cpp @@ -160,9 +160,9 @@ const int orderedOpsL[4]={ #define ADDR_LR_FB_ALG 0xc0 void DivPlatformOPL::acquire_nuked(short** buf, size_t len) { - static short o[4]; - static int os[4]; - static ymfm::ymfm_output<2> aOut; + thread_local short o[4]; + thread_local int os[4]; + thread_local ymfm::ymfm_output<2> aOut; for (size_t h=0; h65535) chan[adpcmChan].freq=65535; immWrite(16,chan[adpcmChan].freq&0xff); immWrite(17,(chan[adpcmChan].freq>>8)&0xff); if (chan[adpcmChan].keyOn || chan[adpcmChan].keyOff) { diff --git a/src/engine/platform/opll.cpp b/src/engine/platform/opll.cpp index 1c7dc72e1..3f1621c02 100644 --- a/src/engine/platform/opll.cpp +++ b/src/engine/platform/opll.cpp @@ -43,8 +43,8 @@ const unsigned char visMapOPLL[9]={ }; void DivPlatformOPLL::acquire_nuked(short** buf, size_t len) { - static int o[2]; - static int os; + thread_local int o[2]; + thread_local int os; for (size_t h=0; hdebug_engine(); diff --git a/src/engine/platform/ym2203.cpp b/src/engine/platform/ym2203.cpp index 2e805f3cd..53d2cfe06 100644 --- a/src/engine/platform/ym2203.cpp +++ b/src/engine/platform/ym2203.cpp @@ -165,8 +165,8 @@ void DivPlatformYM2203::acquire(short** buf, size_t len) { } void DivPlatformYM2203::acquire_combo(short** buf, size_t len) { - static int os; - static short ignored[2]; + thread_local int os; + thread_local short ignored[2]; for (size_t h=0; h OPN @@ -241,7 +241,7 @@ void DivPlatformYM2203::acquire_combo(short** buf, size_t len) { } void DivPlatformYM2203::acquire_ymfm(short** buf, size_t len) { - static int os; + thread_local int os; ymfm::ym2203::fm_engine* fme=fm->debug_fm_engine(); diff --git a/src/engine/platform/ym2608.cpp b/src/engine/platform/ym2608.cpp index 93c09fd67..743cfab46 100644 --- a/src/engine/platform/ym2608.cpp +++ b/src/engine/platform/ym2608.cpp @@ -306,8 +306,8 @@ void DivPlatformYM2608::acquire(short** buf, size_t len) { } void DivPlatformYM2608::acquire_combo(short** buf, size_t len) { - static int os[2]; - static short ignored[2]; + thread_local int os[2]; + thread_local short ignored[2]; ymfm::ssg_engine* ssge=fm->debug_ssg_engine(); ymfm::adpcm_a_engine* aae=fm->debug_adpcm_a_engine(); @@ -419,7 +419,7 @@ void DivPlatformYM2608::acquire_combo(short** buf, size_t len) { } void DivPlatformYM2608::acquire_ymfm(short** buf, size_t len) { - static int os[2]; + thread_local int os[2]; ymfm::ym2608::fm_engine* fme=fm->debug_fm_engine(); ymfm::ssg_engine* ssge=fm->debug_ssg_engine(); @@ -783,6 +783,8 @@ void DivPlatformYM2608::tick(bool sysTick) { chan[15].freq=0; } } + if (chan[adpcmBChanOffs].freq<0) chan[adpcmBChanOffs].freq=0; + if (chan[adpcmBChanOffs].freq>65535) chan[adpcmBChanOffs].freq=65535; immWrite(0x109,chan[15].freq&0xff); immWrite(0x10a,(chan[15].freq>>8)&0xff); hardResetElapsed+=2; diff --git a/src/engine/platform/ym2610.cpp b/src/engine/platform/ym2610.cpp index b7dcdb231..173d882a6 100644 --- a/src/engine/platform/ym2610.cpp +++ b/src/engine/platform/ym2610.cpp @@ -241,8 +241,8 @@ void DivPlatformYM2610::acquire(short** buf, size_t len) { } void DivPlatformYM2610::acquire_combo(short** buf, size_t len) { - static int os[2]; - static short ignored[2]; + thread_local int os[2]; + thread_local short ignored[2]; ymfm::ssg_engine* ssge=fm->debug_ssg_engine(); ymfm::adpcm_a_engine* aae=fm->debug_adpcm_a_engine(); @@ -350,7 +350,7 @@ void DivPlatformYM2610::acquire_combo(short** buf, size_t len) { } void DivPlatformYM2610::acquire_ymfm(short** buf, size_t len) { - static int os[2]; + thread_local int os[2]; ymfm::ym2610::fm_engine* fme=fm->debug_fm_engine(); ymfm::ssg_engine* ssge=fm->debug_ssg_engine(); @@ -717,6 +717,8 @@ void DivPlatformYM2610::tick(bool sysTick) { } else { chan[adpcmBChanOffs].freq=0; } + if (chan[adpcmBChanOffs].freq<0) chan[adpcmBChanOffs].freq=0; + if (chan[adpcmBChanOffs].freq>65535) chan[adpcmBChanOffs].freq=65535; immWrite(0x19,chan[adpcmBChanOffs].freq&0xff); immWrite(0x1a,(chan[adpcmBChanOffs].freq>>8)&0xff); hardResetElapsed+=2; diff --git a/src/engine/platform/ym2610b.cpp b/src/engine/platform/ym2610b.cpp index fe5dbb581..4f00a7761 100644 --- a/src/engine/platform/ym2610b.cpp +++ b/src/engine/platform/ym2610b.cpp @@ -305,8 +305,8 @@ void DivPlatformYM2610B::acquire(short** buf, size_t len) { } void DivPlatformYM2610B::acquire_combo(short** buf, size_t len) { - static int os[2]; - static short ignored[2]; + thread_local int os[2]; + thread_local short ignored[2]; ymfm::ssg_engine* ssge=fm->debug_ssg_engine(); ymfm::adpcm_a_engine* aae=fm->debug_adpcm_a_engine(); @@ -418,7 +418,7 @@ void DivPlatformYM2610B::acquire_combo(short** buf, size_t len) { } void DivPlatformYM2610B::acquire_ymfm(short** buf, size_t len) { - static int os[2]; + thread_local int os[2]; ymfm::ym2610b::fm_engine* fme=fm->debug_fm_engine(); ymfm::ssg_engine* ssge=fm->debug_ssg_engine(); @@ -784,6 +784,8 @@ void DivPlatformYM2610B::tick(bool sysTick) { } else { chan[adpcmBChanOffs].freq=0; } + if (chan[adpcmBChanOffs].freq<0) chan[adpcmBChanOffs].freq=0; + if (chan[adpcmBChanOffs].freq>65535) chan[adpcmBChanOffs].freq=65535; immWrite(0x19,chan[adpcmBChanOffs].freq&0xff); immWrite(0x1a,(chan[adpcmBChanOffs].freq>>8)&0xff); hardResetElapsed+=2; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 270dd7f79..6f6414225 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -22,6 +22,7 @@ #define _USE_MATH_DEFINES #include "dispatch.h" #include "engine.h" +#include "workPool.h" #include "../ta-log.h" #include @@ -1759,6 +1760,13 @@ void DivEngine::runMidiTime(int totalCycles) { } } +void _runDispatch1(void* d) { +} + +void _runDispatch2(void* d) { + +} + void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsigned int size) { lastNBIns=inChans; lastNBOuts=outChans; @@ -1788,6 +1796,13 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi std::chrono::steady_clock::time_point ts_processBegin=std::chrono::steady_clock::now(); + if (renderPool==NULL) { + unsigned int howManyThreads=song.systemLen; + if (howManyThreads<2) howManyThreads=0; + if (howManyThreads>renderPoolThreads) howManyThreads=renderPoolThreads; + renderPool=new DivWorkPool(howManyThreads); + } + // process MIDI events (TODO: everything) if (output) if (output->midiIn) while (!output->midiIn->queue.empty()) { TAMidiMessage& msg=output->midiIn->queue.front(); @@ -2061,20 +2076,30 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi // 5. tick the clock and fill buffers as needed if (cyclespush([](void* d) { + DivDispatchContainer* dc=(DivDispatchContainer*)d; + int total=(dc->cycles*dc->runtotal)/(dc->size<acquire(dc->runPos,total); + dc->runLeft-=total; + dc->runPos+=total; + },&disCont[i]); } + renderPool->wait(); runLeftG-=cycles; cycles=0; } else { cycles-=runLeftG; runLeftG=0; for (int i=0; ipush([](void* d) { + DivDispatchContainer* dc=(DivDispatchContainer*)d; + dc->acquire(dc->runPos,dc->runLeft); + dc->runLeft=0; + },&disCont[i]); } + renderPool->wait(); } } } @@ -2093,8 +2118,12 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi logW("%d: sizepush([](void* d) { + DivDispatchContainer* dc=(DivDispatchContainer*)d; + dc->fillBuf(dc->runtotal,dc->lastAvail,dc->size-dc->lastAvail); + },&disCont[i]); } + renderPool->wait(); } if (metroBufLen + +void* _workThread(void* inst) { + ((DivWorkThread*)inst)->run(); + return NULL; +} + +void DivWorkThread::run() { + //std::unique_lock unique(selfLock); + DivPendingTask task; + bool setFuckingPromise=false; + + logV("running work thread"); + + while (true) { + lock.lock(); + if (tasks.empty()) { + lock.unlock(); + isBusy=false; + if (setFuckingPromise) { + parent->notify.set_value(); + setFuckingPromise=false; + //std::this_thread::yield(); + } + if (terminate) { + break; + } + std::future future=notify.get_future(); + future.wait(); + lock.lock(); + notify=std::promise(); + promiseAlreadySet=false; + lock.unlock(); + continue; + } else { + task=tasks.front(); + tasks.pop(); + lock.unlock(); + + task.func(task.funcArg); + + int busyCount=--parent->busyCount; + if (busyCount<0) { + logE("oh no PROBLEM..."); + } + if (busyCount==0) { + setFuckingPromise=true; + } + } + } +} + +bool DivWorkThread::assign(void (*what)(void*), void* arg) { + lock.lock(); + if (tasks.size()>=30) { + lock.unlock(); + return false; + } + tasks.push(DivPendingTask(what,arg)); + parent->busyCount++; + isBusy=true; + lock.unlock(); + return true; +} + +void DivWorkThread::wait() { + if (!isBusy) return; +} + +bool DivWorkThread::busy() { + return isBusy; +} + +void DivWorkThread::finish() { + lock.lock(); + terminate=true; + notify.set_value(); + lock.unlock(); + thread->join(); +} + +bool DivWorkThread::init(DivWorkPool* p) { + parent=p; + try { + thread=new std::thread(_workThread,this); + } catch (std::system_error& e) { + logE("could not start thread! %s",e.what()); + thread=NULL; + return false; + } + return true; +} + +void DivWorkPool::push(void (*what)(void*), void* arg) { + // if no work threads, just execute + if (!threaded) { + what(arg); + return; + } + + for (unsigned int tryCount=0; tryCount=count) pos=0; + if (workThreads[pos++].assign(what,arg)) return; + } + + // all threads are busy + logW("DivWorkPool: all work threads busy!"); + what(arg); +} + +bool DivWorkPool::busy() { + if (!threaded) return false; + for (unsigned int i=0; i future=notify.get_future(); + + // start running + for (unsigned int i=0; i(); + + pos=0; +} + +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 { + void (*func)(void*); + void* funcArg; + DivPendingTask(void (*f)(void*), void* arg): + func(f), + funcArg(arg) {} + DivPendingTask(): + func(NULL), + funcArg(NULL) {} +}; + +struct DivWorkThread { + DivWorkPool* parent; + std::mutex lock; + std::thread* thread; + std::promise notify; + FixedQueue tasks; + std::atomic isBusy; + bool terminate; + bool promiseAlreadySet; + + void run(); + bool assign(void (*what)(void*), void* arg); + void wait(); + bool busy(); + void finish(); + + bool init(DivWorkPool* p); + DivWorkThread(): + parent(NULL), + isBusy(false), + terminate(false), + promiseAlreadySet(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 { + bool threaded; + unsigned int count; + unsigned int pos; + DivWorkThread* workThreads; + public: + std::promise 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. + */ + void push(void (*what)(void*), void* arg); + + /** + * check whether this work pool is busy. + */ + bool busy(); + + /** + * wait for all work threads to finish. + */ + void wait(); + + DivWorkPool(unsigned int threads=0); + ~DivWorkPool(); +}; + +#endif diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 0c9834795..cdb2fbc23 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -38,7 +38,7 @@ const char* aboutLine[]={ "akumanatt", "cam900", "djtuBIG-MaliceX", - "Eknous-P", + "Eknous", "laoo", "MooingLemur", "OPNA2608", @@ -79,12 +79,13 @@ const char* aboutLine[]={ "Burnt Fishy", "CaptainMalware", "Clingojam", + "Crisps", "DeMOSic", "DevEd", "Dippy", "djtuBIG-MaliceX", "dumbut", - "Eknous-P", + "Eknous", "Electric Keet", "EpicTyphlosion", "FΛDE", @@ -125,6 +126,7 @@ const char* aboutLine[]={ "TakuikaNinja", "TCORPStudios", "Teuthida", + "ThaCuber", "The Blender Fiddler", "TheDuccinator", "theloredev", diff --git a/src/gui/chanOsc.cpp b/src/gui/chanOsc.cpp index 4371d6fd3..04908702e 100644 --- a/src/gui/chanOsc.cpp +++ b/src/gui/chanOsc.cpp @@ -60,7 +60,7 @@ float FurnaceGUI::computeGradPos(int type, int chan) { return 1.0f; break; case GUI_OSCREF_FREQUENCY: - return chanOscPitch[chan]; + return chanOscChan[chan].pitch; break; case GUI_OSCREF_VOLUME: return chanOscVol[chan]; @@ -363,10 +363,17 @@ void FurnaceGUI::drawChanOsc() { std::vector oscChans; int chans=e->getTotalChannelCount(); ImGuiWindow* window=ImGui::GetCurrentWindow(); - ImVec2 waveform[512]; 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); if (buf!=NULL && e->curSubSong->chanShow[i]) { @@ -376,6 +383,148 @@ void FurnaceGUI::drawChanOsc() { } } + // process + for (size_t i=0; irelatedBuf=oscBufs[i]; + fft_->relatedCh=oscChans[i]; + + if (fft_->relatedBuf!=NULL) { + // prepare + if (centerSettingReset) { + fft_->relatedBuf->readNeedle=fft_->relatedBuf->needle; + } + + // check FFT status existence + 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) { + 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()) { + fft_->windowSize=chanOscWindowSize; + fft_->waveCorr=chanOscWaveCorr; + chanOscWorkPool->push([](void* fft_v) { + ChanOscStatus* fft=(ChanOscStatus*)fft_v; + DivDispatchOscBuffer* buf=fft->relatedBuf; + + // 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; + int displaySize=(float)(buf->rate)*(fft->windowSize/1000.0f); + 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) 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 (fft->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 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; + } + } + + // 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; + + // did we find the period size? + if (fft->waveLen<(FURNACE_FFT_SIZE-32)) { + // we got pitch + fft->pitch=pow(1.0-(fft->waveLen/(double)(FURNACE_FFT_SIZE>>1)),4.0); + + 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); + } + + // calculate and lock into phase + phase=(0.5+(atan2(dft[1],dft[0])/(2.0*M_PI))); + + if (fft->waveCorr) { + fft->needle-=phase*fft->waveLen; + } + } + } + + fft->needle-=displaySize; + },fft_); + } + } + } + chanOscWorkPool->wait(); + // 0: none // 1: sqrt(chans) // 2: sqrt(chans+1) @@ -396,6 +545,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->plan=fftw_plan_dft_r2c_1d(FURNACE_FFT_SIZE,fft->inBuf,fft->outBuf,FFTW_ESTIMATE); - } - - int displaySize=(float)(buf->rate)*(chanOscWindowSize/1000.0f); - ImVec2 minArea=window->DC.CursorPos; ImVec2 maxArea=ImVec2( minArea.x+size.x, @@ -437,67 +573,78 @@ 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; ImGui::ItemSize(size,style.FramePadding.y); if (ImGui::ItemAdd(rect,ImGui::GetID("chOscDisplay"))) { if (!e->isRunning()) { - for (unsigned short i=0; irate)*(chanOscWindowSize/1000.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; - } - 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; + + 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)); } } - - // PHASE - fftw_complex& candPoint=fft->outBuf[point]; - double phase=((double)(displaySize*2)/(double)point)*(0.5+(atan2(candPoint[1],candPoint[0])/(M_PI*2))); - - if (chanOscWaveCorr) { - needlePos-=phase; - } - chanOscPitch[ch]=(float)point/32.0f; - - needlePos-=displaySize; - for (unsigned short i=0; idata[(unsigned short)(needlePos+(i*displaySize/precision))]/32768.0f; - if (minLevel>y) minLevel=y; - if (maxLeveldata[(unsigned short)(needlePos+(i*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)); - } - - //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()); } ImU32 color=ImGui::GetColorU32(chanOscColor); if (chanOscUseGrad) { @@ -511,15 +658,18 @@ void FurnaceGUI::drawChanOsc() { } ImGui::PushClipRect(inRect.Min,inRect.Max,false); + //ImDrawListFlags prevFlags=dl->Flags; + //dl->Flags&=~(ImDrawListFlags_AntiAliasedLines|ImDrawListFlags_AntiAliasedLinesUseTex); dl->AddPolyline(waveform,precision,color,ImDrawFlags_None,dpiScale); + //dl->Flags=prevFlags; if (!chanOscTextFormat.empty()) { 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; @@ -560,7 +710,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': { @@ -601,19 +751,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()); } diff --git a/src/gui/compatFlags.cpp b/src/gui/compatFlags.cpp index efa88feec..50fb3b0cd 100644 --- a/src/gui/compatFlags.cpp +++ b/src/gui/compatFlags.cpp @@ -210,11 +210,15 @@ void FurnaceGUI::drawCompatFlags() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("like ProTracker/FamiTracker"); } - if (ImGui::RadioButton("Partial (only 04xy/E5xx)",e->song.linearPitch==1)) { - e->song.linearPitch=1; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("like DefleMask"); + if (e->song.linearPitch==1) { + pushWarningColor(true); + if (ImGui::RadioButton("Partial (only 04xy/E5xx)",e->song.linearPitch==1)) { + e->song.linearPitch=1; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("like DefleMask\n\nthis pitch linearity mode is deprecated due to:\n- excessive complexity\n- lack of possible optimization\n\nit is recommended to change it now because I will remove this option in the future!"); + } + popWarningColor(); } if (ImGui::RadioButton("Full",e->song.linearPitch==2)) { e->song.linearPitch=2; 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) { 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..497aecace 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -614,6 +614,21 @@ void FurnaceGUI::updateWindowTitle() { } if (sdlWin!=NULL) SDL_SetWindowTitle(sdlWin,title.c_str()); + + if (e->song.insLen==1) { + unsigned int checker=0x11111111; + unsigned int checker1=0; + DivInstrument* ins=e->getIns(0); + if (ins->name.size()==15 && e->curSubSong->ordersLen==8) { + for (int i=0; i<15; i++) { + checker^=ins->name[i]<name[i]; + checker=(checker>>1|(((checker)^(checker>>2)^(checker>>3)^(checker>>5))&1)<<31); + checker1<<=1; + } + if (checker==0x5ec4497d && checker1==0x6347ee) nonLatchNibble=true; + } + } } void FurnaceGUI::autoDetectSystem() { @@ -1082,7 +1097,9 @@ float FurnaceGUI::calcBPM(const DivGroovePattern& speeds, float hz, int vN, int void FurnaceGUI::play(int row) { memset(chanOscVol,0,DIV_MAX_CHANS*sizeof(float)); - memset(chanOscPitch,0,DIV_MAX_CHANS*sizeof(float)); + for (int i=0; iwalkSong(loopOrder,loopRow,loopEnd); memset(lastIns,-1,sizeof(int)*DIV_MAX_CHANS); @@ -1567,7 +1584,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 +1596,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 +1605,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save File", {"Furnace song", "*.fur"}, - "Furnace song{.fur}", workingDirSong, dpiScale ); @@ -1600,7 +1614,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 +1623,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 +1639,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 +1656,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 +1690,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Instrument", {"Furnace instrument", "*.fui"}, - "Furnace instrument{.fui}", workingDirIns, dpiScale ); @@ -1691,7 +1699,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Instrument", {"DefleMask preset", "*.dmp"}, - "DefleMask preset{.dmp}", workingDirIns, dpiScale ); @@ -1703,7 +1710,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Load Wavetable", {"compatible files", "*.fuw *.dmw", "all files", "*"}, - "compatible files{.fuw,.dmw},.*", workingDirWave, dpiScale, NULL, // TODO @@ -1715,7 +1721,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Wavetable", {"Furnace wavetable", ".fuw"}, - "Furnace wavetable{.fuw}", workingDirWave, dpiScale ); @@ -1725,7 +1730,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Wavetable", {"DefleMask wavetable", ".dmw"}, - "DefleMask wavetable{.dmw}", workingDirWave, dpiScale ); @@ -1735,7 +1739,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Wavetable", {"raw data", ".raw"}, - "raw data{.raw}", workingDirWave, dpiScale ); @@ -1747,7 +1750,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 +1762,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Load Raw Sample", {"all files", "*"}, - ".*", workingDirSample, dpiScale ); @@ -1770,7 +1771,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Save Sample", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirSample, dpiScale ); @@ -1778,9 +1778,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 +1789,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Audio", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirAudioExport, dpiScale ); @@ -1800,7 +1798,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Audio", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirAudioExport, dpiScale ); @@ -1810,7 +1807,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Audio", {"Wave file", "*.wav"}, - "Wave file{.wav}", workingDirAudioExport, dpiScale ); @@ -1820,7 +1816,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export VGM", {"VGM file", "*.vgm"}, - "VGM file{.vgm}", workingDirVGMExport, dpiScale ); @@ -1830,7 +1825,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export ZSM", {"ZSM file", "*.zsm"}, - "ZSM file{.zsm}", workingDirZSMExport, dpiScale ); @@ -1840,7 +1834,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Command Stream", {"text file", "*.txt"}, - "text file{.txt}", workingDirROMExport, dpiScale ); @@ -1850,7 +1843,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Command Stream", {"binary file", "*.bin"}, - "binary file{.bin}", workingDirROMExport, dpiScale ); @@ -1863,7 +1855,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 +1864,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 +1873,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 +1882,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Color File", {"configuration files", "*.cfgc"}, - "configuration files{.cfgc}", workingDirColors, dpiScale ); @@ -1903,7 +1891,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Keybind File", {"configuration files", "*.cfgk"}, - "configuration files{.cfgk}", workingDirKeybinds, dpiScale ); @@ -1913,7 +1900,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openLoad( "Select Layout File", {".ini files", "*.ini"}, - ".ini files{.ini}", workingDirKeybinds, dpiScale ); @@ -1923,7 +1909,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Colors", {"configuration files", "*.cfgc"}, - "configuration files{.cfgc}", workingDirColors, dpiScale ); @@ -1933,7 +1918,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Keybinds", {"configuration files", "*.cfgk"}, - "configuration files{.cfgk}", workingDirKeybinds, dpiScale ); @@ -1943,7 +1927,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { hasOpened=fileDialog->openSave( "Export Layout", {".ini files", "*.ini"}, - ".ini files{.ini}", workingDirKeybinds, dpiScale ); @@ -1956,7 +1939,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Load ROM", {"compatible files", "*.rom *.bin", "all files", "*"}, - "compatible files{.rom,.bin},.*", workingDirROM, dpiScale ); @@ -1967,7 +1949,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Play Command Stream", {"command stream", "*.bin", "all files", "*"}, - "command stream{.bin},.*", workingDirROM, dpiScale ); @@ -1979,7 +1960,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 +1978,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 +1996,6 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { "Save Test", {"Furnace song", "*.fur", "DefleMask module", "*.dmf"}, - "Furnace song{.fur},DefleMask module{.dmf}", workingDirTest, dpiScale ); @@ -3835,7 +3813,9 @@ bool FurnaceGUI::loop() { if (!e->isRunning()) { activeNotes.clear(); memset(chanOscVol,0,DIV_MAX_CHANS*sizeof(float)); - memset(chanOscPitch,0,DIV_MAX_CHANS*sizeof(float)); + for (int i=0; isynchronized([this]() { @@ -4392,7 +4372,21 @@ bool FurnaceGUI::loop() { info="| Groove"; } - info+=fmt::sprintf(" @ %gHz (%g BPM) | Order %d/%d | Row %d/%d | %d:%.2d:%.2d.%.2d",e->getCurHz(),calcBPM(e->getSpeeds(),e->getCurHz(),e->curSubSong->virtualTempoN,e->curSubSong->virtualTempoD),e->getOrder(),e->curSubSong->ordersLen,e->getRow(),e->curSubSong->patLen,totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000); + info+=fmt::sprintf(" @ %gHz (%g BPM) ",e->getCurHz(),calcBPM(e->getSpeeds(),e->getCurHz(),e->curSubSong->virtualTempoN,e->curSubSong->virtualTempoD)); + + if (settings.orderRowsBase) { + info+=fmt::sprintf("| Order %.2X/%.2X ",e->getOrder(),e->curSubSong->ordersLen-1); + } else { + info+=fmt::sprintf("| Order %d/%d ",e->getOrder(),e->curSubSong->ordersLen-1); + } + + if (settings.patRowsBase) { + info+=fmt::sprintf("| Row %.2X/%.2X ",e->getRow(),e->curSubSong->patLen); + } else { + info+=fmt::sprintf("| Row %d/%d ",e->getRow(),e->curSubSong->patLen); + } + + info+=fmt::sprintf("| %d:%.2d:%.2d.%.2d",totalSeconds/3600,(totalSeconds/60)%60,totalSeconds%60,totalTicks/10000); ImGui::TextUnformatted(info.c_str()); } else { @@ -6724,6 +6718,9 @@ bool FurnaceGUI::init() { } #endif + cpuCores=SDL_GetCPUCount(); + if (cpuCores<1) cpuCores=1; + logI("done!"); return true; } @@ -6896,6 +6893,10 @@ bool FurnaceGUI::finish() { backupTask.get(); } + if (chanOscWorkPool!=NULL) { + delete chanOscWorkPool; + } + return true; } @@ -6950,6 +6951,7 @@ FurnaceGUI::FurnaceGUI(): killGraphics(false), audioEngineChanged(false), settingsChanged(false), + debugFFT(false), vgmExportVersion(0x171), vgmExportTrailingTicks(-1), drawHalt(10), @@ -7319,6 +7321,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), @@ -7418,7 +7421,9 @@ FurnaceGUI::FurnaceGUI(): memset(chanOscLP0,0,sizeof(float)*DIV_MAX_CHANS); memset(chanOscLP1,0,sizeof(float)*DIV_MAX_CHANS); memset(chanOscVol,0,sizeof(float)*DIV_MAX_CHANS); - memset(chanOscPitch,0,sizeof(float)*DIV_MAX_CHANS); + for (int i=0; ilockEngine([this]() { e->curSubSong->optimizePatterns(); }); + MARK_MODIFIED; } ImGui::SameLine(); if (ImGui::Button("Re-arrange patterns")) { e->lockEngine([this]() { e->curSubSong->rearrangePatterns(); }); + MARK_MODIFIED; } if (ImGui::BeginTable("PatManTable",257,ImGuiTableFlags_ScrollX|ImGuiTableFlags_SizingFixedFit)) { @@ -98,6 +100,7 @@ void FurnaceGUI::drawPatManager() { delete e->curSubSong->pat[i].data[k]; e->curSubSong->pat[i].data[k]=NULL; }); + MARK_MODIFIED; } ImGui::PopStyleColor(); } diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 2ca22f14e..3d17bec78 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -262,7 +262,10 @@ void FurnaceGUI::drawSampleEdit() { case DIV_SYSTEM_YM2608_CSM: if (sample->loop) { if (sample->loopStart!=0 || sample->loopEnd!=(int)(sample->samples)) { - SAMPLE_WARN(warnLoopPos,"YM2608: loop point ignored on ADPCM-B (may only loop entire sample)"); + SAMPLE_WARN(warnLoopPos,"YM2608: loop point ignored on ADPCM (may only loop entire sample)"); + } + if (sample->samples&511) { + SAMPLE_WARN(warnLength,"YM2608: sample length will be padded to multiple of 512"); } } break; @@ -276,6 +279,9 @@ void FurnaceGUI::drawSampleEdit() { if (sample->loopStart!=0 || sample->loopEnd!=(int)(sample->samples)) { SAMPLE_WARN(warnLoopPos,"YM2610: loop point ignored on ADPCM-B (may only loop entire sample)"); } + if (sample->samples&511) { + SAMPLE_WARN(warnLength,"YM2610: sample length will be padded to multiple of 512"); + } } if (sample->samples>2097152) { SAMPLE_WARN(warnLength,"YM2610: maximum ADPCM-A sample length is 2097152"); @@ -284,6 +290,16 @@ void FurnaceGUI::drawSampleEdit() { EXACT_RATE("YM2610 (ADPCM-A)",dispatch->chipClock/432); } break; + case DIV_SYSTEM_Y8950: + if (sample->loop) { + if (sample->loopStart!=0 || sample->loopEnd!=(int)(sample->samples)) { + SAMPLE_WARN(warnLoopPos,"Y8950: loop point ignored on ADPCM (may only loop entire sample)"); + } + if (sample->samples&511) { + SAMPLE_WARN(warnLength,"Y8950: sample length will be padded to multiple of 512"); + } + } + break; case DIV_SYSTEM_AMIGA: if (sample->loop) { if (sample->loopStart&1 || sample->loopEnd&1) { @@ -1197,6 +1213,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); @@ -1675,7 +1692,7 @@ void FurnaceGUI::drawSampleEdit() { } dl->PushClipRect(rectMin,rectMax); - if (e->isPreviewingSample()) { + if (e->isPreviewingSample() && e->getSamplePreviewSample()==curSample) { if (!statusBar2.empty()) { statusBar2+=" | "; } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 186905143..5d49c021e 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"); @@ -871,12 +892,44 @@ void FurnaceGUI::drawSettings() { ImGui::EndTable(); } + if (settings.showPool) { + bool renderPoolThreadsB=(settings.renderPoolThreads>0); + if (ImGui::Checkbox("Multi-threaded (EXPERIMENTAL)",&renderPoolThreadsB)) { + if (renderPoolThreadsB) { + settings.renderPoolThreads=2; + } else { + settings.renderPoolThreads=0; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("runs chip emulation on separate threads.\nmay increase performance when using heavy emulation cores.\n\nwarnings:\n- experimental!\n- only useful on multi-chip songs."); + } + + if (renderPoolThreadsB) { + pushWarningColor(settings.renderPoolThreads>cpuCores,settings.renderPoolThreads>cpuCores); + if (ImGui::InputInt("Number of threads",&settings.renderPoolThreads)) { + if (settings.renderPoolThreads<2) settings.renderPoolThreads=2; + if (settings.renderPoolThreads>32) settings.renderPoolThreads=32; + } + if (settings.renderPoolThreads>=DIV_MAX_CHIPS) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("that's the limit!"); + } + } else if (settings.renderPoolThreads>cpuCores) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("it is a VERY bad idea to set this number higher than your CPU core count (%d)!",cpuCores); + } + } + popWarningColor(); + } + } + bool lowLatencyB=settings.lowLatency; if (ImGui::Checkbox("Low-latency mode",&lowLatencyB)) { settings.lowLatency=lowLatencyB; } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("reduces latency by running the engine faster than the tick rate.\nuseful for live playback/jam mode.\n\nwarning: nonly enable if your buffer size is small (10ms or less)."); + ImGui::SetTooltip("reduces latency by running the engine faster than the tick rate.\nuseful for live playback/jam mode.\n\nwarning: only enable if your buffer size is small (10ms or less)."); } bool forceMonoB=settings.forceMono; @@ -1053,9 +1106,7 @@ void FurnaceGUI::drawSettings() { ImGui::TableNextColumn(); ImGui::Text("Action"); ImGui::TableNextColumn(); - ImGui::Text("Learn"); ImGui::TableNextColumn(); - ImGui::Text("Remove"); for (size_t i=0; i