Atari Lynx Support
This commit is contained in:
tildearrow 2022-02-22 17:37:23 -05:00 committed by GitHub
commit 8ca32aa2d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1212 additions and 26 deletions

View file

@ -97,6 +97,8 @@ enum DivDispatchCmds {
DIV_CMD_SAA_ENVELOPE,
DIV_CMD_LYNX_LFSR_LOAD,
DIV_ALWAYS_SET_VOLUME,
DIV_CMD_MAX

View file

@ -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;

View file

@ -47,6 +47,7 @@ enum DivInstrumentType {
DIV_INS_POKEY=20,
DIV_INS_BEEPER=21,
DIV_INS_SWAN=22,
DIV_INS_MIKEY=23,
};
struct DivInstrumentFM {

View file

@ -0,0 +1,384 @@
/**
* 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 <math.h>
#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 WRITE_ATTEN(ch,v) rWrite((0x40+ch),(v))
#define CHIP_DIVIDER 64
#if defined( _MSC_VER )
#include <intrin.h>
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 v<lo?lo:(v>hi?hi:v);
}
const char* regCheatSheetLynx[]={
"DATA", "0",
NULL
};
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) {
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) {
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;
WRITE_FEEDBACK(i, chan[i].duty.feedback);
}
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;
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].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)));
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_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();
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_PANNING:
WRITE_ATTEN(c.chan, c.value);
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];
}
unsigned char* DivPlatformLynx::getRegisterPool()
{
return const_cast<unsigned char*>( mikey->getRegisterPool() );
}
int DivPlatformLynx::getRegisterPoolSize()
{
return 4*8+4;
}
void DivPlatformLynx::reset() {
mikey = std::make_unique<Lynx::Mikey>( rate );
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<DivRegWrite>& 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;
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);
}

View file

@ -0,0 +1,78 @@
#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 {
DivMacroInt std;
MikeyFreqDiv fd;
MikeyDuty duty;
int baseFreq, pitch, note, actualNote, lfsr;
unsigned char ins;
bool active, insChanged, freqChanged, keyOn, keyOff, inPorta;
signed char vol, outVol;
Channel():
std(),
fd(0),
duty(0),
baseFreq(0),
pitch(0),
note(0),
actualNote(0),
lfsr(-1),
ins(-1),
active(false),
insChanged(true),
freqChanged(false),
keyOn(false),
keyOff(false),
inPorta(false),
vol(127),
outVol(127) {}
};
Channel chan[4];
bool isMuted[4];
std::unique_ptr<Lynx::Mikey> 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);
unsigned char* getRegisterPool();
int getRegisterPoolSize();
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<DivRegWrite>& wlist);
const char** getRegisterSheet();
const char* getEffectName( unsigned char effect );
int init(DivEngine* parent, int channels, int sugRate, unsigned int flags);
void quit();
~DivPlatformLynx();
};
#endif

View file

@ -0,0 +1,566 @@
/**
* 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 <array>
#include <cstdint>
#include <vector>
#include <functional>
#include <cassert>
#include <algorithm>
#include <limits>
namespace Lynx
{
namespace
{
static constexpr int64_t CNT_MAX = std::numeric_limits<int64_t>::max() & ~15;
#if defined ( __cpp_lib_bitops )
#define popcnt(X) std::popcount(X)
#elif defined( _MSC_VER )
# include <intrin.h>
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() : mValueUpdateTick{}, mAudShift {}, mEnableReload{}, mEnableCount{}, mTimerDone{}, mBackup{ 0 }, mValue{ 0 }
{
}
int64_t setBackup( int64_t tick, uint8_t backup )
{
mBackup = backup;
return computeAction( tick );
}
int64_t setControlA( int64_t tick, uint8_t controlA )
{
mTimerDone ^= ( controlA & CONTROLA::RESET_DONE ) != 0;
mEnableReload = ( controlA & CONTROLA::ENABLE_RELOAD ) != 0;
mEnableCount = ( controlA & CONTROLA::ENABLE_COUNT ) != 0;
mAudShift = controlA & CONTROLA::AUD_CLOCK_MASK;
return computeAction( tick );
}
int64_t setCount( int64_t tick, uint8_t value )
{
return computeTriggerTime( tick );
}
void setControlB( uint8_t controlB )
{
mTimerDone = ( controlB & CONTROLB::TIMER_DONE ) != 0;
}
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:
int64_t scaleDiff( int64_t older, int64_t newer ) const
{
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( (int64_t)0, 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;
}
return computeTriggerTime( tick );
}
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:
int64_t mValueUpdateTick;
int mAudShift;
bool mEnableReload;
bool mEnableCount;
bool mTimerDone;
uint8_t mBackup;
uint8_t mValue;
};
class AudioChannel
{
public:
AudioChannel( uint32_t number ) : mTimer{}, mNumber{ number }, mShiftRegister{}, mTapSelector{}, mEnableIntegrate{}, mVolume{}, mOutput{}, mCtrlA{}
{
}
int64_t fireAction( int64_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;
}
int64_t setBackup( int64_t tick, uint8_t value )
{
return adjust( mTimer.setBackup( tick, 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 ) ) );
}
int64_t setCounter( int64_t tick, uint8_t value )
{
return adjust( mTimer.setCount( tick, value ) );
}
void setOther( uint8_t value )
{
mShiftRegister = ( mShiftRegister & 0b0000'1111'1111 ) | ( ( (int)value & 0b1111'0000 ) << 4 );
mTimer.setControlB( value & 0b0000'1111 );
}
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:
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.
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<int8_t>::min(), (int32_t)std::numeric_limits<int8_t>::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;
uint8_t mCtrlA;
};
}
/*
"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{ CNT_MAX | 0, CNT_MAX | 1, CNT_MAX | 2, CNT_MAX | 3, CNT_MAX | 4 }
{
}
void push( int64_t value )
{
size_t idx = value & 15;
if ( idx < mTab.size() )
{
if ( value & ~15 )
{
//writing only non-zero values
mTab[idx] = value;
}
}
}
int64_t pop()
{
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 ) < (int64_t)mTab.size() );
mTab[min4 & 15] = CNT_MAX | ( min4 & 15 );
return min4;
}
private:
std::array<int64_t, 5> 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{ AudioChannel{0}, AudioChannel{1}, AudioChannel{2}, AudioChannel{3} },
mAttenuationLeft{ 0x3c, 0x3c, 0x3c, 0x3c },
mAttenuationRight{ 0x3c, 0x3c, 0x3c, 0x3c },
mRegisterPool{}, mPan{ 0xff }, mStereo{}
{
std::fill_n( mRegisterPool.data(), mRegisterPool.size(), (uint8_t)0xff );
}
~MikeyPimpl() {}
int64_t write( int64_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[idx].setVolume( (int8_t)value );
break;
case FEEDBACK:
mAudioChannels[idx].setFeedback( value );
break;
case OUTPUT:
mAudioChannels[idx].setOutput( value );
break;
case SHIFT:
mAudioChannels[idx].setShift( value );
break;
case BACKUP:
return mAudioChannels[idx].setBackup( tick, value );
case CONTROL:
return mAudioChannels[idx].setControl( tick, value );
case COUNTER:
return mAudioChannels[idx].setCounter( tick, value );
case OTHER:
mAudioChannels[idx].setOther( value );
break;
}
}
else
{
int idx = address & 3;
switch ( address )
{
case ATTENREG0:
case ATTENREG1:
case ATTENREG2:
case ATTENREG3:
mRegisterPool[8*4+idx] = value;
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;
}
int64_t fireTimer( int64_t tick )
{
size_t timer = tick & 0x0f;
assert( timer < 4 );
return mAudioChannels[timer].fireAction( tick );
}
AudioSample sampleAudio() const
{
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;
}
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 { (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<AudioChannel, 4> mAudioChannels;
std::array<int, 4> mAttenuationLeft;
std::array<int, 4> mAttenuationRight;
std::array<uint8_t, 4 * 8 + 4> mRegisterPool;
uint8_t mPan;
uint8_t mStereo;
};
Mikey::Mikey( uint32_t sampleRate ) : mMikey{ std::make_unique<MikeyPimpl>() }, mQueue{ std::make_unique<ActionQueue>() }, 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 )
{
int64_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;
enqueueSampling();
}
}
}
uint8_t const* Mikey::getRegisterPool()
{
return mMikey->getRegisterPool( mTick );
}
}

View file

@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <memory>
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 );
uint8_t const* getRegisterPool();
private:
void enqueueSampling();
private:
std::unique_ptr<MikeyPimpl> mMikey;
std::unique_ptr<ActionQueue> mQueue;
uint64_t mTick;
uint64_t mNextTick;
uint32_t mSampleRate;
uint32_t mSamplesRemainder;
std::pair<uint32_t, uint32_t> mTicksPerSample;
};
}

View file

@ -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;
}

View file

@ -87,6 +87,7 @@ enum DivSystem {
DIV_SYSTEM_YM2610_FULL,
DIV_SYSTEM_YM2610_FULL_EXT,
DIV_SYSTEM_OPLL_DRUMS,
DIV_SYSTEM_LYNX
};
struct DivSong {

View file

@ -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;
}

View file

@ -260,6 +260,19 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write
w->writeC(0);
}
break;
case DIV_SYSTEM_LYNX:
w->writeC(0x4e);
w->writeC(0x44);
w->writeC(0xff); //stereo attenuation select
w->writeC(0x4e);
w->writeC(0x50);
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;
}
@ -377,6 +390,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;
@ -457,6 +475,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) {
int hasX1=0;
int hasC352=0;
int hasGA20=0;
int hasLynx=0;
int howManyChips=0;
@ -668,6 +687,17 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop) {
howManyChips++;
}
break;
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;
}
@ -753,7 +783,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);
}

View file

@ -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);
@ -4477,6 +4481,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...")) {
@ -4684,6 +4689,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();
}
}
@ -5225,6 +5231,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));

View file

@ -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,

View file

@ -26,7 +26,7 @@
#include <fmt/printf.h>
#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]={
@ -131,6 +132,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
};
@ -691,10 +696,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;
// TODO: set to 23 for 0.6
if (ImGui::Combo("Type",&insType,insTypes,10)) {
if (ImGui::Combo("Type",&insType,insTypes,24)) {
ins->type=(DivInstrumentType)insType;
}
@ -950,7 +954,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) {
@ -975,6 +979,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;
}
@ -993,6 +1001,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;
@ -1013,7 +1022,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?(&macroHoverNote):NULL),true);
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,false);
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,false);
}
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,false);
}
}
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,false);

View file

@ -474,6 +474,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();
}