furnace/src/engine/song.h
2026-01-14 17:45:21 -05:00

582 lines
15 KiB
C++

/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2026 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 _SONG_H
#define _SONG_H
#include <stdio.h>
#include "../pch.h"
#include "defines.h"
#include "../timeutils.h"
#include "../ta-utils.h"
#include "config.h"
#include "assetDir.h"
#include "orders.h"
#include "instrument.h"
#include "pattern.h"
#include "wavetable.h"
#include "sample.h"
#include "sysDef.h"
enum DivEffectType: unsigned short {
DIV_EFFECT_NULL=0,
DIV_EFFECT_DUMMY,
DIV_EFFECT_EXTERNAL,
DIV_EFFECT_VOLUME,
DIV_EFFECT_FILTER
};
enum DivFileElementType: unsigned char {
DIV_ELEMENT_END=0,
DIV_ELEMENT_SUBSONG,
DIV_ELEMENT_CHIP_FLAGS,
DIV_ELEMENT_ASSET_DIR,
DIV_ELEMENT_INSTRUMENT,
DIV_ELEMENT_WAVETABLE,
DIV_ELEMENT_SAMPLE,
DIV_ELEMENT_PATTERN,
DIV_ELEMENT_COMPAT_FLAGS,
DIV_ELEMENT_COMMENTS,
DIV_ELEMENT_GROOVE,
DIV_ELEMENT_MAX
};
struct DivGroovePattern {
unsigned short val[16];
unsigned short len;
bool readData(SafeReader& reader);
void putData(SafeWriter* w);
DivGroovePattern():
len(1) {
for (int i=0; i<16; i++) {
val[i]=6;
}
}
};
struct DivSongTimestamps {
// song duration (in seconds and microseconds)
TimeMicros totalTime;
uint64_t totalTicks;
int totalRows;
// loop region (order/row positions)
struct Position {
int order, row;
Position():
order(0), row(0) {}
} loopStart, loopEnd;
// set to true if a 0Bxx effect is found and it defines a loop region
bool isLoopDefined;
// set to false if FFxx is found
bool isLoopable;
// timestamp of a row
// DO NOT ACCESS DIRECTLY! use the functions instead.
// if seconds is -1, it means this row is not touched at all.
TimeMicros* orders[DIV_MAX_PATTERNS];
TimeMicros loopStartTime;
// the furthest row that the playhead goes through in an order.
unsigned char maxRow[DIV_MAX_PATTERNS];
// call this function to get the timestamp of a row.
TimeMicros getTimes(int order, int row);
DivSongTimestamps();
~DivSongTimestamps();
};
struct DivSubSong {
String name, notes;
unsigned char hilightA, hilightB;
unsigned char effectDivider, arpLen;
DivGroovePattern speeds;
short virtualTempoN, virtualTempoD;
float hz;
int patLen, ordersLen;
DivOrders orders;
DivChannelData pat[DIV_MAX_CHANS];
bool chanShow[DIV_MAX_CHANS];
bool chanShowChanOsc[DIV_MAX_CHANS];
unsigned char chanCollapse[DIV_MAX_CHANS];
String chanName[DIV_MAX_CHANS];
String chanShortName[DIV_MAX_CHANS];
unsigned int chanColor[DIV_MAX_CHANS];
// song timestamps
DivSongTimestamps ts;
/**
* calculate timestamps (loop position, song length and more).
*/
void calcTimestamps(int chans, std::vector<DivGroovePattern>& grooves, int jumpTreatment, int ignoreJumpAtEnd, int brokenSpeedSel, int delayBehavior, int firstPat=0);
/**
* read sub-song data.
*/
bool readData(SafeReader& reader, int version, int chans);
/**
* write sub-song block to a SafeWriter.
*/
void putData(SafeWriter* w, int chans);
void clearData();
void removeUnusedPatterns();
void optimizePatterns();
void rearrangePatterns();
void sortOrders();
void makePatUnique();
DivSubSong():
hilightA(4),
hilightB(16),
effectDivider(0),
arpLen(1),
virtualTempoN(150),
virtualTempoD(150),
hz(60.0),
patLen(64),
ordersLen(1) {
for (int i=0; i<DIV_MAX_CHANS; i++) {
chanShow[i]=true;
chanShowChanOsc[i]=true;
chanCollapse[i]=0;
chanColor[i]=0;
}
}
};
struct DivEffectStorage {
DivEffectType id;
unsigned short slot, storageVer;
float dryWet;
unsigned char* storage;
size_t storageLen;
DivEffectStorage():
id(DIV_EFFECT_NULL),
slot(0),
storageVer(0),
dryWet(1.0f),
storage(NULL),
storageLen(0) {}
};
struct DivCompatFlags {
bool limitSlides;
// linear pitch
unsigned char linearPitch;
unsigned char pitchSlideSpeed;
// loop behavior
// 0: reset on loop
// 1: fake reset on loop
// 2: don't do anything on loop
unsigned char loopModality;
// cut/delay effect behavior
// 0: strict (don't allow value higher than or equal to speed)
// 1: broken (don't allow value higher than speed)
// 2: lax (allow value higher than speed)
unsigned char delayBehavior;
// 0B/0D treatment
// 0: normal (0B/0D accepted)
// 1: old Furnace (first one accepted)
// 2: DefleMask (0D takes priority over 0B)
unsigned char jumpTreatment;
bool properNoiseLayout;
bool waveDutyIsVol;
bool resetMacroOnPorta;
bool legacyVolumeSlides;
bool compatibleArpeggio;
bool noteOffResetsSlides;
bool targetResetsSlides;
bool arpNonPorta;
bool algMacroBehavior;
bool brokenShortcutSlides;
bool ignoreDuplicateSlides;
bool stopPortaOnNoteOff;
bool continuousVibrato;
bool brokenDACMode;
bool oneTickCut;
bool newInsTriggersInPorta;
bool arp0Reset;
bool brokenSpeedSel;
bool noSlidesOnFirstTick;
bool rowResetsArpPos;
bool ignoreJumpAtEnd;
bool buggyPortaAfterSlide;
bool gbInsAffectsEnvelope;
bool sharedExtStat;
bool ignoreDACModeOutsideIntendedChannel;
bool e1e2AlsoTakePriority;
bool newSegaPCM;
bool fbPortaPause;
bool snDutyReset;
bool pitchMacroIsLinear;
bool oldOctaveBoundary;
bool noOPN2Vol;
bool newVolumeScaling;
bool volMacroLinger;
bool brokenOutVol;
bool brokenOutVol2;
bool e1e2StopOnSameNote;
bool brokenPortaArp;
bool snNoLowPeriods;
bool disableSampleMacro;
bool oldArpStrategy;
bool brokenPortaLegato;
bool brokenFMOff;
bool preNoteNoEffect;
bool oldDPCM;
bool resetArpPhaseOnNewNote;
bool ceilVolumeScaling;
bool oldAlwaysSetVolume;
bool oldSampleOffset;
// new flags as of dev240
bool oldCenterRate;
void setDefaults();
bool areDefaults();
bool readData(SafeReader& reader);
void putData(SafeWriter* w);
bool operator==(const DivCompatFlags& other) {
return (memcmp(this,&other,sizeof(DivCompatFlags))==0);
}
bool operator!=(const DivCompatFlags& other) {
return (memcmp(this,&other,sizeof(DivCompatFlags))!=0);
}
DivCompatFlags() {
memset(this,0,sizeof(DivCompatFlags));
setDefaults();
}
};
struct DivSong {
unsigned short version;
bool isDMF;
// system
int chans;
DivSystem system[DIV_MAX_CHIPS];
unsigned short systemChans[DIV_MAX_CHIPS];
unsigned char systemLen;
float systemVol[DIV_MAX_CHIPS];
float systemPan[DIV_MAX_CHIPS];
float systemPanFR[DIV_MAX_CHIPS];
DivConfig systemFlags[DIV_MAX_CHIPS];
// song information
String name, author, systemName;
// legacy song information
// those will be stored in .fur and mapped to VGM as:
// category -> game name
// writer -> ripper
// createdDate -> year
String carrier, composer, vendor, category, writer, arranger, copyright, manGroup, manInfo, createdDate, revisionDate;
// more VGM specific stuff
String nameJ, authorJ, categoryJ, systemNameJ;
// other things
String notes;
// module details
int insLen, waveLen, sampleLen;
float masterVol;
float tuning;
bool autoSystem;
bool patchbayAuto;
// compatibility flags
DivCompatFlags compatFlags;
std::vector<DivInstrument*> ins;
std::vector<DivWavetable*> wave;
std::vector<DivSample*> sample;
std::vector<DivSubSong*> subsong;
std::vector<unsigned int> patchbay;
std::vector<DivGroovePattern> grooves;
std::vector<DivAssetDir> insDir;
std::vector<DivAssetDir> waveDir;
std::vector<DivAssetDir> sampleDir;
std::vector<DivEffectStorage> effects;
/**
* INTERNAL STATE - do not modify.
*/
// default/"null" instruments (when instrument is none/-1)
DivInstrument nullIns, nullInsOPLL, nullInsOPL, nullInsOPLDrums, nullInsQSound, nullInsESFM;
// default assets, returned by getWave()/getSample() in DivEngine
DivWavetable nullWave;
DivSample nullSample;
// channel information arrays.
// chip of a channel
DivSystem sysOfChan[DIV_MAX_CHANS];
// dispatch (chip index) of a channel
int dispatchOfChan[DIV_MAX_CHANS];
// tracker channel to chip channel mapping
// -1 means "nowhere".
int dispatchChanOfChan[DIV_MAX_CHANS];
// the first channel of a chip, indexed per channel
int dispatchFirstChan[DIV_MAX_CHANS];
// cached DivChanDef for each channel.
DivChanDef chanDef[DIV_MAX_CHANS];
std::vector<DivInstrumentType> possibleInsTypes;
/**
* find data past 0Bxx effects and place that into new sub-songs.
*/
void findSubSongs();
/**
* clear orders and patterns.
*/
void clearSongData();
/**
* clear instruments.
*/
void clearInstruments();
/**
* clear wavetables.
*/
void clearWavetables();
/**
* clear samples.
*/
void clearSamples();
/**
* set systemChans[] to default values.
* call recalcChans() afterwards.
*/
void initDefaultSystemChans();
/**
* recalculate channel count and internal state.
* call after editing system[] or systemChans[].
*/
void recalcChans();
/**
* unloads the song, freeing all memory associated with it.
* use before destroying the object.
*/
void unload();
DivSong():
version(0),
isDMF(false),
chans(0),
systemLen(2),
name(""),
author(""),
systemName(""),
carrier(""),
composer(""),
vendor(""),
category(""),
writer(""),
arranger(""),
copyright(""),
manGroup(""),
manInfo(""),
createdDate(""),
revisionDate(""),
insLen(0),
waveLen(0),
sampleLen(0),
masterVol(1.0f),
tuning(440.0f),
autoSystem(true),
patchbayAuto(true) {
memset(dispatchFirstChan,0,DIV_MAX_CHANS*sizeof(int));
memset(dispatchChanOfChan,0,DIV_MAX_CHANS*sizeof(int));
memset(dispatchOfChan,0,DIV_MAX_CHANS*sizeof(int));
memset(sysOfChan,0,DIV_MAX_CHANS*sizeof(int));
for (int i=0; i<DIV_MAX_CHIPS; i++) {
system[i]=DIV_SYSTEM_NULL;
systemChans[i]=0;
systemVol[i]=1.0;
systemPan[i]=0.0;
systemPanFR[i]=0.0;
}
subsong.push_back(new DivSubSong);
system[0]=DIV_SYSTEM_YM2612;
system[1]=DIV_SYSTEM_SMS;
systemChans[0]=6;
systemChans[1]=4;
// OPLL default instrument contest winner - piano_guitar_idk by Weeppiko
nullInsOPLL.type=DIV_INS_OPLL;
nullInsOPLL.fm.opllPreset=0;
nullInsOPLL.fm.alg=0;
nullInsOPLL.fm.fb=7;
nullInsOPLL.fm.fms=1;
nullInsOPLL.fm.ams=0;
nullInsOPLL.fm.op[0].ar=15;
nullInsOPLL.fm.op[0].dr=5;
nullInsOPLL.fm.op[0].sl=3;
nullInsOPLL.fm.op[0].rr=3;
nullInsOPLL.fm.op[0].tl=40;
nullInsOPLL.fm.op[0].ksl=0;
nullInsOPLL.fm.op[0].mult=5;
nullInsOPLL.fm.op[0].am=0;
nullInsOPLL.fm.op[0].vib=1;
nullInsOPLL.fm.op[0].ksr=0;
nullInsOPLL.fm.op[0].ssgEnv=8;
nullInsOPLL.fm.op[1].ar=15;
nullInsOPLL.fm.op[1].dr=1;
nullInsOPLL.fm.op[1].sl=11;
nullInsOPLL.fm.op[1].rr=6;
nullInsOPLL.fm.op[1].tl=0;
nullInsOPLL.fm.op[1].ksl=0;
nullInsOPLL.fm.op[1].mult=1;
nullInsOPLL.fm.op[1].am=0;
nullInsOPLL.fm.op[1].vib=0;
nullInsOPLL.fm.op[1].ksr=0;
nullInsOPLL.fm.op[1].ssgEnv=8;
nullInsOPLL.name="This is a bug! Report!";
nullInsOPL.type=DIV_INS_OPL;
nullInsOPL.fm.alg=0;
nullInsOPL.fm.fb=7;
nullInsOPL.fm.op[0].dr=2;
nullInsOPL.fm.op[0].rr=7;
nullInsOPL.fm.op[0].tl=22;
nullInsOPL.fm.op[0].ksl=1;
nullInsOPL.fm.op[0].mult=3;
nullInsOPL.fm.op[1].tl=0;
nullInsOPL.fm.op[1].dr=3;
nullInsOPL.fm.op[1].rr=12;
nullInsOPL.fm.op[1].mult=1;
nullInsOPL.name="This is a bug! Report!";
nullInsOPL.fm.kickFreq=(1<<10)|576;
nullInsOPL.fm.snareHatFreq=(1<<10)|672;
nullInsOPL.fm.tomTopFreq=896;
nullInsOPLDrums=nullInsOPL;
nullInsOPLDrums.type=DIV_INS_OPL_DRUMS;
nullInsOPLDrums.fm.fixedDrums=true;
for (int i=0; i<4; i++) {
nullInsOPLDrums.fm.op[i].am=0;
nullInsOPLDrums.fm.op[i].vib=0;
nullInsOPLDrums.fm.op[i].ksr=0;
nullInsOPLDrums.fm.op[i].sus=0;
nullInsOPLDrums.fm.op[i].ws=0;
nullInsOPLDrums.fm.op[i].ksl=0;
nullInsOPLDrums.fm.op[i].tl=0;
}
// snare
nullInsOPLDrums.fm.op[0].ar=13;
nullInsOPLDrums.fm.op[0].dr=8;
nullInsOPLDrums.fm.op[0].sl=4;
nullInsOPLDrums.fm.op[0].rr=8;
nullInsOPLDrums.fm.op[0].mult=1;
// tom
nullInsOPLDrums.fm.op[1].ar=15;
nullInsOPLDrums.fm.op[1].dr=8;
nullInsOPLDrums.fm.op[1].sl=5;
nullInsOPLDrums.fm.op[1].rr=9;
nullInsOPLDrums.fm.op[1].mult=5;
// top
nullInsOPLDrums.fm.op[2].ar=10;
nullInsOPLDrums.fm.op[2].dr=10;
nullInsOPLDrums.fm.op[2].sl=5;
nullInsOPLDrums.fm.op[2].rr=5;
nullInsOPLDrums.fm.op[2].mult=1;
nullInsOPLDrums.fm.op[2].ksr=1;
// hi-hat
nullInsOPLDrums.fm.op[3].ar=12;
nullInsOPLDrums.fm.op[3].dr=8;
nullInsOPLDrums.fm.op[3].sl=10;
nullInsOPLDrums.fm.op[3].rr=7;
nullInsOPLDrums.fm.op[3].mult=2;
nullInsQSound.std.panLMacro.mode=true;
// ESFM default instrument - port of OPN default instrument
nullInsESFM.esfm.noise=0;
nullInsESFM.esfm.op[0].outLvl=0;
nullInsESFM.esfm.op[0].modIn=4;
nullInsESFM.esfm.op[0].dt=2;
nullInsESFM.fm.op[0].tl=42;
nullInsESFM.fm.op[0].ar=15;
nullInsESFM.fm.op[0].dr=3;
nullInsESFM.fm.op[0].sl=15;
nullInsESFM.fm.op[0].rr=3;
nullInsESFM.fm.op[0].mult=5;
nullInsESFM.esfm.op[1].outLvl=0;
nullInsESFM.esfm.op[1].modIn=7;
nullInsESFM.esfm.op[1].dt=-3;
nullInsESFM.fm.op[1].tl=18;
nullInsESFM.fm.op[1].ar=15;
nullInsESFM.fm.op[1].dr=3;
nullInsESFM.fm.op[1].sl=15;
nullInsESFM.fm.op[1].rr=4;
nullInsESFM.fm.op[1].mult=1;
nullInsESFM.esfm.op[2].outLvl=0;
nullInsESFM.esfm.op[2].modIn=7;
nullInsESFM.esfm.op[2].dt=2;
nullInsESFM.fm.op[2].tl=48;
nullInsESFM.fm.op[2].ar=15;
nullInsESFM.fm.op[2].dr=2;
nullInsESFM.fm.op[2].sl=11;
nullInsESFM.fm.op[2].rr=1;
nullInsESFM.fm.op[2].mult=1;
nullInsESFM.fm.op[2].sus=1;
nullInsESFM.esfm.op[3].outLvl=7;
nullInsESFM.esfm.op[3].modIn=7;
nullInsESFM.esfm.op[3].dt=-3;
nullInsESFM.fm.op[3].tl=0;
nullInsESFM.fm.op[3].ar=15;
nullInsESFM.fm.op[3].dr=3;
nullInsESFM.fm.op[3].sl=15;
nullInsESFM.fm.op[3].rr=9;
nullInsESFM.fm.op[3].mult=1;
recalcChans();
}
};
#endif