From 2e4c7ec60a87b7c2c72815917c1c9980e80ec982 Mon Sep 17 00:00:00 2001 From: Waldemar Pawlaszek Date: Sun, 20 Feb 2022 18:15:15 +0100 Subject: [PATCH] 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);