Merge branch 'opz-per-operator-arp-and-pitch-macros-and-more' of https://github.com/LTVA1/furnace into opz-per-operator-arp-and-pitch-macros-and-more

This commit is contained in:
LTVA1 2024-08-25 17:30:49 +03:00
commit 9a811c87a1
31 changed files with 305 additions and 314 deletions

View file

@ -61,7 +61,7 @@ if you need to use more samples, you may change the sample bank using effect `EB
due to limitations in some of those sound chips, some restrictions exist:
- Amiga: maximum frequency is 31469Hz, but anything over 28867 will sound glitchy on hardware. sample lengths and loop will be set to an even number, and your sample can't be longer than 131070.
- NES: if on DPCM mode, only a limited selection of frequencies is available, and loop position isn't supported (only entire sample).
- NES: if on DPCM mode, only a limited selection of frequencies is available.
- SegaPCM: your sample can't be longer than 65535, and the maximum frequency is 31.25KHz.
- QSound: your sample can't be longer than 65535, and the loop length shall not be greater than 32767.
- ADPCM-A: no looping supported. your samples will play at around 18.518KHz.

View file

@ -1,65 +0,0 @@
# Sega Genesis/Mega Drive
a video game console that showed itself as the first true rival to Nintendo's video game market near-monopoly in the US during the '80s.
this console is powered by two sound chips: the [Yamaha YM2612](ym2612.md) and [a derivative of the SN76489](sms.md).
## effects
- `10xy`: **set LFO parameters.**
- `x` toggles the LFO.
- `y` sets its speed.
- `11xx`: **set feedback of channel.**
- `12xx`: **set operator 1 level.**
- `13xx`: **set operator 2 level.**
- `14xx`: **set operator 3 level.**
- `15xx`: **set operator 4 level.**
- `16xy`: **set multiplier of operator.**
- `x` is the operator (1-4).
- `y` is the new MULT value..
- `17xx`: **enable PCM channel.**
- this only works on channel 6.
- _this effect is here for compatibility reasons!_ it is otherwise recommended to use Sample type instruments (which automatically enable PCM mode when used).
- `18xx`: **toggle extended channel 3 mode.**
- `0` disables it and `1` enables it.
- only in extended channel 3 chip.
- `19xx`: **set attack of all operators.**
- `1Axx`: **set attack of operator 1.**
- `1Bxx`: **set attack of operator 2.**
- `1Cxx`: **set attack of operator 3.**
- `1Dxx`: **set attack of operator 4.**
- `20xy`: **set PSG noise mode.**
- `x` controls whether to inherit frequency from PSG channel 3.
- `0`: use one of 3 preset frequencies (`C`: A-2; `C#`: A-3; `D`: A-4).
- `1`: use frequency of PSG channel 3.
- `y` controls whether to select noise or thin pulse.
- `0`: thin pulse.
- `1`: noise.
## system modes
## extended channel 3
in ExtCh mode, channel 3 is split into one column for each of its four operators. feedback and LFO levels are shared. the frequency of each operator may be controlled independently with notes and effects. this can be used for more polyphony or more complex sounds.
all four operators are still combined according to the algorithm in use. for example, algorithm 7 acts as four independent sine waves. algorithm 4 acts as two independent 2-op sounds. even with algorithm 0, placing a note in any operator triggers that operator alone.
## CSM
CSM is short for "Composite Sinusoidal Modeling". CSM works by sending key-on and key-off commands to channel 3 at a specific frequency, controlled by the added "CSM Timer" channel. this can be used to create vocal formants (speech synthesis!) or other complex effects.
CSM is beyond the scope of this documentation. for more information, see this [brief SSG-EG and CSM video tutorial](https://www.youtube.com/watch?v=IKOR0TUlnWU).
## DualPCM
[info here.](ym2612.md)
## Sega CD
this isn't a mode so much as a chip configuration. it adds the [Ricoh RF5C68](ricoh.md) found in the Sega CD add-on, providing 8 channels of PCM.
## chip config
see [YM2612](ym2612.md) and [SN76489](sms.md) for respective configuration.

View file

@ -68,6 +68,9 @@ in most arcade boards the chip was used in combination with a PCM chip, like [Se
- `5Dxx`: **set D2R/SR of operator 2.**
- `5Exx`: **set D2R/SR of operator 3.**
- `5Fxx`: **set D2R/SR of operator 4.**
- `60xx`: **set operator mask.**
- `xx` goes from `0` to `F`. it is a bitfield.
- each bit corresponds to an operator.
## info

View file

@ -99,6 +99,9 @@ several variants of this chip were released as well, with more features.
- `5Dxx`: **set D2R/SR of operator 2.**
- `5Exx`: **set D2R/SR of operator 3.**
- `5Fxx`: **set D2R/SR of operator 4.**
- `60xx`: **set operator mask.**
- `xx` goes from `0` to `F`. it is a bitfield.
- each bit corresponds to an operator.
## extended channel 3

View file

@ -99,6 +99,9 @@ the YM2610 (OPNB) and YM2610B chips are very similar to this one, but the built-
- `5Dxx`: **set D2R/SR of operator 2.**
- `5Exx`: **set D2R/SR of operator 3.**
- `5Fxx`: **set D2R/SR of operator 4.**
- `60xx`: **set operator mask.**
- `xx` goes from `0` to `F`. it is a bitfield.
- each bit corresponds to an operator.
## extended channel 3

View file

@ -97,6 +97,9 @@ its soundchip is a 4-in-1: 4ch 4-op FM, YM2149 (AY-3-8910 clone) and [2 differen
- `5Dxx`: **set D2R/SR of operator 2.**
- `5Exx`: **set D2R/SR of operator 3.**
- `5Fxx`: **set D2R/SR of operator 4.**
- `60xx`: **set operator mask.**
- `xx` goes from `0` to `F`. it is a bitfield.
- each bit corresponds to an operator.
## extended channel 2

View file

@ -96,6 +96,9 @@ it is backward compatible with the original chip.
- `5Dxx`: **set D2R/SR of operator 2.**
- `5Exx`: **set D2R/SR of operator 3.**
- `5Fxx`: **set D2R/SR of operator 4.**
- `60xx`: **set operator mask.**
- `xx` goes from `0` to `F`. it is a bitfield.
- each bit corresponds to an operator.
## extended channel 3

View file

@ -82,6 +82,9 @@ thanks to the Z80 sound CPU, DualPCM can play two samples at once! this mode spl
- `5Dxx`: **set D2R/SR of operator 2.**
- `5Exx`: **set D2R/SR of operator 3.**
- `5Fxx`: **set D2R/SR of operator 4.**
- `60xx`: **set operator mask.**
- `xx` goes from `0` to `F`. it is a bitfield.
- each bit corresponds to an operator.
## info

View file

@ -260,6 +260,7 @@ size | description
| - 0xe4: µPD1771C - 1 channel (UNAVAILABLE)
| - 0xf0: SID2 - 3 channels
| - 0xf1: 5E01 - 5 channels
| - 0xf5: SID3 - 7 channels
| - 0xfc: Pong - 1 channel
| - 0xfd: Dummy System - 8 channels
| - 0xfe: reserved for development

View file

@ -266,6 +266,8 @@ enum DivDispatchCmds {
DIV_CMD_FDS_MOD_AUTO,
DIV_CMD_FM_OPMASK, // (mask)
DIV_CMD_MAX
};

View file

@ -203,11 +203,9 @@ void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) {
}
}
void DivEngine::findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, int& length)
{
if (curSubSong!=NULL)
{
curSubSong->findLength(loopOrder, loopRow, fadeoutLen, rowsForFadeout, hasFFxx, orders, song.grooves, length, chans, song.jumpTreatment, song.ignoreJumpAtEnd);
void DivEngine::findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, int& length) {
if (curSubSong!=NULL) {
curSubSong->findLength(loopOrder,loopRow,fadeoutLen,rowsForFadeout,hasFFxx,orders,song.grooves,length,chans,song.jumpTreatment,song.ignoreJumpAtEnd);
}
}

View file

@ -475,7 +475,7 @@ class DivEngine {
int midiOutTimeRate;
float midiVolExp;
int softLockCount;
int subticks, ticks, curRow, curOrder, prevRow, prevOrder, remainingLoops, totalLoops, lastLoopPos, exportLoopCount, curExportChan /*for per-channel export progress*/, nextSpeed, elapsedBars, elapsedBeats, curSpeed;
int subticks, ticks, curRow, curOrder, prevRow, prevOrder, remainingLoops, totalLoops, lastLoopPos, exportLoopCount, curExportChan, nextSpeed, elapsedBars, elapsedBeats, curSpeed;
size_t curSubSongIndex;
size_t bufferPos;
double divider;
@ -1015,19 +1015,19 @@ class DivEngine {
// get how many loops is left
void getLoopsLeft(int& loops);
//get how many loops in total export needs to do
// get how many loops in total export needs to do
void getTotalLoops(int& loops);
// get current position in song
void getCurSongPos(int& row, int& order);
//get how many files export needs to create
// get how many files export needs to create
void getTotalAudioFiles(int& files);
//get which file is processed right now (progress for e.g. per-channel export)
// get which file is processed right now (progress for e.g. per-channel export)
void getCurFileIndex(int& file);
//get fadeout state
// get fadeout state
bool getIsFadingOut();
// add instrument

View file

@ -761,6 +761,12 @@ int DivPlatformArcade::dispatch(DivCommand c) {
immWrite(0x19,0x80|pmDepth);
break;
}
case DIV_CMD_FM_OPMASK:
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1463,6 +1463,13 @@ int DivPlatformGenesis::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
if (c.chan>=6) break;
chan[c.chan].hardReset=c.value;

View file

@ -28,7 +28,7 @@ struct _nla_table nla_table;
#define CHIP_DIVIDER 16
#define rWrite(a,v) if (!skipRegisterWrites) {doWrite(a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} }
#define rWrite(a,v) if (!skipRegisterWrites) {writes.push(QueuedWrite((a),v)); if (dumpWrites) {addWrite((a),v);} }
const char* regCheatSheetNES[]={
"S0Volume", "4000",
@ -86,10 +86,10 @@ void DivPlatformNES::doWrite(unsigned short addr, unsigned char data) {
unsigned char next=((unsigned char)s->data8[dacPos]+0x80)>>1; \
if (dacAntiClickOn && dacAntiClick<next) { \
dacAntiClick+=8; \
rWrite(0x4011,dacAntiClick); \
doWrite(0x4011,dacAntiClick); \
} else { \
dacAntiClickOn=false; \
rWrite(0x4011,next); \
doWrite(0x4011,next); \
} \
} \
dacPos++; \
@ -108,6 +108,13 @@ void DivPlatformNES::doWrite(unsigned short addr, unsigned char data) {
void DivPlatformNES::acquire_puNES(short** buf, size_t len) {
for (size_t i=0; i<len; i++) {
doPCM;
if (!writes.empty()) {
QueuedWrite w=writes.front();
doWrite(w.addr,w.val);
regPool[w.addr&0x1f]=w.val;
writes.pop();
}
apu_tick(nes,NULL);
nes->apu.odd_cycle=!nes->apu.odd_cycle;
@ -134,6 +141,13 @@ void DivPlatformNES::acquire_NSFPlay(short** buf, size_t len) {
int out2[2];
for (size_t i=0; i<len; i++) {
doPCM;
if (!writes.empty()) {
QueuedWrite w=writes.front();
doWrite(w.addr,w.val);
regPool[w.addr&0x1f]=w.val;
writes.pop();
}
nes1_NP->Tick(8);
nes2_NP->TickFrameSequence(8);
@ -297,7 +311,7 @@ void DivPlatformNES::tick(bool sysTick) {
if (chan[i].sweepChanged) {
chan[i].sweepChanged=false;
if (i==0) {
//rWrite(16+i*5,chan[i].sweep);
// rWrite(16+i*5,chan[i].sweep);
}
}
if (i<3) if (chan[i].std.phaseReset.had) {
@ -338,7 +352,7 @@ void DivPlatformNES::tick(bool sysTick) {
}
}
if (chan[i].keyOff) {
//rWrite(16+i*5+2,8);
// rWrite(16+i*5+2,8);
if (i==2) { // triangle
rWrite(0x4000+i*4,0x00);
} else {
@ -400,6 +414,34 @@ void DivPlatformNES::tick(bool sysTick) {
logV("switching bank to %d",dpcmBank);
if (dumpWrites) addWrite(0xffff0004,dpcmBank);
}
// sample custom loop point...
DivSample* lsamp=parent->getSample(dacSample);
// how it works:
// when the initial sample info is written (see above) and playback is launched,
// the parameters (start point in memory and length) are locked until sample end
// is reached.
// thus, if we write new data after just several APU clock cycles, it will be used only when
// sample finishes one full loop.
// thus we can write sample's loop point as "start address" and sample's looped part length
// as "full sample length".
// APU will play full sample once and then repeatedly cycle through the looped part.
// sources:
// https://www.nesdev.org/wiki/APU_DMC
// https://www.youtube.com/watch?v=vB4P8x2Am6Y
if (lsamp->loopEnd>lsamp->loopStart && goingToLoop) {
int loopStartAddr=(sampleOffDPCM[dacSample]+lsamp->loopStart)>>3;
int loopLen=(lsamp->loopEnd-lsamp->loopStart)>>3;
rWrite(0x4012,(loopStartAddr>>6)&0xff);
rWrite(0x4013,(loopLen>>4)&0xff);
}
}
} else {
if (nextDPCMFreq>=0) {
@ -790,6 +832,7 @@ float DivPlatformNES::getPostAmp() {
}
void DivPlatformNES::reset() {
while (!writes.empty()) writes.pop();
for (int i=0; i<5; i++) {
chan[i]=DivPlatformNES::Channel();
chan[i].std.setEngine(parent);

View file

@ -24,6 +24,7 @@
#include "sound/nes_nsfplay/nes_apu.h"
#include "sound/nes_nsfplay/5e01_apu.h"
#include "../../fixedQueue.h"
class DivPlatformNES: public DivDispatch {
struct Channel: public SharedChannel<signed char> {
@ -44,6 +45,13 @@ class DivPlatformNES: public DivDispatch {
Channel chan[5];
DivDispatchOscBuffer* oscBuf[5];
bool isMuted[5];
struct QueuedWrite {
unsigned short addr;
unsigned char val;
QueuedWrite(): addr(0), val(0) {}
QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v) {}
};
FixedQueue<QueuedWrite,128> writes;
int dacPeriod, dacRate, dpcmPos;
unsigned int dacPos, dacAntiClick;
int dacSample;

View file

@ -135,19 +135,12 @@ static short const gauss [512] =
1299,1300,1300,1301,1302,1302,1303,1303,1303,1304,1304,1304,1304,1304,1305,1305,
};
void SPC_DSP::setupInterpolation(bool interpolate)
{
for(int i = 0; i < voice_count; i++)
{
m.voices[i].interpolate = interpolate;
}
}
void SPC_DSP::setupInterpolation(bool interpolate){for(int i=0;i<voice_count;i++){m.voices[i].interpolate=interpolate;}}
inline int SPC_DSP::interpolate( voice_t const* v )
{
// Make pointers into gaussian based on fractional position between samples
if(v->interpolate)
{
if (v->interpolate) {
int offset = v->interp_pos >> 4 & 0xFF;
short const* fwd = gauss + 255 - offset;
short const* rev = gauss + offset; // mirror left half of gaussian
@ -163,9 +156,7 @@ inline int SPC_DSP::interpolate( voice_t const* v )
CLAMP16( out );
out &= ~1;
return out;
}
else
{
} else {
return v->buf [(v->interp_pos >> 12) + v->buf_pos]; //Furnace addition -- no interpolation
}
}

View file

@ -916,6 +916,9 @@ int DivPlatformTX81Z::dispatch(DivCommand c) {
immWrite(0x17,0x80|pmDepth);
break;
}
case DIV_CMD_FM_OPMASK:
// TODO: if OPZ supports op mask
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1004,6 +1004,13 @@ int DivPlatformYM2203::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1537,6 +1537,13 @@ int DivPlatformYM2608::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1507,6 +1507,13 @@ int DivPlatformYM2610::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1576,6 +1576,13 @@ int DivPlatformYM2610B::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -264,7 +264,9 @@ const char* cmdName[]={
"BIFURCATOR_STATE_LOAD",
"BIFURCATOR_PARAMETER",
"FDS_MOD_AUTO"
"FDS_MOD_AUTO",
"FM_OPMASK"
};
static_assert((sizeof(cmdName)/sizeof(void*))==DIV_CMD_MAX,"update cmdName!");

View file

@ -102,9 +102,8 @@ bool DivSubSong::walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int
return false;
}
double calcRowLenInSeconds(const DivGroovePattern& speeds, float hz, int vN, int vD, int timeBaseFromSong)
{
double hl=1; //count for 1 row
double calcRowLenInSeconds(const DivGroovePattern& speeds, float hz, int vN, int vD, int timeBaseFromSong) {
double hl=1; //count for 1 row
if (hl<=0.0) hl=4.0;
double timeBase=timeBaseFromSong+1;
double speedSum=0;
@ -115,25 +114,23 @@ double calcRowLenInSeconds(const DivGroovePattern& speeds, float hz, int vN, int
if (timeBase<1.0) timeBase=1.0;
if (speedSum<1.0) speedSum=1.0;
if (vD<1) vD=1;
//return (60.0 * hz / (timeBase * hl * speedSum)) * (double)vN / (double)vD;
return 1.0 / ((60.0*hz/(timeBase*hl*speedSum))*(double)vN/(double)vD / 60.0);
return 1.0/((60.0*hz/(timeBase*hl*speedSum))*(double)vN/(double)vD/60.0);
}
void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders_vec, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat)
{
length = 0;
hasFFxx = false;
rowsForFadeout = 0;
void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders_vec, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat) {
length=0;
hasFFxx=false;
rowsForFadeout=0;
float secondsPerThisRow = 0.0f;
float secondsPerThisRow=0.0f;
DivGroovePattern curSpeeds = speeds; //simulate that we are playing the song, track all speed/BPM/tempo/engine rate changes
short curVirtualTempoN = virtualTempoN;
short curVirtualTempoD = virtualTempoD;
float curHz = hz;
double curDivider = (double)timeBase;
DivGroovePattern curSpeeds=speeds; //simulate that we are playing the song, track all speed/BPM/tempo/engine rate changes
short curVirtualTempoN=virtualTempoN;
short curVirtualTempoD=virtualTempoD;
float curHz=hz;
double curDivider=(double)timeBase;
double curLen = 0.0; //how many seconds passed since the start of song
double curLen=0.0; //how many seconds passed since the start of song
int nextOrder=-1;
int nextRow=0;
@ -145,49 +142,39 @@ void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int&
if (firstPat>0) {
memset(wsWalked,255,32*firstPat);
}
for (int i=firstPat; i<ordersLen; i++)
{
bool jumped = false;
for (int i=firstPat; i<ordersLen; i++) {
bool jumped=false;
for (int j=0; j<chans; j++)
{
for (int j=0; j<chans; j++) {
subPat[j]=pat[j].getPattern(orders.ord[j][i],false);
}
if (i>lastSuspectedLoopEnd)
{
if (i>lastSuspectedLoopEnd) {
lastSuspectedLoopEnd=i;
}
for (int j=nextRow; j<patLen; j++)
{
for (int j=nextRow; j<patLen; j++) {
nextRow=0;
bool changingOrder=false;
bool jumpingOrder=false;
if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7)))
{
if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7))) {
return;
}
for (int k=0; k<chans; k++)
{
for (int l=0; l<pat[k].effectCols; l++)
{
for (int k=0; k<chans; k++) {
for (int l=0; l<pat[k].effectCols; l++) {
effectVal=subPat[k]->data[j][5+(l<<1)];
if (effectVal<0) effectVal=0;
if (subPat[k]->data[j][4+(l<<1)]==0xff)
{
hasFFxx = true;
if (subPat[k]->data[j][4+(l<<1)]==0xff) {
hasFFxx=true;
//FFxx makes YOU SHALL NOT PASS!!! move
orders_vec.push_back(j + 1); //order len
length += j + 1; //add length of order to song length
// FFxx makes YOU SHALL NOT PASS!!! move
orders_vec.push_back(j+1); // order len
length+=j+1; // add length of order to song length
return;
}
switch(subPat[k]->data[j][4+(l<<1)]) //track speed/BMP/Hz/tempo changes
{
case 0x09: // select groove pattern/speed 1
{
switch (subPat[k]->data[j][4+(l<<1)]) {
case 0x09: { // select groove pattern/speed 1
if (grooves.empty()) {
if (effectVal>0) curSpeeds.val[0]=effectVal;
} else {
@ -198,8 +185,7 @@ void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int&
}
break;
}
case 0x0f: // speed 1/speed 2
{
case 0x0f: { // speed 1/speed 2
if (curSpeeds.len==2 && grooves.empty()) {
if (effectVal>0) curSpeeds.val[1]=effectVal;
} else {
@ -207,68 +193,47 @@ void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int&
}
break;
}
case 0xfd: // virtual tempo num
{
case 0xfd: { // virtual tempo num
if (effectVal>0) curVirtualTempoN=effectVal;
break;
}
case 0xfe: // virtual tempo den
{
case 0xfe: { // virtual tempo den
if (effectVal>0) curVirtualTempoD=effectVal;
break;
}
case 0xf0: // set Hz by tempo (set bpm)
{
case 0xf0: { // set Hz by tempo (set bpm)
curDivider=(double)effectVal*2.0/5.0;
if (curDivider<1) curDivider=1;
//cycles=got.rate*pow(2,MASTER_CLOCK_PREC)/divider;
//clockDrift=0;
//subticks=0;
break;
}
default: break;
}
if (subPat[k]->data[j][4+(l<<1)]==0x0d)
{
if (jumpTreatment==2)
{
if ((i<ordersLen-1 || !ignoreJumpAtEnd))
{
if (subPat[k]->data[j][4+(l<<1)]==0x0d) {
if (jumpTreatment==2) {
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
}
}
else if (jumpTreatment==1)
{
if (nextOrder==-1 && (i<ordersLen-1 || !ignoreJumpAtEnd))
{
} else if (jumpTreatment==1) {
if (nextOrder==-1 && (i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
}
}
else
{
if ((i<ordersLen-1 || !ignoreJumpAtEnd))
{
if (!changingOrder)
{
} else {
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
if (!changingOrder) {
nextOrder=i+1;
}
jumpingOrder=true;
nextRow=effectVal;
}
}
}
else if (subPat[k]->data[j][4+(l<<1)]==0x0b)
{
if (nextOrder==-1 || jumpTreatment==0)
{
} else if (subPat[k]->data[j][4+(l<<1)]==0x0b) {
if (nextOrder==-1 || jumpTreatment==0) {
nextOrder=effectVal;
if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder)
{
if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder) {
nextRow=0;
}
changingOrder=true;
@ -277,32 +242,29 @@ void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int&
}
}
if(i > loopOrder || (i == loopOrder && j > loopRow))
{
if(curLen <= fadeoutLen && fadeoutLen > 0.0) //we count each row fadeout lasts. When our time is greater than fadeout length we successfully counted the number of fadeout rows
{
secondsPerThisRow = calcRowLenInSeconds(speeds, curHz, curVirtualTempoN, curVirtualTempoD, curDivider);
curLen += secondsPerThisRow;
if (i>loopOrder || (i==loopOrder && j>loopRow)) {
// we count each row fadeout lasts. When our time is greater than fadeout length we successfully counted the number of fadeout rows
if (curLen<=fadeoutLen && fadeoutLen>0.0) {
secondsPerThisRow=calcRowLenInSeconds(speeds,curHz,curVirtualTempoN,curVirtualTempoD,curDivider);
curLen+=secondsPerThisRow;
rowsForFadeout++;
}
}
wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7);
if (nextOrder!=-1)
{
if (nextOrder!=-1) {
i=nextOrder-1;
orders_vec.push_back(j + 1); //order len
length += j + 1; //add length of order to song length
jumped = true;
orders_vec.push_back(j+1); // order len
length+=j+1; // add length of order to song length
jumped=true;
nextOrder=-1;
break;
}
}
if(!jumped) //if no jump occured we add full pattern length
{
orders_vec.push_back(patLen); //order len
length += patLen; //add length of order to song length
if (!jumped) { // if no jump occured we add full pattern length
orders_vec.push_back(patLen); // order len
length+=patLen; // add length of order to song length
}
}
}

View file

@ -186,7 +186,7 @@ struct DivSubSong {
bool walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0);
/**
* find song length in rows (up to specified loop point). Also find length of every row
* find song length in rows (up to specified loop point).
*/
void findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0);

View file

@ -504,6 +504,7 @@ void DivEngine::registerSystems() {
{0x5d, {DIV_CMD_FM_D2R, _("5Dxx: Set decay 2 of operator 2 (0 to 1F)"), constVal<1>, effectValAnd<31>}},
{0x5e, {DIV_CMD_FM_D2R, _("5Exx: Set decay 2 of operator 3 (0 to 1F)"), constVal<2>, effectValAnd<31>}},
{0x5f, {DIV_CMD_FM_D2R, _("5Fxx: Set decay 2 of operator 4 (0 to 1F)"), constVal<3>, effectValAnd<31>}},
{0x60, {DIV_CMD_FM_OPMASK, _("60xx: Set operator mask (bits 0-3)")}},
};
EffectHandlerMap fmOPMPostEffectHandlerMap(fmOPNPostEffectHandlerMap);
@ -514,6 +515,7 @@ void DivEngine::registerSystems() {
{0x1e, {DIV_CMD_FM_AM_DEPTH, _("1Exx: Set AM depth (0 to 7F)"), effectValAnd<127>}},
{0x1f, {DIV_CMD_FM_PM_DEPTH, _("1Fxx: Set PM depth (0 to 7F)"), effectValAnd<127>}},
{0x55, {DIV_CMD_FM_SSG, _("55xy: Set detune 2 (x: operator from 1 to 4 (0 for all ops); y: detune from 0 to 3)"), effectOpVal<4>, effectValAnd<3>}},
{0x60, {DIV_CMD_FM_OPMASK, _("60xx: Set operator mask (bits 0-3)")}},
});
EffectHandlerMap fmOPZPostEffectHandlerMap(fmOPMPostEffectHandlerMap);

View file

@ -33,79 +33,70 @@ bool DivEngine::isExporting() {
return exporting;
}
void DivEngine::getLoopsLeft(int& loops) {
if(totalLoops < 0 || exportLoopCount == 0)
{
loops = 0;
void DivEngine::getLoopsLeft(int &loops) {
if (totalLoops<0 || exportLoopCount==0) {
loops=0;
return;
}
loops = exportLoopCount - 1 - totalLoops;
loops=exportLoopCount-1-totalLoops;
}
void DivEngine::getTotalLoops(int& loops) {
loops = exportLoopCount - 1;
void DivEngine::getTotalLoops(int &loops) {
loops=exportLoopCount-1;
}
void DivEngine::getCurSongPos(int& row, int& order) {
row = curRow;
order = curOrder;
void DivEngine::getCurSongPos(int &row, int &order) {
row=curRow;
order=curOrder;
}
void DivEngine::getTotalAudioFiles(int& files)
{
files = 0;
void DivEngine::getTotalAudioFiles(int &files) {
files=0;
switch(exportMode)
{
case DIV_EXPORT_MODE_ONE:
{
files = 1;
switch (exportMode) {
case DIV_EXPORT_MODE_ONE: {
files=1;
break;
}
case DIV_EXPORT_MODE_MANY_SYS:
{
files = 1; //there actually are several files but they are processed in the same loop, so to correctly draw progress we think of them as one file
case DIV_EXPORT_MODE_MANY_SYS: {
files=1; // there actually are several files but they are processed in the same loop, so to correctly draw progress we think of them as one file
break;
}
case DIV_EXPORT_MODE_MANY_CHAN:
{
for(int i = 0; i < chans; i++)
{
if (exportChannelMask[i]) files++;
case DIV_EXPORT_MODE_MANY_CHAN: {
for (int i=0; i<chans; i++) {
if (exportChannelMask[i]) {
files++;
}
}
break;
}
default: break;
default:
break;
}
}
void DivEngine::getCurFileIndex(int& file)
{
file = 0;
void DivEngine::getCurFileIndex(int &file) {
file=0;
switch(exportMode)
{
case DIV_EXPORT_MODE_ONE:
{
file = 0;
switch (exportMode) {
case DIV_EXPORT_MODE_ONE: {
file=0;
break;
}
case DIV_EXPORT_MODE_MANY_SYS:
{
file = 0; //there actually are several files but they are processed in the same loop, so to correctly draw progress we think of them as one file
case DIV_EXPORT_MODE_MANY_SYS: {
file=0; // there actually are several files but they are processed in the same loop, so to correctly draw progress we think of them as one file
break;
}
case DIV_EXPORT_MODE_MANY_CHAN:
{
file = curExportChan;
case DIV_EXPORT_MODE_MANY_CHAN: {
file=curExportChan;
break;
}
default: break;
default:
break;
}
}
bool DivEngine::getIsFadingOut()
{
bool DivEngine::getIsFadingOut() {
return isFadingOut;
}
@ -327,7 +318,7 @@ void DivEngine::runExportThread() {
// take control of audio output
deinitAudioBackend();
curExportChan = 0;
curExportChan=0;
float* outBuf[DIV_MAX_OUTPUTS];
float* outBufFinal;
@ -463,7 +454,7 @@ void DivEngine::runExportThread() {
}
logI("done!");
exporting=false;
curExportChan = 0;
curExportChan=0;
break;
}
}

View file

@ -302,6 +302,7 @@ const char* aboutLine[]={
_N("NDS sound emulator by cam900"),
"",
_N("greetings to:"),
"floxy!",
"NEOART Costa Rica",
"Xenium Demoparty",
"@party",

View file

@ -2589,34 +2589,32 @@ void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) {
int loopEnd=0;
e->walkSong(loopOrder,loopRow,loopEnd);
e->findSongLength(loopOrder, loopRow, audioExportOptions.fadeOut, songFadeoutSectionLength, songHasSongEndCommand, songOrdersLengths, songLength); //for progress estimation
e->findSongLength(loopOrder,loopRow,audioExportOptions.fadeOut,songFadeoutSectionLength,songHasSongEndCommand,songOrdersLengths,songLength); // for progress estimation
songLoopedSectionLength = songLength;
for(int i = 0; i < loopOrder; i++)
{
songLoopedSectionLength -= songOrdersLengths[i];
songLoopedSectionLength=songLength;
for (int i=0; i<loopOrder; i++) {
songLoopedSectionLength-=songOrdersLengths[i];
}
songLoopedSectionLength -= loopRow;
songLoopedSectionLength-=loopRow;
e->saveAudio(path.c_str(),audioExportOptions);
totalFiles = 0;
totalFiles=0;
e->getTotalAudioFiles(totalFiles);
int totalLoops = 0;
int totalLoops=0;
lengthOfOneFile = songLength;
lengthOfOneFile=songLength;
if(!songHasSongEndCommand)
{
if (!songHasSongEndCommand) {
e->getTotalLoops(totalLoops);
lengthOfOneFile += songLoopedSectionLength * totalLoops;
lengthOfOneFile += songFadeoutSectionLength; //account for fadeout
lengthOfOneFile+=songLoopedSectionLength*totalLoops;
lengthOfOneFile+=songFadeoutSectionLength; // account for fadeout
}
totalLength = lengthOfOneFile * totalFiles;
totalLength=lengthOfOneFile*totalFiles;
curProgress = 0.0f;
curProgress=0.0f;
displayExporting=true;
}
@ -5861,69 +5859,53 @@ bool FurnaceGUI::loop() {
MEASURE_BEGIN(popup);
centerNextWindow(_("Rendering..."),canvasW,canvasH);
if (ImGui::BeginPopupModal(_("Rendering..."),NULL,ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) {
if (ImGui::BeginPopupModal(_("Rendering..."),NULL,ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoMove)) {
// WHAT the HELL?!
WAKE_UP;
if(audioExportOptions.mode != DIV_EXPORT_MODE_MANY_CHAN)
{
if (audioExportOptions.mode!=DIV_EXPORT_MODE_MANY_CHAN) {
ImGui::Text(_("Please wait..."));
}
float* progressLambda = &curProgress;
int curPosInRows = 0;
int* curPosInRowsLambda = &curPosInRows;
int loopsLeft = 0;
int* loopsLeftLambda = &loopsLeft;
int totalLoops = 0;
int* totalLoopsLambda = &totalLoops;
int curFile = 0;
int* curFileLambda = &curFile;
if(e->isExporting())
{
e->lockEngine([this, progressLambda, curPosInRowsLambda, curFileLambda, loopsLeftLambda, totalLoopsLambda]()
{
int curRow = 0;
int curOrder = 0;
e->getCurSongPos(curRow, curOrder);
*curFileLambda = 0;
e->getCurFileIndex(*curFileLambda);
float* progressLambda=&curProgress;
int curPosInRows=0;
int* curPosInRowsLambda=&curPosInRows;
int loopsLeft=0;
int* loopsLeftLambda=&loopsLeft;
int totalLoops=0;
int* totalLoopsLambda=&totalLoops;
int curFile=0;
int* curFileLambda=&curFile;
if (e->isExporting()) {
e->lockEngine([this, progressLambda, curPosInRowsLambda, curFileLambda,
loopsLeftLambda, totalLoopsLambda] () {
int curRow=0; int curOrder=0;
e->getCurSongPos(curRow, curOrder); *curFileLambda=0;
e->getCurFileIndex(*curFileLambda);
*curPosInRowsLambda=curRow; for (int i=0; i<curOrder;
i++) {
*curPosInRowsLambda+=songOrdersLengths[i];}
*curPosInRowsLambda = curRow;
for(int i = 0; i < curOrder; i++)
{
*curPosInRowsLambda += songOrdersLengths[i];
}
if(!songHasSongEndCommand)
{
e->getLoopsLeft(*loopsLeftLambda);
e->getTotalLoops(*totalLoopsLambda);
if((*totalLoopsLambda) != (*loopsLeftLambda)) //we are going 2nd, 3rd, etc. time through the song
{
*curPosInRowsLambda -= (songLength - songLoopedSectionLength); //a hack so progress bar does not jump?
}
if(e->getIsFadingOut()) //we are in fadeout??? why it works like that bruh
{
*curPosInRowsLambda -= (songLength - songLoopedSectionLength); //a hack so progress bar does not jump?
}
}
*progressLambda = (float)((*curPosInRowsLambda) +
((*totalLoopsLambda) - (*loopsLeftLambda)) * songLength +
lengthOfOneFile * (*curFileLambda))
/ (float)totalLength;
});
if (!songHasSongEndCommand) {
e->getLoopsLeft(*loopsLeftLambda); e->getTotalLoops(*totalLoopsLambda); if ((*totalLoopsLambda)!=(*loopsLeftLambda)) //we are going 2nd, 3rd, etc. time through the song
{
*curPosInRowsLambda-=(songLength-songLoopedSectionLength); //a hack so progress bar does not jump?
}
if (e->getIsFadingOut()) //we are in fadeout??? why it works like that bruh
{
// LIVE WITH IT damn it
*curPosInRowsLambda-=(songLength-songLoopedSectionLength); //a hack so progress bar does not jump?
}
}
// this horrible indentation courtesy of `indent`
*progressLambda=(float) ((*curPosInRowsLambda) + ((*totalLoopsLambda)- (*loopsLeftLambda)) * songLength + lengthOfOneFile * (*curFileLambda)) / (float) totalLength;});
}
ImGui::Text(_("Row %d of %d"), curPosInRows +
((totalLoops) - (loopsLeft)) * songLength, lengthOfOneFile);
ImGui::Text(_("Row %d of %d"),curPosInRows+((totalLoops)-(loopsLeft))*songLength,lengthOfOneFile);
if(audioExportOptions.mode == DIV_EXPORT_MODE_MANY_CHAN)
{
ImGui::Text(_("Channel %d of %d"), curFile + 1, totalFiles);
if (audioExportOptions.mode==DIV_EXPORT_MODE_MANY_CHAN) {
ImGui::Text(_("Channel %d of %d"),curFile+1,totalFiles);
}
ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0), fmt::sprintf("%.2f%%", curProgress * 100.0f).c_str());
ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0),fmt::sprintf("%.2f%%",curProgress*100.0f).c_str());
if (ImGui::Button(_("Abort"))) {
if (e->haltAudioFile()) {

View file

@ -1723,13 +1723,13 @@ class FurnaceGUI {
char emptyLabel[32];
char emptyLabel2[32];
std::vector<int> songOrdersLengths; //lengths of all orders (for drawing song export progress)
int songLength; //length of all the song in rows
int songLoopedSectionLength; //length of looped part of the song
int songFadeoutSectionLength; //length of fading part of the song
bool songHasSongEndCommand; //song has "Song end" command (FFxx)
int lengthOfOneFile; //length of one rendering pass. song length times num of loops + fadeout
int totalLength; //total length of render (lengthOfOneFile times num of files for per-channel export)
std::vector<int> songOrdersLengths; // lengths of all orders (for drawing song export progress)
int songLength; // length of all the song in rows
int songLoopedSectionLength; // length of looped part of the song
int songFadeoutSectionLength; // length of fading part of the song
bool songHasSongEndCommand; // song has "Song end" command (FFxx)
int lengthOfOneFile; // length of one rendering pass. song length times num of loops + fadeout
int totalLength; // total length of render (lengthOfOneFile times num of files for per-channel export)
float curProgress;
int totalFiles;

View file

@ -253,16 +253,27 @@ void FurnaceGUI::drawSampleEdit() {
SAMPLE_WARN(warnLength,"QSound: maximum sample length is 65535");
}
break;
case DIV_SYSTEM_NES:
case DIV_SYSTEM_NES: {
if (sample->loop) {
if (sample->loopStart!=0 || sample->loopEnd!=(int)(sample->samples)) {
SAMPLE_WARN(warnLoopPos,_("NES: loop point ignored on DPCM (may only loop entire sample)"));
if (sample->loopStart&511) {
int tryWith=(sample->loopStart)&(~511);
if (tryWith>(int)sample->samples) tryWith-=512;
String alignHint=fmt::sprintf(_("NES: loop start must be a multiple of 512 (try with %d)"),tryWith);
SAMPLE_WARN(warnLoopStart,alignHint);
}
if ((sample->loopEnd-8)&127) {
int tryWith=(sample->loopEnd-8)&(~127);
if (tryWith>(int)sample->samples) tryWith-=128;
tryWith+=8; // +1 bc of how sample length is treated: https://www.nesdev.org/wiki/APU_DMC
String alignHint=fmt::sprintf(_("NES: loop end must be a multiple of 128 (try with %d)"),tryWith);
SAMPLE_WARN(warnLoopEnd,alignHint);
}
}
if (sample->samples>32648) {
SAMPLE_WARN(warnLength,_("NES: maximum DPCM sample length is 32648"));
}
break;
}
case DIV_SYSTEM_X1_010:
if (sample->loop) {
SAMPLE_WARN(warnLoop,_("X1-010: samples can't loop"));