/** * Furnace Tracker - multi-system chiptune tracker * Copyright (C) 2021-2025 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 "lynx.h" #include "../engine.h" #include "../bsr.h" #include #define rWrite(a,v) {if (!skipRegisterWrites) {writes.push(QueuedWrite(a,v)); if (dumpWrites) {addWrite(a,v);}}} #define WRITE_VOLUME(ch,v) rWrite(0x20+(ch<<3),(v)) #define WRITE_FEEDBACK(ch,v) rWrite(0x21+(ch<<3),(v)) #define WRITE_OUTPUT(ch,v) rWrite(0x22+(ch<<3),(v)) #define WRITE_LFSR(ch,v) rWrite(0x23+(ch<<3),(v)) #define WRITE_BACKUP(ch,v) rWrite(0x24+(ch<<3),(v)) #define WRITE_CONTROL(ch,v) rWrite(0x25+(ch<<3),(v)) #define WRITE_OTHER(ch,v) rWrite(0x27+(ch<<3),(v)) #define WRITE_ATTEN(ch,v) rWrite((0x40+ch),(v)) #define WRITE_STEREO(v) rWrite(0x50,(v)) #define CHIP_FREQBASE 16000000 static int32_t clamp(int32_t v, int32_t lo, int32_t hi) { return vhi?hi:v); } const int DUTY_DIVIDERS[]={ 1, 2, 4, 3, 6, 7, 7, 4, 8, 15, 6, 14, 15, 12, 14, 5, 10, 21, 31, 28, 31, 30, 12, 31, 21, 8, 30, 31, 28, 31, 31, 6, 12, 63, 14, 62, 9, 28, 62, 15, 14, 42, 8, 21, 62, 63, 15, 60, 63, 20, 42, 63, 28, 12, 63, 62, 62, 63, 21, 24, 15, 62, 60, 7, 16, 63, 30, 254, 217, 84, 186, 217, 12, 254, 28, 15, 254, 51, 255, 84, 217, 120, 210, 63, 60, 255, 255, 254, 254, 217, 63, 252, 17, 42, 124, 85, 30, 254, 24, 217, 210, 21, 255, 252, 28, 217, 10, 186, 63, 124, 254, 255, 186, 255, 255, 56, 255, 70, 36, 30, 255, 60, 254, 85, 124, 85, 21, 254, 22,1533,2047, 868,1953,2046, 420, 651,1533,2044,2046,2047,1016,1785, 105, 126, 595, 630, 204,1533,1302,2047,2047,2044,2044, 119, 372,1778,1953, 120, 682,1905, 595, 868, 510,2047,2044, 762, 279, 682, 210,1953, 595,1524,1533, 210, 248, 635, 60,2047,2047, 62,1905,2044,2046, 42,2047, 510, 252,1905,1778,2047,1533,1016, 1953,1860,1778, 219, 48,1905,2047,1778, 682,2047, 465,1020,1785, 126,2044, 434, 2044, 63,2047,2046,2047, 280, 30,1953, 210, 682, 868, 89,2046,2047,2047,1524, 1302,1785,2047,1860,2047,2046,1016, 372,2047, 84, 630, 70, 252,2047,1533, 682, 1905,2046, 372,1905, 90,1533, 217, 168, 340,2047,2047,1302,1533, 28, 186,2047, 24,3255, 126,1190, 45,4092, 178, 315, 28, 438, 124, 315,3570, 255,1023,2044, 819,1016, 930,3937,1260,1302, 511,4094,3810, 819, 195,2604,1023,4094, 84,1365, 18,3810, 56,1023, 42,3937, 819,4092, 124,4095, 30,4094, 85,1524,3906, 63, 3906,1023,3255, 120,4095,3570,1020,3937,2667,3556,4094, 195,2044,4095,4095, 762, 28,1302, 84,4095,3906,1023, 255, 292, 16,1085, 42,3066, 315,3556,4094,4095, 3066,4095,3937, 372, 511,4094, 620,1302, 273, 504,1190, 819,4092,4095,1023, 210, 124,1023, 126,3570,3937, 252, 438,4095, 30,4094, 120,4095,4094, 255, 63,1020, 4095,1364, 558,2667, 420,4095, 105,1270,1190,3255, 93,1016, 91, 372,4092,3937, 3255, 44,3066,1365,1736, 510, 91,4094,1302, 511,3937, 420, 105,3906,4092, 819, 252, 585, 255, 210,3937,1016,3570,1023, 455,3066,2044,1365,4094, 126,2667,4092, 3810, 93, 63,1364,1365,3906, 120, 28,1023,2044, 238,4095,3556, 255,4095, 372, 511,1190,1260,1023,3066, 255, 819, 408,2044, 255,1302,4094, 455,2604,4094, 585, 438, 511, 455, 292, 455, 434,1364, 102,1085, 204,3570, 255, 240,4095, 93,4094, 3937,4094, 84, 63,4094, 315, 819, 140,1260,3937,4095,3066,2667, 680,4094, 511, 4095,2044, 178,1023,1020, 819, 21,3810,4094, 20,1023,3556,3937, 210,2040, 273, 252,2667,4095,3906, 63, 124, 930, 455, 510,4094,4092, 511,3570,4095, 30, 744 }; const char* regCheatSheetLynx[]={ "AUDIO0_VOLCNTRL", "20", "AUDIO0_FEEDBACK", "21", "AUDIO0_OUTPUT", "22", "AUDIO0_SHIFT", "23", "AUDIO0_BACKUP", "24", "AUDIO0_CONTROL", "25", "AUDIO0_COUNTER", "26", "AUDIO0_OTHER", "27", "AUDIO1_VOLCNTRL", "28", "AUDIO1_FEEDBACK", "29", "AUDIO1_OUTPUT", "2a", "AUDIO1_SHIFT", "2b", "AUDIO1_BACKUP", "2c", "AUDIO1_CONTROL", "2d", "AUDIO1_COUNTER", "2e", "AUDIO1_OTHER", "2f", "AUDIO2_VOLCNTRL", "30", "AUDIO2_FEEDBACK", "31", "AUDIO2_OUTPUT", "32", "AUDIO2_SHIFT", "33", "AUDIO2_BACKUP", "34", "AUDIO2_CONTROL", "35", "AUDIO2_COUNTER", "36", "AUDIO2_OTHER", "37", "AUDIO3_VOLCNTRL", "38", "AUDIO3_FEEDBACK", "39", "AUDIO3_OUTPUT", "3a", "AUDIO3_SHIFT", "3b", "AUDIO3_BACKUP", "3c", "AUDIO3_CONTROL", "3d", "AUDIO3_COUNTER", "3e", "AUDIO3_OTHER", "3f", "ATTENREG0", "40", "ATTENREG1", "41", "ATTENREG2", "42", "ATTENREG3", "43", "MPAN", "44", "MSTEREO", "50", NULL }; const char** DivPlatformLynx::getRegisterSheet() { return regCheatSheetLynx; } void DivPlatformLynx::processDAC(int sRate) { for (int i=0; i<4; i++) { if (chan[i].pcm && chan[i].sample>=0 && chan[i].samplesong.sampleLen) { chan[i].sampleAccum-=chan[i].sampleFreq; while (chan[i].sampleAccum<0) { chan[i].sampleAccum+=sRate; DivSample* s=parent->getSample(chan[i].sample); if (s!=NULL) { if (isMuted[i]) { WRITE_OUTPUT(i,0); } else { if (chan[i].samplePos<0 || chan[i].samplePos>=(int)s->samples) { WRITE_OUTPUT(i,0); } else { WRITE_OUTPUT(i,CLAMP((s->data8[chan[i].samplePos]*chan[i].outVol)>>7,-128,127)); } } chan[i].samplePos++; if (s->isLoopable() && chan[i].samplePos>=s->loopEnd) { chan[i].samplePos=s->loopStart; } else if (chan[i].samplePos>=(int)s->samples) { chan[i].sample=-1; } } } } } } void DivPlatformLynx::acquire(short** buf, size_t len) { thread_local int chanBuf[4]; for (int i=0; i<4; i++) { oscBuf[i]->begin(len); } for (size_t h=0; hwrite(w.addr,w.val); writes.pop_front(); } mikey->sampleAudio(buf[0]+h,buf[1]+h,1,chanBuf); for (int i=0; i<4; i++) { oscBuf[i]->putSample(h,chanBuf[i]); } } for (int i=0; i<4; i++) { oscBuf[i]->end(len); } } void DivPlatformLynx::fillStream(std::vector& stream, int sRate, size_t len) { writes.clear(); for (size_t i=0; i>7; } WRITE_VOLUME(i,(isMuted[i]?0:(chan[i].outVol&127))); } if (NEW_ARP_STRAT) { chan[i].handleArp(); } else if (chan[i].std.arp.had) { if (!chan[i].inPorta) { double CHIP_DIVIDER=tuned?DUTY_DIVIDERS[chan[i].duty.val&0x1ff]*8:64; chan[i].actualNote=parent->calcArp(chan[i].note,chan[i].std.arp.val); chan[i].baseFreq=NOTE_PERIODIC(chan[i].actualNote); if (chan[i].pcm) chan[i].sampleBaseFreq=NOTE_FREQUENCY(chan[i].actualNote); chan[i].freqChanged=true; } } if (chan[i].std.panL.had) { chan[i].pan&=0x0f; chan[i].pan|=(chan[i].std.panL.val&15)<<4; } if (chan[i].std.panR.had) { chan[i].pan&=0xf0; chan[i].pan|=chan[i].std.panR.val&15; } if (chan[i].std.panL.had || chan[i].std.panR.had) { WRITE_ATTEN(i,chan[i].pan); } if (chan[i].std.pitch.had) { if (chan[i].std.pitch.mode) { chan[i].pitch2+=chan[i].std.pitch.val; CLAMP_VAR(chan[i].pitch2,-32768,32767); } else { chan[i].pitch2=chan[i].std.pitch.val; } chan[i].freqChanged=true; } if (chan[i].std.ex1.had) { chan[i].lfsr=chan[i].std.ex1.val&0xfff; chan[i].freqChanged=true; chan[i].updateLFSR=true; } if (chan[i].std.phaseReset.had) { if (chan[i].std.phaseReset.val==1) { if (chan[i].pcm && chan[i].sample>=0 && chan[i].samplesong.sampleLen) { chan[i].sampleAccum=0; if (chan[i].setPos) { chan[i].setPos=false; } else { chan[i].samplePos=0; } } chan[i].freqChanged=true; chan[i].updateLFSR=true; } } if (chan[i].freqChanged) { if (chan[i].pcm) { double off=1.0; if (chan[i].sample>=0 && chan[i].samplesong.sampleLen) { DivSample* s=parent->getSample(chan[i].sample); if (s->centerRate<1) { off=1.0; } else { off=(double)s->centerRate/parent->getCenterRate(); } } chan[i].sampleFreq=off*parent->calcFreq(chan[i].sampleBaseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE); if (dumpWrites) addWrite(0xffff0001+(i<<8),chan[i].sampleFreq); } else { if (chan[i].updateLFSR) { WRITE_LFSR(i, (chan[i].lfsr&0xff)); WRITE_OTHER(i, ((chan[i].lfsr&0xf00)>>4)); chan[i].updateLFSR=false; } if (chan[i].std.duty.had) { chan[i].duty=chan[i].std.duty.val; if (!chan[i].pcm) { WRITE_FEEDBACK(i, chan[i].duty.feedback); } } double divider=tuned?DUTY_DIVIDERS[chan[i].duty.val&0x1ff]*8:64; chan[i].fd=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,true,0,chan[i].pitch2,chipClock,divider); WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); WRITE_BACKUP( i, chan[i].fd.backup ); } chan[i].freqChanged=false; } else if (chan[i].std.duty.had) { chan[i].duty=chan[i].std.duty.val; if (!chan[i].pcm) { if (tuned) { double divider=DUTY_DIVIDERS[chan[i].duty.val&0x1ff]*8; chan[i].fd=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,true,0,chan[i].pitch2,chipClock,divider); } WRITE_FEEDBACK(i, chan[i].duty.feedback); WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); if (tuned) WRITE_BACKUP( i, chan[i].fd.backup ); } } } } int DivPlatformLynx::dispatch(DivCommand c) { double CHIP_DIVIDER=tuned?DUTY_DIVIDERS[chan[c.chan].duty.val&0x1ff]*8:64; switch (c.cmd) { case DIV_CMD_NOTE_ON: { bool prevPCM=chan[c.chan].pcm; DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_MIKEY); chan[c.chan].macroVolMul=ins->type==DIV_INS_AMIGA?64:127; chan[c.chan].pcm=(ins->type==DIV_INS_AMIGA || ins->amiga.useSample); if (chan[c.chan].pcm!=prevPCM) { if (chan[c.chan].pcm) { WRITE_FEEDBACK(c.chan,0); WRITE_CONTROL(c.chan,0x18); WRITE_BACKUP(c.chan,0); } else { chan[c.chan].sampleNote=DIV_NOTE_NULL; chan[c.chan].sampleNoteDelta=0; WRITE_FEEDBACK(c.chan,chan[c.chan].duty.feedback); WRITE_CONTROL(c.chan,(chan[c.chan].fd.clockDivider|0x18|chan[c.chan].duty.int_feedback7)); WRITE_BACKUP(c.chan,chan[c.chan].fd.backup); } } if (chan[c.chan].pcm) { if (c.value!=DIV_NOTE_NULL) { chan[c.chan].sample=ins->amiga.getSample(c.value); chan[c.chan].sampleNote=c.value; c.value=ins->amiga.getFreq(c.value); chan[c.chan].sampleNoteDelta=c.value-chan[c.chan].sampleNote; chan[c.chan].sampleBaseFreq=NOTE_FREQUENCY(c.value); } else if (chan[c.chan].sampleNote!=DIV_NOTE_NULL) { chan[c.chan].sample=ins->amiga.getSample(chan[c.chan].sampleNote); c.value=ins->amiga.getFreq(chan[c.chan].sampleNote); } chan[c.chan].sampleAccum=0; if (chan[c.chan].setPos) { chan[c.chan].setPos=false; } else { chan[c.chan].samplePos=0; } if (dumpWrites) { addWrite(0xffff0000+(c.chan<<8),chan[c.chan].sample); } } if (c.value!=DIV_NOTE_NULL) { chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; chan[c.chan].actualNote=c.value; chan[c.chan].updateLFSR=true; } chan[c.chan].active=true; WRITE_VOLUME(c.chan,(isMuted[c.chan]?0:(chan[c.chan].vol&127))); chan[c.chan].macroInit(ins); if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; } break; } case DIV_CMD_NOTE_OFF: chan[c.chan].active=false; WRITE_VOLUME(c.chan,0); WRITE_CONTROL(c.chan,0); chan[c.chan].macroInit(NULL); if (chan[c.chan].pcm) { chan[c.chan].pcm=false; if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0); } break; case DIV_CMD_LYNX_LFSR_LOAD: chan[c.chan].freqChanged=true; chan[c.chan].lfsr=c.value; chan[c.chan].updateLFSR=true; break; case DIV_CMD_NOTE_OFF_ENV: case DIV_CMD_ENV_RELEASE: chan[c.chan].std.release(); break; case DIV_CMD_INSTRUMENT: chan[c.chan].ins=c.value; //chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_MIKEY)); break; case DIV_CMD_VOLUME: if (chan[c.chan].vol!=c.value) { chan[c.chan].vol=c.value; if (!chan[c.chan].std.vol.has) { chan[c.chan].outVol=c.value; } if (chan[c.chan].active && !chan[c.chan].pcm) WRITE_VOLUME(c.chan,(isMuted[c.chan]?0:(chan[c.chan].vol&127))); } break; case DIV_CMD_PANNING: chan[c.chan].pan=(c.value&0xf0)|(c.value2>>4); WRITE_ATTEN(c.chan,chan[c.chan].pan); break; case DIV_CMD_GET_VOLUME: if (chan[c.chan].std.vol.has) { return chan[c.chan].vol; } return chan[c.chan].outVol; break; case DIV_CMD_PITCH: chan[c.chan].pitch=c.value; chan[c.chan].freqChanged=true; break; case DIV_CMD_NOTE_PORTA: { int destFreq=NOTE_PERIODIC(c.value2+chan[c.chan].sampleNoteDelta); bool return2=false; if (destFreq>chan[c.chan].baseFreq) { chan[c.chan].baseFreq+=c.value; if (chan[c.chan].baseFreq>=destFreq) { chan[c.chan].baseFreq=destFreq; return2=true; } } else { chan[c.chan].baseFreq-=c.value; if (chan[c.chan].baseFreq<=destFreq) { chan[c.chan].baseFreq=destFreq; return2=true; } } chan[c.chan].freqChanged=true; if (chan[c.chan].pcm && parent->song.linearPitch==2) { chan[c.chan].sampleBaseFreq=chan[c.chan].baseFreq; } if (return2) { chan[c.chan].inPorta=false; return 2; } break; } case DIV_CMD_LEGATO: { int whatAMess=c.value+chan[c.chan].sampleNoteDelta+((HACKY_LEGATO_MESS)?(chan[c.chan].std.arp.val):(0)); chan[c.chan].baseFreq=NOTE_PERIODIC(whatAMess); if (chan[c.chan].pcm) { chan[c.chan].sampleBaseFreq=NOTE_FREQUENCY(whatAMess); } chan[c.chan].freqChanged=true; chan[c.chan].note=c.value; chan[c.chan].actualNote=c.value; break; } case DIV_CMD_PRE_PORTA: if (chan[c.chan].active && c.value2) { if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_MIKEY)); } if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) chan[c.chan].baseFreq=NOTE_PERIODIC(chan[c.chan].note); chan[c.chan].inPorta=c.value; break; case DIV_CMD_SAMPLE_POS: chan[c.chan].samplePos=c.value; chan[c.chan].setPos=true; break; case DIV_CMD_GET_VOLMAX: return 127; break; case DIV_CMD_MACRO_OFF: chan[c.chan].std.mask(c.value,true); break; case DIV_CMD_MACRO_ON: chan[c.chan].std.mask(c.value,false); break; case DIV_CMD_MACRO_RESTART: chan[c.chan].std.restart(c.value); break; default: break; } return 1; } void DivPlatformLynx::muteChannel(int ch, bool mute) { isMuted[ch]=mute; if (chan[ch].active) WRITE_VOLUME(ch,(isMuted[ch]?0:(chan[ch].outVol&127))); } int DivPlatformLynx::getOutputCount() { return 2; } void DivPlatformLynx::forceIns() { for (int i=0; i<4; i++) { if (chan[i].active) { chan[i].insChanged=true; chan[i].freqChanged=true; if (!chan[i].pcm) { WRITE_FEEDBACK(i,chan[i].duty.feedback); } } WRITE_ATTEN(i,chan[i].pan); } } void* DivPlatformLynx::getChanState(int ch) { return &chan[ch]; } DivMacroInt* DivPlatformLynx::getChanMacroInt(int ch) { return &chan[ch].std; } unsigned short DivPlatformLynx::getPan(int ch) { return ((chan[ch].pan&0xf0)<<4)|(chan[ch].pan&15); } DivSamplePos DivPlatformLynx::getSamplePos(int ch) { if (ch>=4) return DivSamplePos(); if (!chan[ch].pcm) return DivSamplePos(); return DivSamplePos( chan[ch].sample, chan[ch].samplePos, chan[ch].sampleFreq ); } DivDispatchOscBuffer* DivPlatformLynx::getOscBuffer(int ch) { return oscBuf[ch]; } unsigned char* DivPlatformLynx::getRegisterPool() { return const_cast( mikey->getRegisterPool() ); } int DivPlatformLynx::getRegisterPoolSize() { return 4*8+4; } void DivPlatformLynx::reset() { mikey=std::make_unique(rate); for (int i=0; i<4; i++) { chan[i]=DivPlatformLynx::Channel(); chan[i].std.setEngine(parent); } writes.clear(); if (dumpWrites) { addWrite(0xffffffff,0); } WRITE_STEREO(0); } bool DivPlatformLynx::keyOffAffectsArp(int ch) { return true; } bool DivPlatformLynx::keyOffAffectsPorta(int ch) { return true; } bool DivPlatformLynx::getLegacyAlwaysSetVolume() { return false; } //int DivPlatformLynx::getPortaFloor(int ch) { // return 12; //} void DivPlatformLynx::setFlags(const DivConfig& flags) { tuned=flags.getBool("tuned",false); chipClock=16000000; CHECK_CUSTOM_CLOCK; rate=chipClock/128; for (int i=0; i<4; i++) { oscBuf[i]->setRate(rate); } } void DivPlatformLynx::notifyInsDeletion(void* ins) { for (int i=0; i<4; i++) { chan[i].std.notifyInsDeletion((DivInstrument*)ins); } } void DivPlatformLynx::poke(unsigned int addr, unsigned short val) { rWrite(addr,val); } void DivPlatformLynx::poke(std::vector& wlist) { for (DivRegWrite& i: wlist) rWrite(i.addr, i.val); } int DivPlatformLynx::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { parent=p; dumpWrites=false; skipRegisterWrites=false; for (int i=0; i<4; i++) { isMuted[i]=false; oscBuf[i]=new DivDispatchOscBuffer; } setFlags(flags); reset(); return 4; } void DivPlatformLynx::quit() { for (int i=0; i<4; i++) { delete oscBuf[i]; } mikey.reset(); } DivPlatformLynx::~DivPlatformLynx() { } DivPlatformLynx::MikeyFreqDiv::MikeyFreqDiv(int frequency) { int clamped=clamp(frequency, 36, 16383); auto top=bsr(clamped); if (top>7) { clockDivider=top-7; backup=clamped>>(top-7); } else { clockDivider=0; backup=clamped; } } DivPlatformLynx::MikeyDuty::MikeyDuty(int duty) { //duty: //9: int //8: f11 //7: f10 //6: f7 //5: f5 //4: f4 //3: f3 //2: f2 //1: f1 //0: f0 val=duty; //f7 moved to bit 7 and int moved to bit 5 int_feedback7=((duty&0x40)<<1)|((duty&0x200)>>4); //f11 and f10 moved to bits 7 & 6 feedback=(duty&0x3f)|((duty&0x180)>>1); }