furnace/src/engine/platform/sound/lynx/Mikey.cpp

603 lines
14 KiB
C++

/**
* 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>
#if defined( _MSC_VER )
#include <intrin.h>
static uint32_t popcnt_generic( uint32_t x )
{
int v = 0;
while ( x != 0 )
{
x &= x - 1;
v++;
}
return v;
}
#if defined( _M_IX86 ) || defined( _M_X64 )
static uint32_t popcnt_intrinsic( uint32_t x )
{
return __popcnt( x );
}
static uint32_t( *popcnt )( uint32_t );
//detecting popcnt availability on msvc intel
static void selectPOPCNT()
{
int info[4];
__cpuid( info, 1 );
if ( ( info[2] & ( (int)1 << 23 ) ) != 0 )
{
popcnt = &popcnt_intrinsic;
}
else
{
popcnt = &popcnt_generic;
}
}
#else //defined( _M_IX86 ) || defined( _M_X64 )
//MSVC non INTEL should use generic implementation
inline void selectPOPCNT()
{
}
#define popcnt popcnt_generic
#endif
#else //defined( _MSC_VER )
//non MVSC should use builtin implementation
inline void selectPOPCNT()
{
}
#define popcnt __builtin_popcount
#endif
namespace Lynx
{
namespace
{
static constexpr int64_t CNT_MAX = std::numeric_limits<int64_t>::max() & ~15;
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;
mAttenuationRight[idx] = ( value & 0x0f ) << 2;
mAttenuationLeft[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( DivDispatchOscBuffer** oscb ) const
{
int left{};
int right{};
for ( size_t i = 0; i < 4; ++i )
{
int oscbWrite = 0;
if ( ( mStereo & ( (uint8_t)0x01 << i ) ) == 0 )
{
const int attenuation = ( mPan & ( (uint8_t)0x01 << i ) ) != 0 ? mAttenuationLeft[i] : 0x3c;
left += mAudioChannels[i].getOutput() * attenuation;
oscbWrite += 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;
oscbWrite += mAudioChannels[i].getOutput() * attenuation;
}
if (oscb!=NULL) {
oscb[i]->data[oscb[i]->needle++]=oscbWrite;
}
}
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 }
{
selectPOPCNT();
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, DivDispatchOscBuffer** oscb )
{
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( oscb );
bufL[i] = sample.left;
bufR[i] = sample.right;
i += 1;
enqueueSampling();
}
}
}
uint8_t const* Mikey::getRegisterPool()
{
return mMikey->getRegisterPool( mTick );
}
}