Merge branch 'master' into nmk112
This commit is contained in:
commit
e839212aa0
|
@ -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
|
||||
|
|
BIN
demos/arcade/last_day_of_summer_NamcoC15.fur
Normal file
BIN
demos/arcade/last_day_of_summer_NamcoC15.fur
Normal file
Binary file not shown.
BIN
demos/nes/sweatsmile_bossfight.fur
Normal file
BIN
demos/nes/sweatsmile_bossfight.fur
Normal file
Binary file not shown.
|
@ -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.
|
||||
|
||||
|
|
139
doc/1-intro/glossary.md
Normal file
139
doc/1-intro/glossary.md
Normal file
|
@ -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.
|
|
@ -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
|
||||
|
||||
|
|
3
extern/igfd/ImGuiFileDialog.cpp
vendored
3
extern/igfd/ImGuiFileDialog.cpp
vendored
|
@ -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;
|
||||
}
|
||||
|
|
1
extern/igfd/ImGuiFileDialog.h
vendored
1
extern/igfd/ImGuiFileDialog.h
vendored
|
@ -745,6 +745,7 @@ namespace IGFD
|
|||
{
|
||||
public:
|
||||
std::string filter;
|
||||
std::string firstFilter;
|
||||
std::set<std::string> collectionfilters;
|
||||
|
||||
public:
|
||||
|
|
BIN
res/intro.fur
BIN
res/intro.fur
Binary file not shown.
24
src/check/calc_checksum.c
Normal file
24
src/check/calc_checksum.c
Normal file
|
@ -0,0 +1,24 @@
|
|||
#include <stdio.h>
|
||||
|
||||
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))<<index;
|
||||
checker1+=(unsigned int)(*i);
|
||||
checker=(checker>>1|(((checker)^(checker>>2)^(checker>>3)^(checker>>5))&1)<<31);
|
||||
checker1<<=1;
|
||||
index=(index+1)&31;
|
||||
}
|
||||
|
||||
printf("%.8x %x\n",checker,checker1);
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -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; i<song.systemLen; i++) {
|
||||
disCont[i].setRates(got.rate);
|
||||
|
@ -3273,6 +3282,10 @@ void DivEngine::quitDispatch() {
|
|||
for (int i=0; i<DIV_MAX_CHANS; i++) {
|
||||
isMuted[i]=0;
|
||||
}
|
||||
if (renderPool!=NULL) {
|
||||
delete renderPool;
|
||||
renderPool=NULL;
|
||||
}
|
||||
BUSY_END;
|
||||
}
|
||||
|
||||
|
@ -3310,6 +3323,7 @@ bool DivEngine::initAudioBackend() {
|
|||
midiOutMode=getConfInt("midiOutMode",DIV_MIDI_MODE_NOTE);
|
||||
if (metroVol<0.0f) metroVol=0.0f;
|
||||
if (metroVol>2.0f) metroVol=2.0f;
|
||||
renderPoolThreads=getConfInt("renderPoolThreads",0);
|
||||
|
||||
if (lowLatency) logI("using low latency mode.");
|
||||
|
||||
|
|
|
@ -39,6 +39,8 @@
|
|||
#include <unordered_map>
|
||||
#include <deque>
|
||||
|
||||
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<int(const TAMidiMessage&)> 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),
|
||||
|
|
|
@ -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; h<len; h++) {
|
||||
bool hsync=bypassLimits;
|
||||
outL=0;
|
||||
|
|
|
@ -52,7 +52,7 @@ const char** DivPlatformArcade::getRegisterSheet() {
|
|||
}
|
||||
|
||||
void DivPlatformArcade::acquire_nuked(short** buf, size_t len) {
|
||||
static int o[2];
|
||||
thread_local int o[2];
|
||||
|
||||
for (size_t h=0; h<len; h++) {
|
||||
for (int i=0; i<8; i++) {
|
||||
|
@ -92,7 +92,7 @@ void DivPlatformArcade::acquire_nuked(short** buf, size_t len) {
|
|||
}
|
||||
|
||||
void DivPlatformArcade::acquire_ymfm(short** buf, size_t len) {
|
||||
static int os[2];
|
||||
thread_local int os[2];
|
||||
|
||||
ymfm::ym2151::fm_engine* fme=fm_ymfm->debug_engine();
|
||||
|
||||
|
|
|
@ -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; h<len; h++) {
|
||||
processDAC(rate);
|
||||
|
@ -213,7 +213,7 @@ void DivPlatformGenesis::acquire_nuked(short** buf, size_t len) {
|
|||
}
|
||||
|
||||
void DivPlatformGenesis::acquire_ymfm(short** buf, size_t len) {
|
||||
static int os[2];
|
||||
thread_local int os[2];
|
||||
|
||||
ymfm::ym2612::fm_engine* fme=fm_ymfm->debug_engine();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; h<len; h++) {
|
||||
os[0]=0; os[1]=0; os[2]=0; os[3]=0;
|
||||
|
@ -549,6 +549,8 @@ void DivPlatformOPL::tick(bool sysTick) {
|
|||
chan[adpcmChan].freq=5461; // 4KHz
|
||||
}
|
||||
}
|
||||
if (chan[adpcmChan].freq<0) chan[adpcmChan].freq=0;
|
||||
if (chan[adpcmChan].freq>65535) chan[adpcmChan].freq=65535;
|
||||
immWrite(16,chan[adpcmChan].freq&0xff);
|
||||
immWrite(17,(chan[adpcmChan].freq>>8)&0xff);
|
||||
if (chan[adpcmChan].keyOn || chan[adpcmChan].keyOff) {
|
||||
|
|
|
@ -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; h<len; h++) {
|
||||
os=0;
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
#define chWrite(c,a,v) rWrite(((c)<<3)+(a),v)
|
||||
|
||||
void DivPlatformSegaPCM::acquire(short** buf, size_t len) {
|
||||
static int os[2];
|
||||
thread_local int os[2];
|
||||
|
||||
for (size_t h=0; h<len; h++) {
|
||||
while (!writes.empty()) {
|
||||
|
|
|
@ -58,7 +58,7 @@ const char** DivPlatformTX81Z::getRegisterSheet() {
|
|||
}
|
||||
|
||||
void DivPlatformTX81Z::acquire(short** buf, size_t len) {
|
||||
static int os[2];
|
||||
thread_local int os[2];
|
||||
|
||||
ymfm::ym2414::fm_engine* fme=fm_ymfm->debug_engine();
|
||||
|
||||
|
|
|
@ -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<len; h++) {
|
||||
// AY -> 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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#define _USE_MATH_DEFINES
|
||||
#include "dispatch.h"
|
||||
#include "engine.h"
|
||||
#include "workPool.h"
|
||||
#include "../ta-log.h"
|
||||
#include <math.h>
|
||||
|
||||
|
@ -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 (cycles<runLeftG) {
|
||||
for (int i=0; i<song.systemLen; i++) {
|
||||
int total=(cycles*disCont[i].runtotal)/(size<<MASTER_CLOCK_PREC);
|
||||
disCont[i].acquire(disCont[i].runPos,total);
|
||||
disCont[i].runLeft-=total;
|
||||
disCont[i].runPos+=total;
|
||||
disCont[i].cycles=cycles;
|
||||
disCont[i].size=size;
|
||||
renderPool->push([](void* d) {
|
||||
DivDispatchContainer* dc=(DivDispatchContainer*)d;
|
||||
int total=(dc->cycles*dc->runtotal)/(dc->size<<MASTER_CLOCK_PREC);
|
||||
dc->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; i<song.systemLen; i++) {
|
||||
disCont[i].acquire(disCont[i].runPos,disCont[i].runLeft);
|
||||
disCont[i].runLeft=0;
|
||||
renderPool->push([](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: size<lastAvail! %d<%d",i,size,disCont[i].lastAvail);
|
||||
continue;
|
||||
}
|
||||
disCont[i].fillBuf(disCont[i].runtotal,disCont[i].lastAvail,size-disCont[i].lastAvail);
|
||||
renderPool->push([](void* d) {
|
||||
DivDispatchContainer* dc=(DivDispatchContainer*)d;
|
||||
dc->fillBuf(dc->runtotal,dc->lastAvail,dc->size-dc->lastAvail);
|
||||
},&disCont[i]);
|
||||
}
|
||||
renderPool->wait();
|
||||
}
|
||||
|
||||
if (metroBufLen<size || metroBuf==NULL) {
|
||||
|
|
|
@ -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({
|
||||
|
@ -1179,7 +1180,7 @@ void DivEngine::registerSystems() {
|
|||
|
||||
sysDefs[DIV_SYSTEM_SWAN]=new DivSysDef(
|
||||
"WonderSwan", NULL, 0x96, 0, 4, false, true, 0x171, false, 1U<<DIV_SAMPLE_DEPTH_8BIT,
|
||||
"developed by the same team under the Game Boy and the Virtual Boy...",
|
||||
"developed by the makers of the Game Boy and the Virtual Boy...",
|
||||
{"Wave", "Wave/PCM", "Wave", "Wave/Noise"},
|
||||
{"CH1", "CH2", "CH3", "CH4"},
|
||||
{DIV_CH_WAVE, DIV_CH_PCM, DIV_CH_WAVE, DIV_CH_NOISE},
|
||||
|
|
204
src/engine/workPool.cpp
Normal file
204
src/engine/workPool.cpp
Normal file
|
@ -0,0 +1,204 @@
|
|||
/**
|
||||
* 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"
|
||||
#include "../ta-log.h"
|
||||
#include <thread>
|
||||
|
||||
void* _workThread(void* inst) {
|
||||
((DivWorkThread*)inst)->run();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void DivWorkThread::run() {
|
||||
//std::unique_lock<std::mutex> 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<void> future=notify.get_future();
|
||||
future.wait();
|
||||
lock.lock();
|
||||
notify=std::promise<void>();
|
||||
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; tryCount++) {
|
||||
if (pos>=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<count; i++) {
|
||||
if (workThreads[i].busy()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DivWorkPool::wait() {
|
||||
if (!threaded) return;
|
||||
|
||||
if (busyCount==0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::future<void> future=notify.get_future();
|
||||
|
||||
// start running
|
||||
for (unsigned int i=0; i<count; i++) {
|
||||
if (!workThreads[i].promiseAlreadySet && !workThreads[i].tasks.empty()) {
|
||||
try {
|
||||
workThreads[i].lock.lock();
|
||||
workThreads[i].promiseAlreadySet=true;
|
||||
workThreads[i].notify.set_value();
|
||||
workThreads[i].lock.unlock();
|
||||
} catch (std::exception& e) {
|
||||
logE("ERROR IN THREAD SYNC! %s",e.what());
|
||||
abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
//std::this_thread::yield();
|
||||
|
||||
// wait
|
||||
future.wait();
|
||||
|
||||
notify=std::promise<void>();
|
||||
|
||||
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<count; i++) {
|
||||
if (!workThreads[i].init(this)) {
|
||||
count=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (count<=0) {
|
||||
logE("DivWorkPool: couldn't start any threads! falling back to non-threaded mode.");
|
||||
delete[] workThreads;
|
||||
threaded=false;
|
||||
workThreads=NULL;
|
||||
}
|
||||
} else {
|
||||
workThreads=NULL;
|
||||
}
|
||||
}
|
||||
|
||||
DivWorkPool::~DivWorkPool() {
|
||||
if (threaded) {
|
||||
for (unsigned int i=0; i<count; i++) {
|
||||
workThreads[i].finish();
|
||||
}
|
||||
delete[] workThreads;
|
||||
}
|
||||
}
|
101
src/engine/workPool.h
Normal file
101
src/engine/workPool.h
Normal file
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* 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 <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
|
||||
#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<void> notify;
|
||||
FixedQueue<DivPendingTask,32> tasks;
|
||||
std::atomic<bool> 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<void> notify;
|
||||
std::atomic<int> 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
|
|
@ -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",
|
||||
|
|
|
@ -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<int> 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; i<chans; i++) {
|
||||
DivDispatchOscBuffer* buf=e->getOscBuffer(i);
|
||||
if (buf!=NULL && e->curSubSong->chanShow[i]) {
|
||||
|
@ -376,6 +383,148 @@ void FurnaceGUI::drawChanOsc() {
|
|||
}
|
||||
}
|
||||
|
||||
// process
|
||||
for (size_t i=0; i<oscBufs.size(); i++) {
|
||||
ChanOscStatus* fft_=oscFFTs[i];
|
||||
|
||||
fft_->relatedBuf=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; j<FURNACE_FFT_SIZE; j++) {
|
||||
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));
|
||||
}
|
||||
|
||||
// only proceed if not quiet
|
||||
if (fft->loudEnough) {
|
||||
fftw_execute(fft->plan);
|
||||
|
||||
// auto-correlation and second FFT
|
||||
for (int j=0; j<FURNACE_FFT_SIZE; j++) {
|
||||
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;
|
||||
}
|
||||
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]<waveLenCandL) {
|
||||
waveLenCandL=fft->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; k<fft->waveLen; 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; i<oscBufs.size(); i++) {
|
||||
if (i%chanOscCols==0) ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
|
@ -409,20 +559,6 @@ void FurnaceGUI::drawChanOsc() {
|
|||
ImVec2 size=ImGui::GetContentRegionAvail();
|
||||
size.y=availY/rows;
|
||||
|
||||
if (centerSettingReset) {
|
||||
buf->readNeedle=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; i<precision; i++) {
|
||||
float x=(float)i/(float)precision;
|
||||
waveform[i]=ImLerp(inRect.Min,inRect.Max,ImVec2(x,0.5f));
|
||||
for (unsigned short j=0; j<precision; j++) {
|
||||
float x=(float)j/(float)precision;
|
||||
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;
|
||||
unsigned short needlePos=buf->needle;
|
||||
//unsigned short needlePosOrig=needlePos;
|
||||
for (int i=0; i<FURNACE_FFT_SIZE; i++) {
|
||||
fft->inBuf[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; j++) {
|
||||
float x=(float)j/(float)precision;
|
||||
float y;
|
||||
if (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; j<precision; j++) {
|
||||
float y=(float)buf->data[(unsigned short)(fft->needle+(j*displaySize/precision))]/32768.0f;
|
||||
if (minLevel>y) minLevel=y;
|
||||
if (maxLevel<y) maxLevel=y;
|
||||
}
|
||||
dcOff=(minLevel+maxLevel)*0.5f;
|
||||
for (unsigned short j=0; j<precision; j++) {
|
||||
float x=(float)j/(float)precision;
|
||||
float y=(float)buf->data[(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; i<precision; i++) {
|
||||
float y=(float)buf->data[(unsigned short)(needlePos+(i*displaySize/precision))]/32768.0f;
|
||||
if (minLevel>y) minLevel=y;
|
||||
if (maxLevel<y) maxLevel=y;
|
||||
}
|
||||
dcOff=(minLevel+maxLevel)*0.5f;
|
||||
for (unsigned short i=0; i<precision; i++) {
|
||||
float x=(float)i/(float)precision;
|
||||
float y=(float)buf->data[(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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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; i<e->song.systemLen; i++) {
|
||||
DivSystem system=e->song.system[i];
|
||||
if (e->getChannelCount(system)>0) {
|
||||
|
|
|
@ -76,7 +76,38 @@ void _nfdThread(const NFDState state, std::atomic<bool>* ok, std::vector<String>
|
|||
}
|
||||
#endif
|
||||
|
||||
bool FurnaceGUIFileDialog::openLoad(String header, std::vector<String> filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback, bool allowMultiple) {
|
||||
void FurnaceGUIFileDialog::convertFilterList(std::vector<String>& filter) {
|
||||
memset(noSysFilter,0,4096);
|
||||
|
||||
String result;
|
||||
|
||||
for (size_t i=0; (i+1)<filter.size(); i+=2) {
|
||||
String label=filter[i];
|
||||
String ext;
|
||||
|
||||
if (filter[i+1]=="*") {
|
||||
ext=".*";
|
||||
} else for (char i: filter[i+1]) {
|
||||
switch (i) {
|
||||
case '*':
|
||||
break;
|
||||
case ' ':
|
||||
ext+=',';
|
||||
break;
|
||||
default:
|
||||
ext+=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.empty()) result+=',';
|
||||
result+=fmt::sprintf("%s{%s}",label,ext);
|
||||
}
|
||||
|
||||
strncpy(noSysFilter,result.c_str(),4095);
|
||||
}
|
||||
|
||||
bool FurnaceGUIFileDialog::openLoad(String header, std::vector<String> 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<String> 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<String> filter, c
|
|||
return true;
|
||||
}
|
||||
|
||||
bool FurnaceGUIFileDialog::openSave(String header, std::vector<String> filter, const char* noSysFilter, String path, double dpiScale) {
|
||||
bool FurnaceGUIFileDialog::openSave(String header, std::vector<String> filter, String path, double dpiScale) {
|
||||
if (opened) return false;
|
||||
|
||||
#ifdef ANDROID
|
||||
|
@ -233,6 +266,8 @@ bool FurnaceGUIFileDialog::openSave(String header, std::vector<String> filter, c
|
|||
} else {
|
||||
hasError=false;
|
||||
|
||||
convertFilterList(filter);
|
||||
|
||||
ImGuiFileDialog::Instance()->singleClickSel=false;
|
||||
ImGuiFileDialog::Instance()->DpiScale=dpiScale;
|
||||
ImGuiFileDialog::Instance()->mobileMode=mobileUI;
|
||||
|
|
|
@ -31,6 +31,7 @@ class FurnaceGUIFileDialog {
|
|||
bool opened;
|
||||
bool saving;
|
||||
bool hasError;
|
||||
char noSysFilter[4096];
|
||||
String curPath;
|
||||
std::vector<String> fileName;
|
||||
#ifdef USE_NFD
|
||||
|
@ -46,10 +47,12 @@ class FurnaceGUIFileDialog {
|
|||
pfd::open_file* dialogO;
|
||||
pfd::save_file* dialogS;
|
||||
#endif
|
||||
|
||||
void convertFilterList(std::vector<String>& filter);
|
||||
public:
|
||||
bool mobileUI;
|
||||
bool openLoad(String header, std::vector<String> filter, const char* noSysFilter, String path, double dpiScale, FileDialogSelectCallback clickCallback=NULL, bool allowMultiple=false);
|
||||
bool openSave(String header, std::vector<String> filter, const char* noSysFilter, String path, double dpiScale);
|
||||
bool openLoad(String header, std::vector<String> filter, String path, double dpiScale, FileDialogSelectCallback clickCallback=NULL, bool allowMultiple=false);
|
||||
bool openSave(String header, std::vector<String> filter, String path, double dpiScale);
|
||||
bool accepted();
|
||||
void close();
|
||||
bool render(const ImVec2& min, const ImVec2& max);
|
||||
|
|
|
@ -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]<<i;
|
||||
checker1+=ins->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; i<DIV_MAX_CHANS; i++) {
|
||||
chanOscChan[i].pitch=0.0f;
|
||||
}
|
||||
memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float));
|
||||
e->walkSong(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; i<DIV_MAX_CHANS; i++) {
|
||||
chanOscChan[i].pitch=0.0f;
|
||||
}
|
||||
memset(chanOscBright,0,DIV_MAX_CHANS*sizeof(float));
|
||||
|
||||
e->synchronized([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; i<DIV_MAX_CHANS; i++) {
|
||||
chanOscChan[i].pitch=0.0f;
|
||||
}
|
||||
memset(chanOscBright,0,sizeof(float)*DIV_MAX_CHANS);
|
||||
memset(lastCorrPos,0,sizeof(short)*DIV_MAX_CHANS);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
@ -1334,7 +1335,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;
|
||||
|
@ -1346,6 +1347,7 @@ class FurnaceGUI {
|
|||
int mobileEditPage;
|
||||
int wheelCalmDown;
|
||||
int shallDetectScale;
|
||||
int cpuCores;
|
||||
float mobileMenuPos, autoButtonSize, mobileEditAnim;
|
||||
ImVec2 mobileEditButtonPos, mobileEditButtonSize;
|
||||
const int* curSysSection;
|
||||
|
@ -1515,6 +1517,7 @@ class FurnaceGUI {
|
|||
int noMultiSystem;
|
||||
int oldMacroVSlider;
|
||||
int displayAllInsTypes;
|
||||
int displayPartial;
|
||||
int noteCellSpacing;
|
||||
int insCellSpacing;
|
||||
int volCellSpacing;
|
||||
|
@ -1572,6 +1575,9 @@ class FurnaceGUI {
|
|||
int insIconsStyle;
|
||||
int classicChipOptions;
|
||||
int wasapiEx;
|
||||
int chanOscThreads;
|
||||
int renderPoolThreads;
|
||||
int showPool;
|
||||
unsigned int maxUndoSteps;
|
||||
String mainFontPath;
|
||||
String headFontPath;
|
||||
|
@ -1691,6 +1697,7 @@ class FurnaceGUI {
|
|||
noMultiSystem(0),
|
||||
oldMacroVSlider(0),
|
||||
displayAllInsTypes(0),
|
||||
displayPartial(0),
|
||||
noteCellSpacing(0),
|
||||
insCellSpacing(0),
|
||||
volCellSpacing(0),
|
||||
|
@ -1747,6 +1754,9 @@ class FurnaceGUI {
|
|||
insIconsStyle(1),
|
||||
classicChipOptions(0),
|
||||
wasapiEx(0),
|
||||
chanOscThreads(0),
|
||||
renderPoolThreads(0),
|
||||
showPool(0),
|
||||
maxUndoSteps(100),
|
||||
mainFontPath(""),
|
||||
headFontPath(""),
|
||||
|
@ -2047,27 +2057,46 @@ 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];
|
||||
float chanOscPitch[DIV_MAX_CHANS];
|
||||
float chanOscBright[DIV_MAX_CHANS];
|
||||
unsigned short lastNeedlePos[DIV_MAX_CHANS];
|
||||
unsigned short lastCorrPos[DIV_MAX_CHANS];
|
||||
struct ChanOscStatus {
|
||||
double* inBuf;
|
||||
fftw_complex* outBuf;
|
||||
double* corrBuf;
|
||||
DivDispatchOscBuffer* relatedBuf;
|
||||
size_t inBufPos;
|
||||
double inBufPosFrac;
|
||||
double waveLen;
|
||||
int waveLenBottom, waveLenTop, relatedCh;
|
||||
float pitch, windowSize;
|
||||
unsigned short needle;
|
||||
fftw_complex* outBuf;
|
||||
bool ready, loudEnough, waveCorr;
|
||||
fftw_plan plan;
|
||||
fftw_plan planI;
|
||||
ChanOscStatus():
|
||||
inBuf(NULL),
|
||||
outBuf(NULL),
|
||||
corrBuf(NULL),
|
||||
relatedBuf(NULL),
|
||||
inBufPos(0),
|
||||
inBufPosFrac(0.0f),
|
||||
waveLen(0.0),
|
||||
waveLenBottom(0),
|
||||
waveLenTop(0),
|
||||
relatedCh(0),
|
||||
pitch(0.0f),
|
||||
windowSize(1.0f),
|
||||
needle(0),
|
||||
outBuf(NULL),
|
||||
plan(NULL) {}
|
||||
ready(false),
|
||||
loudEnough(false),
|
||||
waveCorr(false),
|
||||
plan(NULL),
|
||||
planI(NULL) {}
|
||||
} chanOscChan[DIV_MAX_CHANS];
|
||||
|
||||
// visualizer
|
||||
|
|
24233
src/gui/introTune.cpp
24233
src/gui/introTune.cpp
File diff suppressed because it is too large
Load diff
|
@ -39,12 +39,14 @@ void FurnaceGUI::drawPatManager() {
|
|||
e->lockEngine([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();
|
||||
}
|
||||
|
|
|
@ -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+=" | ";
|
||||
}
|
||||
|
|
|
@ -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<midiMap.binds.size(); i++) {
|
||||
MIDIBind& bind=midiMap.binds[i];
|
||||
|
@ -1157,13 +1208,15 @@ void FurnaceGUI::drawSettings() {
|
|||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Button((learning==(int)i)?("waiting...##BLearn"):(ICON_FA_SQUARE_O "##BLearn"))) {
|
||||
pushToggleColors(learning==(int)i);
|
||||
if (ImGui::Button((learning==(int)i)?("waiting...##BLearn"):("Learn##BLearn"))) {
|
||||
if (learning==(int)i) {
|
||||
learning=-1;
|
||||
} else {
|
||||
learning=i;
|
||||
}
|
||||
}
|
||||
popToggleColors();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (ImGui::Button(ICON_FA_TIMES "##BRemove")) {
|
||||
|
@ -3011,6 +3064,8 @@ void FurnaceGUI::drawSettings() {
|
|||
// "Debug" - toggles mobile UI
|
||||
// "Nice Amiga cover of the song!" - enables hidden systems (YMU759/SoundUnit/Dummy)
|
||||
// "42 63" - enables all instrument types
|
||||
// "4-bit FDS" - enables partial pitch linearity option
|
||||
// "Power of the Chip" - enables options for multi-threaded audio
|
||||
// "????" - enables stuff
|
||||
CONFIG_SECTION("Cheat Codes") {
|
||||
// SUBSECTION ENTER CODE:
|
||||
|
@ -3041,6 +3096,14 @@ void FurnaceGUI::drawSettings() {
|
|||
mmlString[30]="enabled all instrument types";
|
||||
settings.displayAllInsTypes=!settings.displayAllInsTypes;
|
||||
}
|
||||
if (checker==0x3f88abcc && checker1==0xf4a6) {
|
||||
mmlString[30]="OK, if I bring your Partial pitch linearity will you stop bothering me?";
|
||||
settings.displayPartial=1;
|
||||
}
|
||||
if (checker==0x8537719f && checker1==0x17a1f34) {
|
||||
mmlString[30]="unlocked audio multi-threading options!";
|
||||
settings.showPool=1;
|
||||
}
|
||||
|
||||
mmlString[31]="";
|
||||
}
|
||||
|
@ -3198,6 +3261,7 @@ void FurnaceGUI::syncSettings() {
|
|||
settings.noMultiSystem=e->getConfInt("noMultiSystem",0);
|
||||
settings.oldMacroVSlider=e->getConfInt("oldMacroVSlider",0);
|
||||
settings.displayAllInsTypes=e->getConfInt("displayAllInsTypes",0);
|
||||
settings.displayPartial=e->getConfInt("displayPartial",0);
|
||||
settings.noteCellSpacing=e->getConfInt("noteCellSpacing",0);
|
||||
settings.insCellSpacing=e->getConfInt("insCellSpacing",0);
|
||||
settings.volCellSpacing=e->getConfInt("volCellSpacing",0);
|
||||
|
@ -3262,6 +3326,9 @@ 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);
|
||||
settings.renderPoolThreads=e->getConfInt("renderPoolThreads",0);
|
||||
settings.showPool=e->getConfInt("showPool",0);
|
||||
|
||||
clampSetting(settings.mainFontSize,2,96);
|
||||
clampSetting(settings.headFontSize,2,96);
|
||||
|
@ -3355,6 +3422,7 @@ void FurnaceGUI::syncSettings() {
|
|||
clampSetting(settings.noMultiSystem,0,1);
|
||||
clampSetting(settings.oldMacroVSlider,0,1);
|
||||
clampSetting(settings.displayAllInsTypes,0,1);
|
||||
clampSetting(settings.displayPartial,0,1);
|
||||
clampSetting(settings.noteCellSpacing,0,32);
|
||||
clampSetting(settings.insCellSpacing,0,32);
|
||||
clampSetting(settings.volCellSpacing,0,32);
|
||||
|
@ -3410,6 +3478,9 @@ void FurnaceGUI::syncSettings() {
|
|||
clampSetting(settings.insIconsStyle,0,2);
|
||||
clampSetting(settings.classicChipOptions,0,1);
|
||||
clampSetting(settings.wasapiEx,0,1);
|
||||
clampSetting(settings.chanOscThreads,0,256);
|
||||
clampSetting(settings.renderPoolThreads,0,DIV_MAX_CHIPS);
|
||||
clampSetting(settings.showPool,0,1);
|
||||
|
||||
if (settings.exportLoops<0.0) settings.exportLoops=0.0;
|
||||
if (settings.exportFadeOut<0.0) settings.exportFadeOut=0.0;
|
||||
|
@ -3602,6 +3673,7 @@ void FurnaceGUI::commitSettings() {
|
|||
e->setConf("noMultiSystem",settings.noMultiSystem);
|
||||
e->setConf("oldMacroVSlider",settings.oldMacroVSlider);
|
||||
e->setConf("displayAllInsTypes",settings.displayAllInsTypes);
|
||||
e->setConf("displayPartial",settings.displayPartial);
|
||||
e->setConf("noteCellSpacing",settings.noteCellSpacing);
|
||||
e->setConf("insCellSpacing",settings.insCellSpacing);
|
||||
e->setConf("volCellSpacing",settings.volCellSpacing);
|
||||
|
@ -3665,6 +3737,9 @@ void FurnaceGUI::commitSettings() {
|
|||
e->setConf("insIconsStyle",settings.insIconsStyle);
|
||||
e->setConf("classicChipOptions",settings.classicChipOptions);
|
||||
e->setConf("wasapiEx",settings.wasapiEx);
|
||||
e->setConf("chanOscThreads",settings.chanOscThreads);
|
||||
e->setConf("renderPoolThreads",settings.renderPoolThreads);
|
||||
e->setConf("showPool",settings.showPool);
|
||||
|
||||
// colors
|
||||
for (int i=0; i<GUI_COLOR_MAX; i++) {
|
||||
|
@ -4183,6 +4258,12 @@ void FurnaceGUI::applyUISettings(bool updateFonts) {
|
|||
}
|
||||
}
|
||||
|
||||
// chan osc work pool
|
||||
if (chanOscWorkPool!=NULL) {
|
||||
delete chanOscWorkPool;
|
||||
chanOscWorkPool=NULL;
|
||||
}
|
||||
|
||||
// colors
|
||||
if (updateFonts) {
|
||||
for (int i=0; i<GUI_COLOR_MAX; i++) {
|
||||
|
|
|
@ -44,20 +44,6 @@ void FurnaceGUI::drawSongInfo(bool asChild) {
|
|||
if (ImGui::InputText("##Name",&e->song.name,ImGuiInputTextFlags_UndoRedo)) { MARK_MODIFIED
|
||||
updateWindowTitle();
|
||||
}
|
||||
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]<<i;
|
||||
checker1+=ins->name[i];
|
||||
checker=(checker>>1|(((checker)^(checker>>2)^(checker>>3)^(checker>>5))&1)<<31);
|
||||
checker1<<=1;
|
||||
}
|
||||
if (checker==0x5ec4497d && checker1==0x6347ee) nonLatchNibble=true;
|
||||
}
|
||||
}
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
|
|
Loading…
Reference in a new issue