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

29
.gitignore vendored
View file

@ -1,14 +1,15 @@
.vscode/
build/
release/
t/
winbuild/
win32build/
macbuild/
linuxbuild/
*.swp
.cache/
.DS_Store
test/songs/
test/delta/
test/result/
.vscode/
build/
release/
t/
winbuild/
win32build/
macbuild/
linuxbuild/
*.swp
.cache/
.DS_Store
test/songs/
test/delta/
test/result/
.vs/

View file

@ -272,6 +272,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
@ -307,6 +309,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

@ -22,6 +22,7 @@ depending on the instrument type, there are currently 10 different types of an i
- [TIA](tia.md) - for use with Atari 2600 system.
- [AY-3-8910](ay8910.md) - for use with AY-3-8910 PSG sound source and SSG portion in YM2610.
- [Amiga/sample](amiga.md) for controlling Amiga and other sample based synthsizers like YM2612's Channel 6 PCM mode, NES channel 5, Sega PCM and PC Engine's sample playback mode.
- [Atari Lynx](lynx.md) - for use with Atari Lynx handheld console.
# macros

View file

@ -0,0 +1,25 @@
# Atari Lynx instrument editor
Atari Lynx instrument editor consists of only three macros:
- [Volume] - volume sequence
- [Arpeggio] - pitch sequencer
- [Duty/Int] - bit pattern for LFSR taps and integration.
## Audio generation description
Atari Lynx to generate sound uses 12-bit linear feedback shift register with configurable tap. Nine separate bits can be enable to be the source of feedback.
Namely bits 0, 1, 2, 3, 4, 5, 7, 10 and 11. To generate ANY sound at least one bit MUST be enable.
### Square wave
The LFSR is shifted at the rate define by sound pitch and generates square wave by setting channel output value to +volume or -volume, depending on the bit shifted in.
### Triangle wave
Alternatively when "int" bit is set sound wave is generated by adding or subtracting volume from output effectively producing triangle wave.
#### How triangle wave works?
Hint: To obtain triangle set bits "int" and "11" in "Duty/Int" sequence and set volume to about 22.
By enabling 11th tap bit the value shifted in is negated after 11 bit is shifted in hence the volume is added for 11 cycles and then subtracted for 11 cycles.

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