Initial Atari Lynx Support

This commit is contained in:
Waldemar Pawlaszek 2022-02-20 18:15:15 +01:00
parent 47d7e68958
commit 2e4c7ec60a
14 changed files with 1038 additions and 25 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ linuxbuild/
test/songs/
test/delta/
test/result/
.vs/

View file

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

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,332 @@
#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 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;
}
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<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;
mikey = std::make_unique<Lynx::Mikey>(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);
}

View file

@ -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<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);
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();
int init(DivEngine* parent, int channels, int sugRate, unsigned int flags);
void quit();
~DivPlatformLynx();
};
#endif

View file

@ -0,0 +1,492 @@
#include "Mikey.hpp"
#include <array>
#include <cstdint>
#include <vector>
#include <functional>
#include <cassert>
#include <algorithm>
namespace Lynx
{
namespace
{
#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() : 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<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;
};
}
/*
"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<uint64_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{}, mAttenuationLeft{ 0x3c, 0x3c, 0x3c, 0x3c }, mAttenuationRight{ 0x3c, 0x3c, 0x3c, 0x3c }, mPan{ 0xff }, mStereo{}
{
mAudioChannels[0] = std::make_unique<AudioChannel>( 0 );
mAudioChannels[1] = std::make_unique<AudioChannel>( 1 );
mAudioChannels[2] = std::make_unique<AudioChannel>( 2 );
mAudioChannels[3] = std::make_unique<AudioChannel>( 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<float, float> 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<std::unique_ptr<AudioChannel>, 4> mAudioChannels;
std::array<int, 4> mAttenuationLeft;
std::array<int, 4> mAttenuationRight;
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 )
{
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();
}
}
}
}

View file

@ -0,0 +1,37 @@
#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 );
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

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

View file

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

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

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]={
@ -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,8 +1002,13 @@ 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));
if (dutyMax>0) {
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);
}