mirror of
https://github.com/tildearrow/furnace.git
synced 2024-11-26 06:25:16 +00:00
Initial Atari Lynx Support
This commit is contained in:
parent
47d7e68958
commit
2e4c7ec60a
14 changed files with 1038 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@ linuxbuild/
|
|||
test/songs/
|
||||
test/delta/
|
||||
test/result/
|
||||
.vs/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -47,6 +47,7 @@ enum DivInstrumentType {
|
|||
DIV_INS_POKEY=20,
|
||||
DIV_INS_BEEPER=21,
|
||||
DIV_INS_SWAN=22,
|
||||
DIV_INS_MIKEY=23,
|
||||
};
|
||||
|
||||
struct DivInstrumentFM {
|
||||
|
|
332
src/engine/platform/lynx.cpp
Normal file
332
src/engine/platform/lynx.cpp
Normal 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);
|
||||
}
|
74
src/engine/platform/lynx.h
Normal file
74
src/engine/platform/lynx.h
Normal 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
|
492
src/engine/platform/sound/lynx/Mikey.cpp
Normal file
492
src/engine/platform/sound/lynx/Mikey.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
37
src/engine/platform/sound/lynx/Mikey.hpp
Normal file
37
src/engine/platform/sound/lynx/Mikey.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
|
@ -87,6 +87,7 @@ enum DivSystem {
|
|||
DIV_SYSTEM_YM2610_FULL,
|
||||
DIV_SYSTEM_YM2610_FULL_EXT,
|
||||
DIV_SYSTEM_OPLL_DRUMS,
|
||||
DIV_SYSTEM_LYNX
|
||||
};
|
||||
|
||||
struct DivSong {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?(¯oHoverNote):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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue