From 2e4c7ec60a87b7c2c72815917c1c9980e80ec982 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Sun, 20 Feb 2022 18:15:15 +0100 Subject: [PATCH 01/11] Initial Atari Lynx Support --- .gitignore | 29 +- CMakeLists.txt | 3 + src/engine/dispatchContainer.cpp | 4 + src/engine/instrument.h | 1 + src/engine/platform/lynx.cpp | 332 +++++++++++++++ src/engine/platform/lynx.h | 74 ++++ src/engine/platform/sound/lynx/Mikey.cpp | 492 +++++++++++++++++++++++ src/engine/platform/sound/lynx/Mikey.hpp | 37 ++ src/engine/song.h | 1 + src/engine/sysDef.cpp | 29 +- src/engine/vgmOps.cpp | 27 +- src/gui/gui.cpp | 6 + src/gui/gui.h | 1 + src/gui/insEdit.cpp | 27 +- 14 files changed, 1038 insertions(+), 25 deletions(-) create mode 100644 src/engine/platform/lynx.cpp create mode 100644 src/engine/platform/lynx.h create mode 100644 src/engine/platform/sound/lynx/Mikey.cpp create mode 100644 src/engine/platform/sound/lynx/Mikey.hpp diff --git a/.gitignore b/.gitignore index d1d24bff..df492a37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,15 @@ -.vscode/ -build/ -release/ -t/ -winbuild/ -win32build/ -macbuild/ -linuxbuild/ -*.swp -.cache/ -.DS_Store -test/songs/ -test/delta/ -test/result/ +.vscode/ +build/ +release/ +t/ +winbuild/ +win32build/ +macbuild/ +linuxbuild/ +*.swp +.cache/ +.DS_Store +test/songs/ +test/delta/ +test/result/ +.vs/ diff --git a/CMakeLists.txt b/CMakeLists.txt index c5d8d6e5..555b6172 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -260,6 +260,8 @@ src/engine/platform/sound/ymfm/ymfm_opn.cpp src/engine/platform/sound/ymfm/ymfm_opz.cpp src/engine/platform/sound/ymfm/ymfm_ssg.cpp +src/engine/platform/sound/lynx/Mikey.cpp + src/engine/platform/ym2610Interface.cpp src/engine/blip_buf.c @@ -295,6 +297,7 @@ src/engine/platform/tia.cpp src/engine/platform/saa.cpp src/engine/platform/amiga.cpp src/engine/platform/dummy.cpp +src/engine/platform/lynx.cpp ) if (WIN32) diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 002629a2..318b53b1 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -35,6 +35,7 @@ #include "platform/saa.h" #include "platform/amiga.h" #include "platform/dummy.h" +#include "platform/lynx.h" #include "../ta-log.h" #include "song.h" @@ -199,6 +200,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do ((DivPlatformSAA1099*)dispatch)->setCore((DivSAACores)saaCore); break; } + case DIV_SYSTEM_LYNX: + dispatch = new DivPlatformLynx; + break; default: logW("this system is not supported yet! using dummy platform.\n"); dispatch=new DivPlatformDummy; diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 45548a42..2d02a4bf 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -47,6 +47,7 @@ enum DivInstrumentType { DIV_INS_POKEY=20, DIV_INS_BEEPER=21, DIV_INS_SWAN=22, + DIV_INS_MIKEY=23, }; struct DivInstrumentFM { diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp new file mode 100644 index 00000000..83e33d38 --- /dev/null +++ b/src/engine/platform/lynx.cpp @@ -0,0 +1,332 @@ +#include "lynx.h" +#include "../engine.h" +#include + +#define rWrite(a,v) {if (!skipRegisterWrites) {mikey->write(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_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 CHIP_DIVIDER 64 + +#if defined( _MSC_VER ) + +#include + +static int bsr(uint16_t v) { + unsigned long idx; + if (_BitScanReverse(&idx,(unsigned long)v)) { + return idx; + } + else { + return -1; + } +} + +#elif defined( __GNUC__ ) + +static int bsr(uint16_t v) +{ + if (v) { + return 16 - __builtin_clz(v); + } + else{ + return -1; + } +} + +#else + +static int bsr(uint16_t v) +{ + uint16_t mask = 0x8000; + for (int i = 31; i >= 0; --i) { + if (v&mask) + return (int)i; + mask>>=1; + } + + return -1; +} + +#endif + +static int32_t clamp(int32_t v, int32_t lo, int32_t hi) +{ + return vhi?hi:v); +} + +const char* regCheatSheetLynx[]={ + "DATA", "0", + NULL +}; + + +const char** DivPlatformLynx::getRegisterSheet() { + return regCheatSheetLynx; +} + +void DivPlatformLynx::acquire(short* bufL, short* bufR, size_t start, size_t len) { + mikey->sampleAudio( bufL + start, bufR + start, len ); +} + +void DivPlatformLynx::tick() { + for (int i=0; i<4; i++) { + chan[i].std.next(); + if (chan[i].std.hadVol) { + chan[i].outVol=((chan[i].vol&127)*MIN(127,chan[i].std.vol))>>7; + WRITE_VOLUME(i,(isMuted[i]?0:(chan[i].outVol&127))); + } + if (chan[i].std.hadArp) { + if (!chan[i].inPorta) { + if (chan[i].std.arpMode) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].std.arp); + chan[i].actualNote=chan[i].std.arp; + } else { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note+chan[i].std.arp); + chan[i].actualNote=chan[i].note+chan[i].std.arp; + } + chan[i].freqChanged=true; + } + } else { + if (chan[i].std.arpMode && chan[i].std.finishedArp) { + chan[i].baseFreq=NOTE_PERIODIC(chan[i].note); + chan[i].actualNote=chan[i].note; + chan[i].freqChanged=true; + } + } + + if (chan[i].freqChanged) { + chan[i].fd=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); + if (chan[i].resetLFSR) { + chan[i].resetLFSR=false; + WRITE_LFSR(i, 0); + WRITE_OTHER(i, 0); + } + if (chan[i].std.hadDuty) { + chan[i].duty = chan[i].std.duty; + WRITE_FEEDBACK(i, chan[i].duty.feedback); + } + WRITE_BACKUP(i, chan[i].fd.backup); + WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); + } + else if (chan[i].std.hadDuty) + { + chan[i].duty = chan[i].std.duty; + WRITE_FEEDBACK(i, chan[i].duty.feedback); + WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); + } + } +} + +int DivPlatformLynx::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + chan[c.chan].freqChanged=true; + chan[c.chan].resetLFSR=true; + chan[c.chan].note=c.value; + chan[c.chan].actualNote=c.value; + } + chan[c.chan].active=true; + WRITE_VOLUME(c.chan,(isMuted[c.chan]?0:(chan[c.chan].vol&127))); + chan[c.chan].std.init(parent->getIns(chan[c.chan].ins)); + break; + case DIV_CMD_NOTE_OFF: + chan[c.chan].active=false; + WRITE_VOLUME(c.chan, 0); + chan[c.chan].std.init(NULL); + 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].std.init(parent->getIns(chan[c.chan].ins)); + break; + case DIV_CMD_VOLUME: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.hasVol) { + chan[c.chan].outVol=c.value; + } + if (chan[c.chan].active) WRITE_VOLUME(c.chan,(isMuted[c.chan]?0:(chan[c.chan].vol&127))); + } + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.hasVol) { + 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); + 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 (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+((chan[c.chan].std.willArp && !chan[c.chan].std.arpMode)?(chan[c.chan].std.arp):(0))); + 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].std.init(parent->getIns(chan[c.chan].ins)); + } + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 127; + break; + case DIV_ALWAYS_SET_VOLUME: + return 0; + 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))); +} + +void DivPlatformLynx::forceIns() { + for (int i=0; i<4; i++) { + if (chan[i].active) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + } + } +} + +void* DivPlatformLynx::getChanState(int ch) { + return &chan[ch]; +} + +void DivPlatformLynx::reset() { + for (int i=0; i<4; i++) { + chan[i]= DivPlatformLynx::Channel(); + } + if (dumpWrites) { + addWrite(0xffffffff,0); + } +} + +bool DivPlatformLynx::keyOffAffectsArp(int ch) { + return true; +} + +bool DivPlatformLynx::keyOffAffectsPorta(int ch) { + return true; +} + +//int DivPlatformLynx::getPortaFloor(int ch) { +// return 12; +//} + +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, unsigned int flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + + for (int i=0; i<4; i++) { + isMuted[i]=false; + } + + chipClock = 16000000; + rate = chipClock/128; + + mikey = std::make_unique(rate); + reset(); + return 4; +} + +void DivPlatformLynx::quit() { + 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=frequency>>(top-7); + } + else + { + clockDivider=0; + backup=frequency; + } +} + +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 + + //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); +} diff --git a/src/engine/platform/lynx.h b/src/engine/platform/lynx.h new file mode 100644 index 00000000..e0f095b8 --- /dev/null +++ b/src/engine/platform/lynx.h @@ -0,0 +1,74 @@ +#ifndef _LYNX_H +#define _LYNX_H + +#include "../dispatch.h" +#include "../macroInt.h" +#include "sound/lynx/Mikey.hpp" + +class DivPlatformLynx: public DivDispatch { + + struct MikeyFreqDiv { + uint8_t clockDivider; + uint8_t backup; + + MikeyFreqDiv(int frequency); + }; + + struct MikeyDuty { + uint8_t int_feedback7; + uint8_t feedback; + + MikeyDuty(int duty); + }; + + struct Channel { + MikeyFreqDiv fd; + MikeyDuty duty; + int baseFreq, pitch, note, actualNote; + unsigned char ins; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, resetLFSR; + signed char vol, outVol; + DivMacroInt std; + Channel(): + fd(0), + duty(0), + baseFreq(0), + pitch(0), + note(0), + actualNote(0), + ins(-1), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + inPorta(false), + resetLFSR(false), + vol(127), + outVol(127) {} + }; + Channel chan[4]; + bool isMuted[4]; + std::unique_ptr mikey; + friend void putDispatchChan(void*,int,int); + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool keyOffAffectsArp(int ch); + bool keyOffAffectsPorta(int ch); + //int getPortaFloor(int ch); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformLynx(); +}; + +#endif diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp new file mode 100644 index 00000000..fa83527c --- /dev/null +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -0,0 +1,492 @@ +#include "Mikey.hpp" +#include +#include +#include +#include +#include +#include + +namespace Lynx +{ + +namespace +{ + +#if defined ( __cpp_lib_bitops ) + +#define popcnt(X) std::popcount(X) + +#elif defined( _MSC_VER ) + +# include + +uint32_t popcnt( uint32_t x ) +{ + return __popcnt( x ); +} + +#elif defined( __GNUC__ ) + +uint32_t popcnt( uint32_t x ) +{ + return __builtin_popcount( x ); +} + +#else + +uint32_t popcnt( uint32_t x ) +{ + int v = 0; + while ( x != 0 ) + { + x &= x - 1; + v++; + } + return v; +} + +#endif + +int32_t clamp( int32_t v, int32_t lo, int32_t hi ) +{ + return v < lo ? lo : ( v > hi ? hi : v ); +} + +class Timer +{ +public: + Timer() : mResetDone{}, mEnableReload{}, mEnableCount{}, mAudShift{}, mValue{}, mCtrlA{-1}, mBackup{}, mTimerDone{} + { + } + + uint64_t setBackup( uint64_t tick, uint8_t backup ) + { + if ( mBackup == backup ) + return 0; + mBackup = backup; + return computeAction( tick ); + } + + uint64_t setControlA( uint64_t tick, uint8_t controlA ) + { + if ( mCtrlA == controlA ) + return 0; + mCtrlA = controlA; + + mResetDone = ( controlA & CONTROLA::RESET_DONE ) != 0; + mEnableReload = ( controlA & CONTROLA::ENABLE_RELOAD ) != 0; + mEnableCount = ( controlA & CONTROLA::ENABLE_COUNT ) != 0; + mAudShift = controlA & CONTROLA::AUD_CLOCK_MASK; + + if ( mResetDone ) + mTimerDone = false; + + return computeAction( tick ); + } + + uint64_t setCount( uint64_t tick, uint8_t value ) + { + if ( mValue == value ) + return 0; + mValue = value; + return computeAction( tick ); + } + + void setControlB( uint8_t controlB ) + { + mTimerDone = ( controlB & CONTROLB::TIMER_DONE ) != 0; + } + + uint64_t fireAction( uint64_t tick ) + { + mTimerDone = true; + + return computeAction( tick ); + } + +private: + + uint64_t computeAction( uint64_t tick ) + { + if ( !mEnableCount || ( mTimerDone && !mEnableReload ) ) + return ~15ull; //infinite + + if ( mValue == 0 || mEnableReload ) + { + mValue = mBackup; + } + + if ( mValue == 0 ) + return ~15ull; //infinite + + //tick value is increased by multipy of 16 (1 MHz resolution) lower bits are unchaged + return tick + ( 1ull + mValue ) * ( 1ull << mAudShift ) * 16; + } + +private: + struct CONTROLA + { + static constexpr uint8_t RESET_DONE = 0b01000000; + static constexpr uint8_t ENABLE_RELOAD = 0b00010000; + static constexpr uint8_t ENABLE_COUNT = 0b00001000; + static constexpr uint8_t AUD_CLOCK_MASK = 0b00000111; + }; + struct CONTROLB + { + static constexpr uint8_t TIMER_DONE = 0b00001000; + }; + +private: + int mAudShift; + int mCtrlA; + bool mResetDone; + bool mEnableReload; + bool mEnableCount; + bool mTimerDone; + uint8_t mBackup; + uint8_t mValue; +}; + +class AudioChannel +{ +public: + AudioChannel( uint32_t number ) : mTimer{}, mNumber{ number }, mShiftRegister{}, mTapSelector{ 1 }, mEnableIntegrate{}, mVolume{}, mOutput{} + { + } + + uint64_t fireAction( uint64_t tick ) + { + trigger(); + return adjust( mTimer.fireAction( tick ) ); + } + + void setVolume( int8_t value ) + { + mVolume = value; + } + + void setFeedback( uint8_t value ) + { + mTapSelector = ( mTapSelector & 0b0011'1100'0000 ) | ( value & 0b0011'1111 ) | ( ( (int)value & 0b1100'0000 ) << 4 ); + } + + void setOutput( uint8_t value ) + { + mOutput = value; + } + + void setShift( uint8_t value ) + { + mShiftRegister = ( mShiftRegister & 0xff00 ) | value; + } + + uint64_t setBackup( uint64_t tick, uint8_t value ) + { + return adjust( mTimer.setBackup( tick, value ) ); + } + + uint64_t setControl( uint64_t tick, uint8_t value ) + { + mTapSelector = ( mTapSelector & 0b1111'0111'1111 ) | ( value & FEEDBACK_7 ); + mEnableIntegrate = ( value & ENABLE_INTEGRATE ) != 0; + return adjust( mTimer.setControlA( tick, value & ~( FEEDBACK_7 | ENABLE_INTEGRATE ) ) ); + } + + uint64_t setCounter( uint64_t tick, uint8_t value ) + { + return adjust( mTimer.setCount( tick, value ) ); + } + + void setOther( uint64_t tick, uint8_t value ) + { + mShiftRegister = mShiftRegister & 0b0000'1111'1111 | ( ( (int)value & 0b1111'0000 ) << 4 ); + mTimer.setControlB( value & 0b0000'1111 ); + } + + int8_t getOutput() + { + return mOutput; + } + +private: + + uint64_t adjust( uint64_t tick ) const + { + //ticks are advancing in 1 MHz resolution, so lower 4 bits are unused. + //timer number is encoded on lowest 2 bits. + return tick | mNumber; + } + + void trigger() + { + uint32_t xorGate = mTapSelector & mShiftRegister; + uint32_t parity = popcnt( xorGate ) & 1; + uint32_t newShift = ( mShiftRegister << 1 ) | ( parity ^ 1 ); + mShiftRegister = newShift; + + if ( mEnableIntegrate ) + { + int32_t temp = mOutput + ( ( newShift & 1 ) ? mVolume : -mVolume ); + mOutput = (int8_t)clamp( temp, (int32_t)std::numeric_limits::min(), (int32_t)std::numeric_limits::max() ); + } + else + { + mOutput = ( newShift & 1 ) ? mVolume : -mVolume; + } + } + +private: + static constexpr uint8_t FEEDBACK_7 = 0b10000000; + static constexpr uint8_t ENABLE_INTEGRATE = 0b00100000; + +private: + Timer mTimer; + uint32_t mNumber; + + uint32_t mShiftRegister; + uint32_t mTapSelector; + bool mEnableIntegrate; + int8_t mVolume; + int8_t mOutput; +}; + +} + + +/* + "Queue" holding event timepoints. + - 4 channel timer fire points + - 1 sample point + Time is in 16 MHz units but only with 1 MHz resolution. + Four LSBs are used to encode event kind 0-3 are channels, 4 is sampling. +*/ +class ActionQueue +{ +public: + ActionQueue() : mTab{ ~15ull | 0, ~15ull | 1, ~15ull | 2, ~15ull | 3, ~15ull | 4 } + { + } + + void push( uint64_t value ) + { + size_t idx = value & 15; + if ( idx < mTab.size() ) + { + if ( value & ~15 ) + { + //writing only non-zero values + mTab[idx] = value; + } + } + } + + uint64_t pop() + { + uint64_t min1 = std::min( mTab[0], mTab[1] ); + uint64_t min2 = std::min( mTab[2], mTab[3] ); + uint64_t min3 = std::min( min1, mTab[4] ); + uint64_t min4 = std::min( min2, min3 ); + + assert( ( min4 & 15 ) < mTab.size() ); + mTab[min4 & 15] = ~15ull | min4 & 15; + + return min4; + } + +private: + std::array mTab; +}; + + +class MikeyPimpl +{ +public: + + struct AudioSample + { + int16_t left; + int16_t right; + }; + + static constexpr uint16_t VOLCNTRL = 0x0; + static constexpr uint16_t FEEDBACK = 0x1; + static constexpr uint16_t OUTPUT = 0x2; + static constexpr uint16_t SHIFT = 0x3; + static constexpr uint16_t BACKUP = 0x4; + static constexpr uint16_t CONTROL = 0x5; + static constexpr uint16_t COUNTER = 0x6; + static constexpr uint16_t OTHER = 0x7; + + static constexpr uint16_t ATTENREG0 = 0x40; + static constexpr uint16_t ATTENREG1 = 0x41; + static constexpr uint16_t ATTENREG2 = 0x42; + static constexpr uint16_t ATTENREG3 = 0x43; + static constexpr uint16_t MPAN = 0x44; + static constexpr uint16_t MSTEREO = 0x50; + + MikeyPimpl() : mAudioChannels{}, mAttenuationLeft{ 0x3c, 0x3c, 0x3c, 0x3c }, mAttenuationRight{ 0x3c, 0x3c, 0x3c, 0x3c }, mPan{ 0xff }, mStereo{} + { + mAudioChannels[0] = std::make_unique( 0 ); + mAudioChannels[1] = std::make_unique( 1 ); + mAudioChannels[2] = std::make_unique( 2 ); + mAudioChannels[3] = std::make_unique( 3 ); + } + + ~MikeyPimpl() {} + + uint64_t write( uint64_t tick, uint8_t address, uint8_t value ) + { + assert( address >= 0x20 ); + + if ( address < 0x40 ) + { + size_t idx = ( address >> 3 ) & 3; + switch ( address & 0x7 ) + { + case VOLCNTRL: + mAudioChannels[( address >> 3 ) & 3]->setVolume( (int8_t)value ); + break; + case FEEDBACK: + mAudioChannels[( address >> 3 ) & 3]->setFeedback( value ); + break; + case OUTPUT: + mAudioChannels[( address >> 3 ) & 3]->setOutput( value ); + break; + case SHIFT: + mAudioChannels[( address >> 3 ) & 3]->setShift( value ); + break; + case BACKUP: + return mAudioChannels[( address >> 3 ) & 3]->setBackup( tick, value ); + case CONTROL: + return mAudioChannels[( address >> 3 ) & 3]->setControl( tick, value ); + case COUNTER: + return mAudioChannels[( address >> 3 ) & 3]->setCounter( tick, value ); + case OTHER: + mAudioChannels[( address >> 3 ) & 3]->setOther( tick, value ); + break; + } + } + else + { + int idx = address & 3; + switch ( address ) + { + case ATTENREG0: + case ATTENREG1: + case ATTENREG2: + case ATTENREG3: + mAttenuationLeft[idx] = ( value & 0x0f ) << 2; + mAttenuationRight[idx] = ( value & 0xf0 ) >> 2; + break; + case MPAN: + mPan = value; + break; + case MSTEREO: + mStereo = value; + break; + default: + break; + } + } + return 0; + } + + uint64_t fireTimer( uint64_t tick ) + { + size_t timer = tick & 0x0f; + assert( timer < 4 ); + return mAudioChannels[timer]->fireAction( tick ); + } + + AudioSample sampleAudio() const + { + int16_t left{}; + int16_t right{}; + std::pair result{}; + + for ( size_t i = 0; i < 4; ++i ) + { + if ( ( mStereo & ( (uint8_t)0x01 << i ) ) == 0 ) + { + const int attenuation = ( mPan & ( (uint8_t)0x01 << i ) ) != 0 ? mAttenuationLeft[i] : 0x3c; + left += mAudioChannels[i]->getOutput() * attenuation; + } + + if ( ( mStereo & ( (uint8_t)0x10 << i ) ) == 0 ) + { + const int attenuation = ( mPan & ( (uint8_t)0x01 << i ) ) != 0 ? mAttenuationRight[i] : 0x3c; + right += mAudioChannels[i]->getOutput() * attenuation; + } + } + + return { left, right }; + } + +private: + + std::array, 4> mAudioChannels; + std::array mAttenuationLeft; + std::array mAttenuationRight; + + uint8_t mPan; + uint8_t mStereo; +}; + + +Mikey::Mikey( uint32_t sampleRate ) : mMikey{ std::make_unique() }, mQueue{ std::make_unique() }, mTick{}, mNextTick{}, mSampleRate{ sampleRate }, mSamplesRemainder{}, mTicksPerSample{ 16000000 / mSampleRate, 16000000 % mSampleRate } +{ + enqueueSampling(); +} + +Mikey::~Mikey() +{ +} + +void Mikey::write( uint8_t address, uint8_t value ) +{ + if ( auto action = mMikey->write( mTick, address, value ) ) + { + mQueue->push( action ); + } +} + +void Mikey::enqueueSampling() +{ + mTick = mNextTick & ~15; + mNextTick = mNextTick + mTicksPerSample.first; + mSamplesRemainder += mTicksPerSample.second; + if ( mSamplesRemainder > mSampleRate ) + { + mSamplesRemainder %= mSampleRate; + mNextTick += 1; + } + + mQueue->push( mNextTick & ~15 | 4 ); +} + +void Mikey::sampleAudio( int16_t* bufL, int16_t* bufR, size_t size ) +{ + size_t i = 0; + while ( i < size ) + { + uint64_t value = mQueue->pop(); + if ( ( value & 4 ) == 0 ) + { + if ( auto newAction = mMikey->fireTimer( value ) ) + { + mQueue->push( newAction ); + } + } + else + { + auto sample = mMikey->sampleAudio(); + bufL[i] = sample.left; + bufR[i] = sample.right; + i += 1; + mTick = value; + enqueueSampling(); + } + } +} + +} diff --git a/src/engine/platform/sound/lynx/Mikey.hpp b/src/engine/platform/sound/lynx/Mikey.hpp new file mode 100644 index 00000000..5da1cdfb --- /dev/null +++ b/src/engine/platform/sound/lynx/Mikey.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace Lynx +{ + +class MikeyPimpl; +class ActionQueue; + +class Mikey +{ +public: + + + Mikey( uint32_t sampleRate ); + ~Mikey(); + + void write( uint8_t address, uint8_t value ); + void sampleAudio( int16_t* bufL, int16_t* bufR, size_t size ); + +private: + void enqueueSampling(); + +private: + + std::unique_ptr mMikey; + std::unique_ptr mQueue; + uint64_t mTick; + uint64_t mNextTick; + uint32_t mSampleRate; + uint32_t mSamplesRemainder; + std::pair mTicksPerSample; +}; + +} diff --git a/src/engine/song.h b/src/engine/song.h index 4582077e..78ce4dc2 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -87,6 +87,7 @@ enum DivSystem { DIV_SYSTEM_YM2610_FULL, DIV_SYSTEM_YM2610_FULL_EXT, DIV_SYSTEM_OPLL_DRUMS, + DIV_SYSTEM_LYNX }; struct DivSong { diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 55fb3a19..494a2999 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -129,6 +129,8 @@ DivSystem DivEngine::systemFromFile(unsigned char val) { return DIV_SYSTEM_YM2610_FULL_EXT; case 0xa7: return DIV_SYSTEM_OPLL_DRUMS; + case 0xa8: + return DIV_SYSTEM_LYNX; } return DIV_SYSTEM_NULL; } @@ -242,6 +244,8 @@ unsigned char DivEngine::systemToFile(DivSystem val) { return 0xa6; case DIV_SYSTEM_OPLL_DRUMS: return 0xa7; + case DIV_SYSTEM_LYNX: + return 0xa8; case DIV_SYSTEM_NULL: return 0; @@ -354,6 +358,8 @@ int DivEngine::getChannelCount(DivSystem sys) { return 17; case DIV_SYSTEM_OPLL_DRUMS: return 11; + case DIV_SYSTEM_LYNX: + return 4; } return 0; } @@ -474,6 +480,8 @@ const char* DivEngine::getSystemName(DivSystem sys) { return "Yamaha OPL3 with drums"; case DIV_SYSTEM_OPLL_DRUMS: return "Yamaha OPLL with drums"; + case DIV_SYSTEM_LYNX: + return "Atari Lynx"; } return "Unknown"; } @@ -589,6 +597,8 @@ const char* DivEngine::getSystemChips(DivSystem sys) { return "Yamaha YM2610 (extended channel 2)"; case DIV_SYSTEM_OPLL_DRUMS: return "Yamaha YM2413 with drums"; + case DIV_SYSTEM_LYNX: + return "Mikey"; } return "Unknown"; } @@ -681,7 +691,7 @@ const char* chanNames[36][24]={ {"FM 1", "FM 2", "FM 3", "FM 4", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610 {"FM 1", "FM 2 OP1", "FM 2 OP2", "FM 2 OP3", "FM 2 OP4", "FM 3", "FM 4", "PSG 1", "PSG 2", "PSG 3", "ADPCM-A 1", "ADPCM-A 2", "ADPCM-A 3", "ADPCM-A 4", "ADPCM-A 5", "ADPCM-A 6", "ADPCM-B"}, // YM2610 (extended channel 2) {"PSG 1", "PSG 2", "PSG 3"}, // AY-3-8910 - {"Channel 1", "Channel 2", "Channel 3", "Channel 4"}, // Amiga/POKEY/Swan + {"Channel 1", "Channel 2", "Channel 3", "Channel 4"}, // Amiga/POKEY/Swan/Lynx {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6", "FM 7", "FM 8"}, // YM2151/YM2414 {"FM 1", "FM 2", "FM 3", "FM 4", "FM 5", "FM 6"}, // YM2612 {"Channel 1", "Channel 2"}, // TIA @@ -720,7 +730,7 @@ const char* chanShortNames[36][24]={ {"F1", "F2", "F3", "F4", "S1", "S2", "S3", "P1", "P2", "P3", "P4", "P5", "P6", "B"}, // YM2610 {"F1", "O1", "O2", "O3", "O4", "F3", "F4", "S1", "S2", "S3", "P1", "P2", "P3", "P4", "P5", "P6", "B"}, // YM2610 (extended channel 2) {"S1", "S2", "S3"}, // AY-3-8910 - {"CH1", "CH2", "CH3", "CH4"}, // Amiga + {"CH1", "CH2", "CH3", "CH4"}, // Amiga/Lynx {"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8"}, // YM2151 {"F1", "F2", "F3", "F4", "F5", "F6"}, // YM2612 {"CH1", "CH2"}, // TIA @@ -746,7 +756,7 @@ const char* chanShortNames[36][24]={ {"F1", "F2", "F3", "Q1", "Q2", "Q3", "Q4", "Q5", "Q6", "BD", "SD", "TM", "TP", "HH"}, // OPL3 4-op + drums }; -const int chanTypes[36][24]={ +const int chanTypes[37][24]={ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}, // YMU759 {0, 0, 0, 0, 0, 0, 1, 1, 1, 2}, // Genesis {0, 0, 5, 5, 5, 5, 0, 0, 0, 1, 1, 1, 2}, // Genesis (extended channel 3) @@ -783,9 +793,10 @@ const int chanTypes[36][24]={ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 drums {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // OPL3 4-op {0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}, // OPL3 4-op + drums + {3, 3, 3, 3}, //Lynx }; -const DivInstrumentType chanPrefType[42][24]={ +const DivInstrumentType chanPrefType[43][24]={ {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM}, // YMU759 {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis {DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_FM, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD, DIV_INS_STD}, // Genesis (extended channel 3) @@ -828,6 +839,7 @@ const DivInstrumentType chanPrefType[42][24]={ {DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER, DIV_INS_BEEPER}, // ZX beeper {DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN, DIV_INS_SWAN}, // Swan {DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ, DIV_INS_OPZ}, // Z + {DIV_INS_MIKEY, DIV_INS_MIKEY, DIV_INS_MIKEY, DIV_INS_MIKEY}, // Lynx }; const char* DivEngine::getChannelName(int chan) { @@ -881,6 +893,7 @@ const char* DivEngine::getChannelName(int chan) { case DIV_SYSTEM_AMIGA: case DIV_SYSTEM_POKEY: case DIV_SYSTEM_SWAN: + case DIV_SYSTEM_LYNX: return chanNames[12][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_YM2151: @@ -1011,6 +1024,7 @@ const char* DivEngine::getChannelShortName(int chan) { case DIV_SYSTEM_AMIGA: case DIV_SYSTEM_POKEY: case DIV_SYSTEM_SWAN: + case DIV_SYSTEM_LYNX: return chanShortNames[12][dispatchChanOfChan[chan]]; break; case DIV_SYSTEM_YM2151: @@ -1214,6 +1228,9 @@ int DivEngine::getChannelType(int chan) { case DIV_SYSTEM_AY8930: return chanTypes[17][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_LYNX: + return chanTypes[36][dispatchChanOfChan[chan]]; + break; } return 1; } @@ -1355,6 +1372,9 @@ DivInstrumentType DivEngine::getPreferInsType(int chan) { case DIV_SYSTEM_OPZ: return chanPrefType[41][dispatchChanOfChan[chan]]; break; + case DIV_SYSTEM_LYNX: + return chanPrefType[42][dispatchChanOfChan[chan]]; + break; } return DIV_INS_FM; } @@ -1375,6 +1395,7 @@ bool DivEngine::isVGMExportable(DivSystem which) { case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8930: case DIV_SYSTEM_SAA1099: + case DIV_SYSTEM_LYNX: return true; default: return false; diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index 84c673e5..de47a5b1 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -260,6 +260,14 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeC(0); } break; + case DIV_SYSTEM_LYNX: + w->writeC(0x4e); + w->writeC(0x40); + w->writeC(0xff); //panning + w->writeC(0x4e); + w->writeC(0x50); + w->writeC(0x00); //stereo + break; default: break; } @@ -377,6 +385,11 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write w->writeC((isSecond?0x80:0)|(write.addr&0xff)); w->writeC(write.val); break; + case DIV_SYSTEM_LYNX: + w->writeC(0x4e); + w->writeC(write.addr&0xff); + w->writeC(write.val&0xff); + break; default: logW("write not handled!\n"); break; @@ -456,6 +469,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { int hasX1=0; int hasC352=0; int hasGA20=0; + int hasLynx=0; int howManyChips=0; @@ -666,6 +680,16 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { hasOPM|=0x40000000; howManyChips++; } + case DIV_SYSTEM_LYNX: + if (!hasLynx) { + hasLynx=disCont[i].dispatch->chipClock; + willExport[i] = true; + } else if (!(hasLynx&0x40000000)) { + isSecond[i]=true; + willExport[i]=true; + hasLynx|=0x40000000; + howManyChips++; + } break; default: break; @@ -752,7 +776,8 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { w->writeI(hasX1); w->writeI(hasC352); w->writeI(hasGA20); - for (int i=0; i<7; i++) { // reserved + w->writeI(hasLynx); + for (int i=0; i<6; i++) { // reserved w->writeI(0); } diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 953f6f0d..e44fca86 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1156,6 +1156,10 @@ void FurnaceGUI::drawInsList() { ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_SWAN]); name=fmt::sprintf(ICON_FA_GAMEPAD " %.2X: %s##_INS%d\n",i,ins->name,i); break; + case DIV_INS_MIKEY: + ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_MIKEY]); + name=fmt::sprintf(ICON_FA_BAR_CHART " %.2X: %s##_INS%d\n",i,ins->name,i); + break; default: ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_INSTR_UNKNOWN]); name=fmt::sprintf(ICON_FA_QUESTION " %.2X: %s##_INS%d\n",i,ins->name,i); @@ -4390,6 +4394,7 @@ bool FurnaceGUI::loop() { sysAddOption(DIV_SYSTEM_TIA); sysAddOption(DIV_SYSTEM_SAA1099); sysAddOption(DIV_SYSTEM_AY8930); + sysAddOption(DIV_SYSTEM_LYNX); ImGui::EndMenu(); } if (ImGui::BeginMenu("configure system...")) { @@ -5132,6 +5137,7 @@ void FurnaceGUI::applyUISettings() { GET_UI_COLOR(GUI_COLOR_INSTR_POKEY,ImVec4(0.5f,1.0f,0.3f,1.0f)); GET_UI_COLOR(GUI_COLOR_INSTR_BEEPER,ImVec4(0.0f,1.0f,0.0f,1.0f)); GET_UI_COLOR(GUI_COLOR_INSTR_SWAN,ImVec4(0.3f,0.5f,1.0f,1.0f)); + GET_UI_COLOR(GUI_COLOR_INSTR_MIKEY,ImVec4(0.5f,1.0f,0.3f,1.0f)); GET_UI_COLOR(GUI_COLOR_INSTR_UNKNOWN,ImVec4(0.3f,0.3f,0.3f,1.0f)); GET_UI_COLOR(GUI_COLOR_CHANNEL_FM,ImVec4(0.2f,0.8f,1.0f,1.0f)); GET_UI_COLOR(GUI_COLOR_CHANNEL_PULSE,ImVec4(0.4f,1.0f,0.2f,1.0f)); diff --git a/src/gui/gui.h b/src/gui/gui.h index 3e15b141..bf671234 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -66,6 +66,7 @@ enum FurnaceGUIColors { GUI_COLOR_INSTR_POKEY, GUI_COLOR_INSTR_BEEPER, GUI_COLOR_INSTR_SWAN, + GUI_COLOR_INSTR_MIKEY, GUI_COLOR_INSTR_UNKNOWN, GUI_COLOR_CHANNEL_FM, GUI_COLOR_CHANNEL_PULSE, diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 66e18f9b..6f2b926c 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -26,7 +26,7 @@ #include #include "plot_nolerp.h" -const char* insTypes[23]={ +const char* insTypes[24]={ "Standard", "FM (4-operator)", "Game Boy", @@ -49,7 +49,8 @@ const char* insTypes[23]={ "FM (OPZ)", "POKEY", "PC Beeper", - "WonderSwan" + "WonderSwan", + "Atari Lynx" }; const char* ssgEnvTypes[8]={ @@ -111,6 +112,10 @@ const char* c64SpecialBits[3]={ "sync", "ring", NULL }; +const char* mikeyFeedbackBits[11] = { + "0", "1", "2", "3", "4", "5", "7", "10", "11", "int", NULL +}; + const int orderedOps[4]={ 0, 2, 1, 3 }; @@ -671,9 +676,9 @@ void FurnaceGUI::drawInsEdit() { } else { DivInstrument* ins=e->song.ins[curIns]; ImGui::InputText("Name",&ins->name); - if (ins->type<0 || ins->type>22) ins->type=DIV_INS_FM; + if (ins->type<0 || ins->type>23) ins->type=DIV_INS_FM; int insType=ins->type; - if (ImGui::Combo("Type",&insType,insTypes,23)) { + if (ImGui::Combo("Type",&insType,insTypes,24)) { ins->type=(DivInstrumentType)insType; } @@ -929,7 +934,7 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_AMIGA) { volMax=64; } - if (ins->type==DIV_INS_FM) { + if (ins->type==DIV_INS_FM || ins->type == DIV_INS_MIKEY) { volMax=127; } if (ins->type==DIV_INS_GB) { @@ -954,6 +959,10 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_FM) { dutyLabel="Noise Freq"; } + if (ins->type == DIV_INS_MIKEY) { + dutyLabel = "Duty/Int"; + dutyMax = 10; + } if (ins->type==DIV_INS_AY8930) { dutyMax=255; } @@ -972,6 +981,7 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_C64) waveMax=4; if (ins->type==DIV_INS_SAA1099) waveMax=2; if (ins->type==DIV_INS_FM) waveMax=0; + if (ins->type==DIV_INS_MIKEY) waveMax=0; const char** waveNames=ayShapeBits; if (ins->type==DIV_INS_C64) waveNames=c64ShapeBits; @@ -992,7 +1002,12 @@ void FurnaceGUI::drawInsEdit() { } NORMAL_MACRO(ins->std.arpMacro,ins->std.arpMacroLen,ins->std.arpMacroLoop,ins->std.arpMacroRel,arpMacroScroll,arpMacroScroll+24,"arp","Arpeggio",160,ins->std.arpMacroOpen,false,NULL,true,&arpMacroScroll,(arpMode?0:-80),0,0,&ins->std.arpMacroMode,uiColors[GUI_COLOR_MACRO_PITCH],mmlString[1],-92,94,(ins->std.arpMacroMode?(¯oHoverNote):NULL)); if (dutyMax>0) { - NORMAL_MACRO(ins->std.dutyMacro,ins->std.dutyMacroLen,ins->std.dutyMacroLoop,ins->std.dutyMacroRel,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL); + if (ins->type == DIV_INS_MIKEY) { + NORMAL_MACRO(ins->std.dutyMacro,ins->std.dutyMacroLen,ins->std.dutyMacroLoop,ins->std.dutyMacroRel,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacroOpen,true,mikeyFeedbackBits,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL); + } + else { + NORMAL_MACRO(ins->std.dutyMacro,ins->std.dutyMacroLen,ins->std.dutyMacroLoop,ins->std.dutyMacroRel,0,dutyMax,"duty",dutyLabel,160,ins->std.dutyMacroOpen,false,NULL,false,NULL,0,0,0,NULL,uiColors[GUI_COLOR_MACRO_OTHER],mmlString[2],0,dutyMax,NULL); + } } if (waveMax>0) { NORMAL_MACRO(ins->std.waveMacro,ins->std.waveMacroLen,ins->std.waveMacroLoop,ins->std.waveMacroRel,0,waveMax,"wave","Waveform",bitMode?64:160,ins->std.waveMacroOpen,bitMode,waveNames,false,NULL,0,0,((ins->type==DIV_INS_AY || ins->type==DIV_INS_AY8930)?1:0),NULL,uiColors[GUI_COLOR_MACRO_WAVE],mmlString[3],0,waveMax,NULL); From 6e79e84e539db5421a3e971d9c29265b9987921f Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Mon, 21 Feb 2022 12:41:06 +0100 Subject: [PATCH 02/11] Added panning and load LFSR commands. --- src/engine/dispatch.h | 2 ++ src/engine/platform/lynx.cpp | 39 +++++++++++++++++++++++++++--------- src/engine/platform/lynx.h | 10 +++++---- src/engine/playback.cpp | 7 +++++++ src/engine/vgmOps.cpp | 11 +++++++--- src/gui/gui.cpp | 1 + src/gui/settings.cpp | 1 + 7 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index 0740c783..b9048158 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -97,6 +97,8 @@ enum DivDispatchCmds { DIV_CMD_SAA_ENVELOPE, + DIV_CMD_LYNX_LFSR_LOAD, + DIV_ALWAYS_SET_VOLUME, DIV_CMD_MAX diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 83e33d38..533fbdab 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -10,6 +10,7 @@ #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 CHIP_DIVIDER 64 @@ -68,6 +69,19 @@ const char* regCheatSheetLynx[]={ const char** DivPlatformLynx::getRegisterSheet() { return regCheatSheetLynx; +} + +const char* DivPlatformLynx::getEffectName(unsigned char effect) { + switch (effect) + { + case 0x30: case 0x31: case 0x32: case 0x33: + case 0x34: case 0x35: case 0x36: case 0x37: + case 0x38: case 0x39: case 0x3a: case 0x3b: + case 0x3c: case 0x3d: case 0x3e: case 0x3f: + return "3xxx: Load LFSR (0 to FFF)"; + break; + } + return NULL; } void DivPlatformLynx::acquire(short* bufL, short* bufR, size_t start, size_t len) { @@ -101,21 +115,20 @@ void DivPlatformLynx::tick() { } if (chan[i].freqChanged) { - chan[i].fd=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); - if (chan[i].resetLFSR) { - chan[i].resetLFSR=false; - WRITE_LFSR(i, 0); - WRITE_OTHER(i, 0); + if (chan[i].lfsr >= 0) { + WRITE_LFSR(i, (chan[i].lfsr&0xff)); + WRITE_OTHER(i, ((chan[i].lfsr&0xf00)>>4)); + chan[i].lfsr=-1; } + chan[i].fd=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,true); if (chan[i].std.hadDuty) { - chan[i].duty = chan[i].std.duty; + chan[i].duty=chan[i].std.duty; WRITE_FEEDBACK(i, chan[i].duty.feedback); } WRITE_BACKUP(i, chan[i].fd.backup); WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); } - else if (chan[i].std.hadDuty) - { + else if (chan[i].std.hadDuty) { chan[i].duty = chan[i].std.duty; WRITE_FEEDBACK(i, chan[i].duty.feedback); WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); @@ -129,9 +142,10 @@ int DivPlatformLynx::dispatch(DivCommand c) { if (c.value!=DIV_NOTE_NULL) { chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); chan[c.chan].freqChanged=true; - chan[c.chan].resetLFSR=true; chan[c.chan].note=c.value; chan[c.chan].actualNote=c.value; + if (chan[c.chan].lfsr<0) + chan[c.chan].lfsr=0; } chan[c.chan].active=true; WRITE_VOLUME(c.chan,(isMuted[c.chan]?0:(chan[c.chan].vol&127))); @@ -142,6 +156,10 @@ int DivPlatformLynx::dispatch(DivCommand c) { WRITE_VOLUME(c.chan, 0); chan[c.chan].std.init(NULL); break; + case DIV_CMD_LYNX_LFSR_LOAD: + chan[c.chan].freqChanged=true; + chan[c.chan].lfsr=c.value; + break; case DIV_CMD_NOTE_OFF_ENV: case DIV_CMD_ENV_RELEASE: chan[c.chan].std.release(); @@ -159,6 +177,9 @@ int DivPlatformLynx::dispatch(DivCommand c) { if (chan[c.chan].active) WRITE_VOLUME(c.chan,(isMuted[c.chan]?0:(chan[c.chan].vol&127))); } break; + case DIV_CMD_PANNING: + WRITE_ATTEN(c.chan, c.value); + break; case DIV_CMD_GET_VOLUME: if (chan[c.chan].std.hasVol) { return chan[c.chan].vol; diff --git a/src/engine/platform/lynx.h b/src/engine/platform/lynx.h index e0f095b8..a45cdb83 100644 --- a/src/engine/platform/lynx.h +++ b/src/engine/platform/lynx.h @@ -22,20 +22,22 @@ class DivPlatformLynx: public DivDispatch { }; struct Channel { + DivMacroInt std; MikeyFreqDiv fd; MikeyDuty duty; - int baseFreq, pitch, note, actualNote; + int baseFreq, pitch, note, actualNote, lfsr; unsigned char ins; - bool active, insChanged, freqChanged, keyOn, keyOff, inPorta, resetLFSR; + bool active, insChanged, freqChanged, keyOn, keyOff, inPorta; signed char vol, outVol; - DivMacroInt std; Channel(): + std(), fd(0), duty(0), baseFreq(0), pitch(0), note(0), actualNote(0), + lfsr(-1), ins(-1), active(false), insChanged(true), @@ -43,7 +45,6 @@ class DivPlatformLynx: public DivDispatch { keyOn(false), keyOff(false), inPorta(false), - resetLFSR(false), vol(127), outVol(127) {} }; @@ -66,6 +67,7 @@ class DivPlatformLynx: public DivDispatch { void poke(unsigned int addr, unsigned short val); void poke(std::vector& wlist); const char** getRegisterSheet(); + const char* getEffectName( unsigned char effect ); int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); void quit(); ~DivPlatformLynx(); diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 85811349..301bc434 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -218,6 +218,13 @@ bool DivEngine::perSystemEffect(int ch, unsigned char effect, unsigned char effe return false; } break; + case DIV_SYSTEM_LYNX: + if (effect>=0x30 && effect<0x40) { + int value = ((int)(effect&0x0f)<<8)|effectVal; + dispatchCmd(DivCommand(DIV_CMD_LYNX_LFSR_LOAD,ch,value)); + break; + } + return false; default: return false; } diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index 31c3609a..99089131 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -262,11 +262,16 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write break; case DIV_SYSTEM_LYNX: w->writeC(0x4e); - w->writeC(0x40); - w->writeC(0xff); //panning + w->writeC(0x44); + w->writeC(0xff); //stereo attenuation select w->writeC(0x4e); w->writeC(0x50); - w->writeC(0x00); //stereo + w->writeC(0x00); //stereo channel disable + for (int i=0; i<4; i++) { //stereo attenuation value + w->writeC(0x4e); + w->writeC(0x40+i); + w->writeC(0xff); + } break; default: break; diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 617feacf..a61a0bc7 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -4602,6 +4602,7 @@ bool FurnaceGUI::loop() { sysChangeOption(i,DIV_SYSTEM_TIA); sysChangeOption(i,DIV_SYSTEM_SAA1099); sysChangeOption(i,DIV_SYSTEM_AY8930); + sysChangeOption(i,DIV_SYSTEM_LYNX); ImGui::EndMenu(); } } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index cf73921f..8bf46a53 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -461,6 +461,7 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_POKEY,"POKEY"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_BEEPER,"PC Beeper"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_SWAN,"WonderSwan"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_MIKEY,"Lynx"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_UNKNOWN,"Other/Unknown"); ImGui::TreePop(); } From de6cbba4727e78a4a7697c899b4e520fbaae9757 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Mon, 21 Feb 2022 19:58:14 +0100 Subject: [PATCH 03/11] Fixed gcc errors --- src/engine/platform/sound/lynx/Mikey.cpp | 24 ++++++++++++------------ src/engine/vgmOps.cpp | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index fa83527c..efd1a095 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -55,7 +55,7 @@ int32_t clamp( int32_t v, int32_t lo, int32_t hi ) class Timer { public: - Timer() : mResetDone{}, mEnableReload{}, mEnableCount{}, mAudShift{}, mValue{}, mCtrlA{-1}, mBackup{}, mTimerDone{} + Timer() : mAudShift{}, mCtrlA{ -1 }, mResetDone{}, mEnableReload{}, mEnableCount{}, mTimerDone{}, mBackup{}, mValue{} { } @@ -199,7 +199,7 @@ public: void setOther( uint64_t tick, uint8_t value ) { - mShiftRegister = mShiftRegister & 0b0000'1111'1111 | ( ( (int)value & 0b1111'0000 ) << 4 ); + mShiftRegister = ( mShiftRegister & 0b0000'1111'1111 ) | ( ( (int)value & 0b1111'0000 ) << 4 ); mTimer.setControlB( value & 0b0000'1111 ); } @@ -288,7 +288,7 @@ public: uint64_t min4 = std::min( min2, min3 ); assert( ( min4 & 15 ) < mTab.size() ); - mTab[min4 & 15] = ~15ull | min4 & 15; + mTab[min4 & 15] = ~15ull | ( min4 & 15 ); return min4; } @@ -344,25 +344,25 @@ public: switch ( address & 0x7 ) { case VOLCNTRL: - mAudioChannels[( address >> 3 ) & 3]->setVolume( (int8_t)value ); + mAudioChannels[idx]->setVolume( (int8_t)value ); break; case FEEDBACK: - mAudioChannels[( address >> 3 ) & 3]->setFeedback( value ); + mAudioChannels[idx]->setFeedback( value ); break; case OUTPUT: - mAudioChannels[( address >> 3 ) & 3]->setOutput( value ); + mAudioChannels[idx]->setOutput( value ); break; case SHIFT: - mAudioChannels[( address >> 3 ) & 3]->setShift( value ); + mAudioChannels[idx]->setShift( value ); break; case BACKUP: - return mAudioChannels[( address >> 3 ) & 3]->setBackup( tick, value ); + return mAudioChannels[idx]->setBackup( tick, value ); case CONTROL: - return mAudioChannels[( address >> 3 ) & 3]->setControl( tick, value ); + return mAudioChannels[idx]->setControl( tick, value ); case COUNTER: - return mAudioChannels[( address >> 3 ) & 3]->setCounter( tick, value ); + return mAudioChannels[idx]->setCounter( tick, value ); case OTHER: - mAudioChannels[( address >> 3 ) & 3]->setOther( tick, value ); + mAudioChannels[idx]->setOther( tick, value ); break; } } @@ -461,7 +461,7 @@ void Mikey::enqueueSampling() mNextTick += 1; } - mQueue->push( mNextTick & ~15 | 4 ); + mQueue->push( ( mNextTick & ~15 ) | 4 ); } void Mikey::sampleAudio( int16_t* bufL, int16_t* bufR, size_t size ) diff --git a/src/engine/vgmOps.cpp b/src/engine/vgmOps.cpp index 99089131..e8c01c97 100644 --- a/src/engine/vgmOps.cpp +++ b/src/engine/vgmOps.cpp @@ -685,6 +685,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) { hasOPM|=0x40000000; howManyChips++; } + break; case DIV_SYSTEM_LYNX: if (!hasLynx) { hasLynx=disCont[i].dispatch->chipClock; From 5093a1ca555ac005085cfdc87dee08287244693a Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Mon, 21 Feb 2022 20:00:08 +0100 Subject: [PATCH 04/11] Disabling LYNX VGM export --- src/engine/sysDef.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index cf0c1c29..1b9a701d 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1395,7 +1395,6 @@ bool DivEngine::isVGMExportable(DivSystem which) { case DIV_SYSTEM_AY8910: case DIV_SYSTEM_AY8930: case DIV_SYSTEM_SAA1099: - case DIV_SYSTEM_LYNX: return true; default: return false; From bbd8669a1ac7b2d40080b4eaa894c6e8ec6ca086 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Mon, 21 Feb 2022 20:17:16 +0100 Subject: [PATCH 05/11] Missing license --- src/engine/platform/lynx.cpp | 19 +++++++++++++++++++ src/engine/platform/sound/lynx/Mikey.cpp | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 533fbdab..c5705dcd 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -1,3 +1,22 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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 diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index efd1a095..408a97ab 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -1,3 +1,22 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 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 "Mikey.hpp" #include #include From 37ceb1542f1faeda1227de9047494ac3d1506d72 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Mon, 21 Feb 2022 20:43:02 +0100 Subject: [PATCH 06/11] Further build errors correction --- src/engine/platform/sound/lynx/Mikey.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index 408a97ab..f54e65ac 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -24,6 +24,7 @@ #include #include #include +#include namespace Lynx { @@ -421,7 +422,6 @@ public: { int16_t left{}; int16_t right{}; - std::pair result{}; for ( size_t i = 0; i < 4; ++i ) { From 69fb99bafdf21fb9342b7c27a1c77fce7a829273 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Tue, 22 Feb 2022 12:52:05 +0100 Subject: [PATCH 07/11] Implementing register pool. Slightly reworking emulation core. --- src/engine/platform/lynx.cpp | 12 +- src/engine/platform/lynx.h | 2 + src/engine/platform/sound/lynx/Mikey.cpp | 198 +++++++++++++++-------- src/engine/platform/sound/lynx/Mikey.hpp | 2 + 4 files changed, 142 insertions(+), 72 deletions(-) diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index c5705dcd..7545a986 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -144,8 +144,8 @@ void DivPlatformLynx::tick() { chan[i].duty=chan[i].std.duty; WRITE_FEEDBACK(i, chan[i].duty.feedback); } - WRITE_BACKUP(i, chan[i].fd.backup); WRITE_CONTROL(i, (chan[i].fd.clockDivider|0x18|chan[i].duty.int_feedback7)); + WRITE_BACKUP( i, chan[i].fd.backup ); } else if (chan[i].std.hadDuty) { chan[i].duty = chan[i].std.duty; @@ -272,6 +272,16 @@ void DivPlatformLynx::forceIns() { void* DivPlatformLynx::getChanState(int ch) { return &chan[ch]; +} + +unsigned char* DivPlatformLynx::getRegisterPool() +{ + return const_cast( mikey->getRegisterPool() ); +} + +int DivPlatformLynx::getRegisterPoolSize() +{ + return 4*8+4; } void DivPlatformLynx::reset() { diff --git a/src/engine/platform/lynx.h b/src/engine/platform/lynx.h index a45cdb83..2fa6cf08 100644 --- a/src/engine/platform/lynx.h +++ b/src/engine/platform/lynx.h @@ -56,6 +56,8 @@ class DivPlatformLynx: public DivDispatch { void acquire(short* bufL, short* bufR, size_t start, size_t len); int dispatch(DivCommand c); void* getChanState(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); void reset(); void forceIns(); void tick(); diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index f54e65ac..dadbe37e 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -32,6 +32,8 @@ namespace Lynx namespace { +static constexpr int64_t CNT_MAX = std::numeric_limits::max() & ~15; + #if defined ( __cpp_lib_bitops ) #define popcnt(X) std::popcount(X) @@ -75,41 +77,29 @@ int32_t clamp( int32_t v, int32_t lo, int32_t hi ) class Timer { public: - Timer() : mAudShift{}, mCtrlA{ -1 }, mResetDone{}, mEnableReload{}, mEnableCount{}, mTimerDone{}, mBackup{}, mValue{} + Timer() : mValueUpdateTick{}, mAudShift {}, mCtrlA{ -1 }, mEnableReload{}, mEnableCount{}, mTimerDone{}, mBackup{ 0 }, mValue{ 0 } { } - uint64_t setBackup( uint64_t tick, uint8_t backup ) + int64_t setBackup( int64_t tick, uint8_t backup ) { - if ( mBackup == backup ) - return 0; mBackup = backup; return computeAction( tick ); } - uint64_t setControlA( uint64_t tick, uint8_t controlA ) + int64_t setControlA( int64_t tick, uint8_t controlA ) { - if ( mCtrlA == controlA ) - return 0; - mCtrlA = controlA; - - mResetDone = ( controlA & CONTROLA::RESET_DONE ) != 0; + mTimerDone ^= ( controlA & CONTROLA::RESET_DONE ) != 0; mEnableReload = ( controlA & CONTROLA::ENABLE_RELOAD ) != 0; mEnableCount = ( controlA & CONTROLA::ENABLE_COUNT ) != 0; mAudShift = controlA & CONTROLA::AUD_CLOCK_MASK; - if ( mResetDone ) - mTimerDone = false; - return computeAction( tick ); } - uint64_t setCount( uint64_t tick, uint8_t value ) + int64_t setCount( int64_t tick, uint8_t value ) { - if ( mValue == value ) - return 0; - mValue = value; - return computeAction( tick ); + return computeTriggerTime( tick ); } void setControlB( uint8_t controlB ) @@ -117,30 +107,61 @@ public: mTimerDone = ( controlB & CONTROLB::TIMER_DONE ) != 0; } - uint64_t fireAction( uint64_t tick ) + int64_t fireAction( int64_t tick ) { mTimerDone = true; return computeAction( tick ); } + uint8_t getBackup() const + { + return mBackup; + } + + uint8_t getCount( int64_t tick ) + { + updateValue( tick ); + return mValue; + } + private: - uint64_t computeAction( uint64_t tick ) + int64_t scaleDiff( int64_t older, int64_t newer ) const { - if ( !mEnableCount || ( mTimerDone && !mEnableReload ) ) - return ~15ull; //infinite + int64_t const mask = ~0 << ( mAudShift + 4 ); + return ( ( newer & mask ) - ( older & mask ) ) >> ( mAudShift + 4 ); + } - if ( mValue == 0 || mEnableReload ) + void updateValue( int64_t tick ) + { + if ( mEnableCount ) + mValue = (uint8_t)std::max( 0ll, mValue - scaleDiff( mValueUpdateTick, tick ) ); + mValueUpdateTick = tick; + } + + int64_t computeTriggerTime( int64_t tick ) + { + if ( mEnableCount && mValue != 0 ) + { + //tick value is increased by multipy of 16 (1 MHz resolution) lower bits are unchanged + return tick + ( 1ull + mValue ) * ( 1ull << ( mAudShift + 4 ) ); + } + else + { + return CNT_MAX; //infinite + } + } + + int64_t computeAction( int64_t tick ) + { + updateValue( tick ); + if ( mValue == 0 && mEnableReload ) { mValue = mBackup; } - if ( mValue == 0 ) - return ~15ull; //infinite - - //tick value is increased by multipy of 16 (1 MHz resolution) lower bits are unchaged - return tick + ( 1ull + mValue ) * ( 1ull << mAudShift ) * 16; + return computeTriggerTime( tick ); } private: @@ -157,9 +178,9 @@ private: }; private: + int64_t mValueUpdateTick; int mAudShift; int mCtrlA; - bool mResetDone; bool mEnableReload; bool mEnableCount; bool mTimerDone; @@ -170,11 +191,11 @@ private: class AudioChannel { public: - AudioChannel( uint32_t number ) : mTimer{}, mNumber{ number }, mShiftRegister{}, mTapSelector{ 1 }, mEnableIntegrate{}, mVolume{}, mOutput{} + AudioChannel( uint32_t number ) : mTimer{}, mNumber{ number }, mShiftRegister{}, mTapSelector{}, mEnableIntegrate{}, mVolume{}, mOutput{}, mCtrlA{} { } - uint64_t fireAction( uint64_t tick ) + int64_t fireAction( int64_t tick ) { trigger(); return adjust( mTimer.fireAction( tick ) ); @@ -200,37 +221,53 @@ public: mShiftRegister = ( mShiftRegister & 0xff00 ) | value; } - uint64_t setBackup( uint64_t tick, uint8_t value ) + int64_t setBackup( int64_t tick, uint8_t value ) { return adjust( mTimer.setBackup( tick, value ) ); } - uint64_t setControl( uint64_t tick, uint8_t value ) + int64_t setControl( int64_t tick, uint8_t value ) { + if ( mCtrlA == value ) + return 0; + mCtrlA = value; + mTapSelector = ( mTapSelector & 0b1111'0111'1111 ) | ( value & FEEDBACK_7 ); mEnableIntegrate = ( value & ENABLE_INTEGRATE ) != 0; return adjust( mTimer.setControlA( tick, value & ~( FEEDBACK_7 | ENABLE_INTEGRATE ) ) ); } - uint64_t setCounter( uint64_t tick, uint8_t value ) + int64_t setCounter( int64_t tick, uint8_t value ) { return adjust( mTimer.setCount( tick, value ) ); } - void setOther( uint64_t tick, uint8_t value ) + void setOther( uint8_t value ) { mShiftRegister = ( mShiftRegister & 0b0000'1111'1111 ) | ( ( (int)value & 0b1111'0000 ) << 4 ); mTimer.setControlB( value & 0b0000'1111 ); } - int8_t getOutput() + int8_t getOutput() const { return mOutput; } + void fillRegisterPool( int64_t tick, uint8_t* regs ) + { + regs[0] = mVolume; + regs[1] = mTapSelector & 0xff; + regs[2] = mOutput; + regs[3] = mShiftRegister & 0xff; + regs[4] = mTimer.getBackup(); + regs[5] = mCtrlA; + regs[6] = mTimer.getCount( tick ); + regs[7] = ( ( mShiftRegister >> 4 ) & 0xf0 ); + } + private: - uint64_t adjust( uint64_t tick ) const + int64_t adjust( int64_t tick ) const { //ticks are advancing in 1 MHz resolution, so lower 4 bits are unused. //timer number is encoded on lowest 2 bits. @@ -268,6 +305,7 @@ private: bool mEnableIntegrate; int8_t mVolume; int8_t mOutput; + uint8_t mCtrlA; }; } @@ -283,11 +321,13 @@ private: class ActionQueue { public: - ActionQueue() : mTab{ ~15ull | 0, ~15ull | 1, ~15ull | 2, ~15ull | 3, ~15ull | 4 } + + + ActionQueue() : mTab{ CNT_MAX | 0, CNT_MAX | 1, CNT_MAX | 2, CNT_MAX | 3, CNT_MAX | 4 } { } - void push( uint64_t value ) + void push( int64_t value ) { size_t idx = value & 15; if ( idx < mTab.size() ) @@ -300,21 +340,21 @@ public: } } - uint64_t pop() + int64_t pop() { - uint64_t min1 = std::min( mTab[0], mTab[1] ); - uint64_t min2 = std::min( mTab[2], mTab[3] ); - uint64_t min3 = std::min( min1, mTab[4] ); - uint64_t min4 = std::min( min2, min3 ); + int64_t min1 = std::min( mTab[0], mTab[1] ); + int64_t min2 = std::min( mTab[2], mTab[3] ); + int64_t min3 = std::min( min1, mTab[4] ); + int64_t min4 = std::min( min2, min3 ); - assert( ( min4 & 15 ) < mTab.size() ); - mTab[min4 & 15] = ~15ull | ( min4 & 15 ); + assert( ( min4 & 15 ) < (int64_t)mTab.size() ); + mTab[min4 & 15] = CNT_MAX | ( min4 & 15 ); return min4; } private: - std::array mTab; + std::array mTab; }; @@ -344,17 +384,17 @@ public: static constexpr uint16_t MPAN = 0x44; static constexpr uint16_t MSTEREO = 0x50; - MikeyPimpl() : mAudioChannels{}, mAttenuationLeft{ 0x3c, 0x3c, 0x3c, 0x3c }, mAttenuationRight{ 0x3c, 0x3c, 0x3c, 0x3c }, mPan{ 0xff }, mStereo{} + MikeyPimpl() : mAudioChannels{ AudioChannel{0}, AudioChannel{1}, AudioChannel{2}, AudioChannel{3} }, + mAttenuationLeft{ 0x3c, 0x3c, 0x3c, 0x3c }, + mAttenuationRight{ 0x3c, 0x3c, 0x3c, 0x3c }, + mRegisterPool{}, mPan{ 0xff }, mStereo{} { - mAudioChannels[0] = std::make_unique( 0 ); - mAudioChannels[1] = std::make_unique( 1 ); - mAudioChannels[2] = std::make_unique( 2 ); - mAudioChannels[3] = std::make_unique( 3 ); + std::fill_n( mRegisterPool.data(), mRegisterPool.size(), (uint8_t)0xff ); } ~MikeyPimpl() {} - uint64_t write( uint64_t tick, uint8_t address, uint8_t value ) + int64_t write( int64_t tick, uint8_t address, uint8_t value ) { assert( address >= 0x20 ); @@ -364,25 +404,25 @@ public: switch ( address & 0x7 ) { case VOLCNTRL: - mAudioChannels[idx]->setVolume( (int8_t)value ); + mAudioChannels[idx].setVolume( (int8_t)value ); break; case FEEDBACK: - mAudioChannels[idx]->setFeedback( value ); + mAudioChannels[idx].setFeedback( value ); break; case OUTPUT: - mAudioChannels[idx]->setOutput( value ); + mAudioChannels[idx].setOutput( value ); break; case SHIFT: - mAudioChannels[idx]->setShift( value ); + mAudioChannels[idx].setShift( value ); break; case BACKUP: - return mAudioChannels[idx]->setBackup( tick, value ); + return mAudioChannels[idx].setBackup( tick, value ); case CONTROL: - return mAudioChannels[idx]->setControl( tick, value ); + return mAudioChannels[idx].setControl( tick, value ); case COUNTER: - return mAudioChannels[idx]->setCounter( tick, value ); + return mAudioChannels[idx].setCounter( tick, value ); case OTHER: - mAudioChannels[idx]->setOther( tick, value ); + mAudioChannels[idx].setOther( value ); break; } } @@ -395,6 +435,7 @@ public: case ATTENREG1: case ATTENREG2: case ATTENREG3: + mRegisterPool[8*4+idx] = value; mAttenuationLeft[idx] = ( value & 0x0f ) << 2; mAttenuationRight[idx] = ( value & 0xf0 ) >> 2; break; @@ -411,41 +452,52 @@ public: return 0; } - uint64_t fireTimer( uint64_t tick ) + int64_t fireTimer( int64_t tick ) { size_t timer = tick & 0x0f; assert( timer < 4 ); - return mAudioChannels[timer]->fireAction( tick ); + return mAudioChannels[timer].fireAction( tick ); } AudioSample sampleAudio() const { - int16_t left{}; - int16_t right{}; + int left{}; + int right{}; for ( size_t i = 0; i < 4; ++i ) { if ( ( mStereo & ( (uint8_t)0x01 << i ) ) == 0 ) { const int attenuation = ( mPan & ( (uint8_t)0x01 << i ) ) != 0 ? mAttenuationLeft[i] : 0x3c; - left += mAudioChannels[i]->getOutput() * attenuation; + left += mAudioChannels[i].getOutput() * attenuation; } if ( ( mStereo & ( (uint8_t)0x10 << i ) ) == 0 ) { const int attenuation = ( mPan & ( (uint8_t)0x01 << i ) ) != 0 ? mAttenuationRight[i] : 0x3c; - right += mAudioChannels[i]->getOutput() * attenuation; + right += mAudioChannels[i].getOutput() * attenuation; } } - return { left, right }; + return { (int16_t)left, (int16_t)right }; + } + + uint8_t const* getRegisterPool( int64_t tick ) + { + for ( size_t i = 0; i < mAudioChannels.size(); ++i ) + { + mAudioChannels[i].fillRegisterPool( tick, mRegisterPool.data() + 8 * i ); + } + + return mRegisterPool.data(); } private: - std::array, 4> mAudioChannels; + std::array mAudioChannels; std::array mAttenuationLeft; std::array mAttenuationRight; + std::array mRegisterPool; uint8_t mPan; uint8_t mStereo; @@ -488,7 +540,7 @@ void Mikey::sampleAudio( int16_t* bufL, int16_t* bufR, size_t size ) size_t i = 0; while ( i < size ) { - uint64_t value = mQueue->pop(); + int64_t value = mQueue->pop(); if ( ( value & 4 ) == 0 ) { if ( auto newAction = mMikey->fireTimer( value ) ) @@ -502,10 +554,14 @@ void Mikey::sampleAudio( int16_t* bufL, int16_t* bufR, size_t size ) bufL[i] = sample.left; bufR[i] = sample.right; i += 1; - mTick = value; enqueueSampling(); } } } +uint8_t const* Mikey::getRegisterPool() +{ + return mMikey->getRegisterPool( mTick ); +} + } diff --git a/src/engine/platform/sound/lynx/Mikey.hpp b/src/engine/platform/sound/lynx/Mikey.hpp index 5da1cdfb..20b8b086 100644 --- a/src/engine/platform/sound/lynx/Mikey.hpp +++ b/src/engine/platform/sound/lynx/Mikey.hpp @@ -20,6 +20,8 @@ public: void write( uint8_t address, uint8_t value ); void sampleAudio( int16_t* bufL, int16_t* bufR, size_t size ); + uint8_t const* getRegisterPool(); + private: void enqueueSampling(); From d15b3d6ffd00dae70d445b211c3ecb41f9b60a0e Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Tue, 22 Feb 2022 13:36:21 +0100 Subject: [PATCH 08/11] Some documentation --- papers/doc/4-instrument/README.md | 1 + papers/doc/4-instrument/lynx.md | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 papers/doc/4-instrument/lynx.md diff --git a/papers/doc/4-instrument/README.md b/papers/doc/4-instrument/README.md index 590ffe39..b9d4b8f4 100644 --- a/papers/doc/4-instrument/README.md +++ b/papers/doc/4-instrument/README.md @@ -22,6 +22,7 @@ depending on the instrument type, there are currently 10 different types of an i - [TIA](tia.md) - for use with Atari 2600 system. - [AY-3-8910](ay8910.md) - for use with AY-3-8910 PSG sound source and SSG portion in YM2610. - [Amiga/sample](amiga.md) for controlling Amiga and other sample based synthsizers like YM2612's Channel 6 PCM mode, NES channel 5, Sega PCM and PC Engine's sample playback mode. +- [Atari Lynx](lynx.md) - for use with Atari Lynx handheld console. # macros diff --git a/papers/doc/4-instrument/lynx.md b/papers/doc/4-instrument/lynx.md new file mode 100644 index 00000000..5badd47f --- /dev/null +++ b/papers/doc/4-instrument/lynx.md @@ -0,0 +1,25 @@ +# Atari Lynx instrument editor + +Atari Lynx instrument editor consists of only three macros: + +- [Volume] - volume sequence +- [Arpeggio] - pitch sequencer +- [Duty/Int] - bit pattern for LFSR taps and integration. + +## Audio generation description + +Atari Lynx to generate sound uses 12-bit linear feedback shift register with configurable tap. Nine separate bits can be enable to be the source of feedback. +Namely bits 0, 1, 2, 3, 4, 5, 7, 10 and 11. To generate ANY sound at least one bit MUST be enable. + +### Square wave + +The LFSR is shifted at the rate define by sound pitch and generates square wave by setting channel output value to +volume or -volume, depending on the bit shifted in. + +### Triangle wave + +Alternatively when "int" bit is set sound wave is generated by adding or subtracting volume from output effectively producing triangle wave. + +#### How triangle wave works? + +Hint: To obtain triangle set bits "int" and "11" in "Duty/Int" sequence and set volume to about 22. +By enabling 11th tap bit the value shifted in is negated after 11 bit is shifted in hence the volume is added for 11 cycles and then subtracted for 11 cycles. From d07f28aec5c640315ea70a2aae3196efb02d5e49 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Tue, 22 Feb 2022 21:12:10 +0100 Subject: [PATCH 09/11] Compilation error fix --- src/engine/platform/sound/lynx/Mikey.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index dadbe37e..2f3b23ed 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -129,14 +129,14 @@ private: int64_t scaleDiff( int64_t older, int64_t newer ) const { - int64_t const mask = ~0 << ( mAudShift + 4 ); + int64_t const mask = (int64_t)( ~0ull << ( mAudShift + 4 ) ); return ( ( newer & mask ) - ( older & mask ) ) >> ( mAudShift + 4 ); } void updateValue( int64_t tick ) { if ( mEnableCount ) - mValue = (uint8_t)std::max( 0ll, mValue - scaleDiff( mValueUpdateTick, tick ) ); + mValue = (uint8_t)std::max( (int64_t)0, mValue - scaleDiff( mValueUpdateTick, tick ) ); mValueUpdateTick = tick; } From 2e45b3066a11ebb0fe0d3e1e0b8a441316650e0b Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Tue, 22 Feb 2022 21:34:56 +0100 Subject: [PATCH 10/11] Removing unused variables --- src/engine/platform/sound/lynx/Mikey.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engine/platform/sound/lynx/Mikey.cpp b/src/engine/platform/sound/lynx/Mikey.cpp index 2f3b23ed..ad112baf 100644 --- a/src/engine/platform/sound/lynx/Mikey.cpp +++ b/src/engine/platform/sound/lynx/Mikey.cpp @@ -77,7 +77,7 @@ int32_t clamp( int32_t v, int32_t lo, int32_t hi ) class Timer { public: - Timer() : mValueUpdateTick{}, mAudShift {}, mCtrlA{ -1 }, mEnableReload{}, mEnableCount{}, mTimerDone{}, mBackup{ 0 }, mValue{ 0 } + Timer() : mValueUpdateTick{}, mAudShift {}, mEnableReload{}, mEnableCount{}, mTimerDone{}, mBackup{ 0 }, mValue{ 0 } { } @@ -180,7 +180,6 @@ private: private: int64_t mValueUpdateTick; int mAudShift; - int mCtrlA; bool mEnableReload; bool mEnableCount; bool mTimerDone; From b1120575e4d0b7bc35082e35bd27814afeeffac8 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Tue, 22 Feb 2022 21:35:17 +0100 Subject: [PATCH 11/11] Proper device resetting --- src/engine/platform/lynx.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 7545a986..b49dd362 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -285,6 +285,9 @@ int DivPlatformLynx::getRegisterPoolSize() } void DivPlatformLynx::reset() { + + mikey = std::make_unique( rate ); + for (int i=0; i<4; i++) { chan[i]= DivPlatformLynx::Channel(); } @@ -331,7 +334,6 @@ int DivPlatformLynx::init(DivEngine* p, int channels, int sugRate, unsigned int chipClock = 16000000; rate = chipClock/128; - mikey = std::make_unique(rate); reset(); return 4; }