mirror of
https://github.com/QB64-Phoenix-Edition/QB64pe.git
synced 2024-09-08 12:20:15 +00:00
This is a single commit adding all of a740g's audio backend. Later commits will connect it together with QB64PE itself.
1266 lines
38 KiB
C++
1266 lines
38 KiB
C++
/*
|
|
|
|
C++ player code for Reality Adlib Tracker 2.0a (file version 2.1).
|
|
|
|
Please note, this is just the player code. This does no checking of the tune data before
|
|
it tries to play it, as most use cases will be a known tune being used in a production.
|
|
So if you're writing an application that loads unknown tunes in at run time then you'll
|
|
want to do more validity checking.
|
|
|
|
To use:
|
|
|
|
- Instantiate the RADPlayer object
|
|
|
|
- Initialise player for your tune by calling the Init() method. Supply a pointer to the
|
|
tune file and a function for writing to the OPL3 registers.
|
|
|
|
- Call the Update() method a number of times per second as returned by GetHertz(). If
|
|
your tune is using the default BPM setting you can safely just call it 50 times a
|
|
second, unless it's a legacy "slow-timer" tune then it'll need to be 18.2 times a
|
|
second.
|
|
|
|
- When you're done, stop calling Update() and call the Stop() method to turn off all
|
|
sound and reset the OPL3 hardware.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
|
|
#ifndef RAD_DETECT_REPEATS
|
|
#define RAD_DETECT_REPEATS 0
|
|
#endif
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// RAD player class.
|
|
//==================================================================================================
|
|
class RADPlayer {
|
|
|
|
// Various constants
|
|
enum {
|
|
kTracks = 100,
|
|
kChannels = 9,
|
|
kTrackLines = 64,
|
|
kRiffTracks = 10,
|
|
kInstruments = 127,
|
|
|
|
cmPortamentoUp = 0x1,
|
|
cmPortamentoDwn = 0x2,
|
|
cmToneSlide = 0x3,
|
|
cmToneVolSlide = 0x5,
|
|
cmVolSlide = 0xA,
|
|
cmSetVol = 0xC,
|
|
cmJumpToLine = 0xD,
|
|
cmSetSpeed = 0xF,
|
|
cmIgnore = ('I' - 55),
|
|
cmMultiplier = ('M' - 55),
|
|
cmRiff = ('R' - 55),
|
|
cmTranspose = ('T' - 55),
|
|
cmFeedback = ('U' - 55),
|
|
cmVolume = ('V' - 55),
|
|
};
|
|
|
|
enum e_Source {
|
|
SNone, SRiff, SIRiff,
|
|
};
|
|
|
|
enum {
|
|
fKeyOn = 1 << 0,
|
|
fKeyOff = 1 << 1,
|
|
fKeyedOn = 1 << 2,
|
|
};
|
|
|
|
struct CInstrument {
|
|
uint8_t Feedback[2];
|
|
uint8_t Panning[2];
|
|
uint8_t Algorithm;
|
|
uint8_t Detune;
|
|
uint8_t Volume;
|
|
uint8_t RiffSpeed;
|
|
uint8_t * Riff;
|
|
uint8_t Operators[4][5];
|
|
};
|
|
|
|
struct CEffects {
|
|
int8_t PortSlide;
|
|
int8_t VolSlide;
|
|
uint16_t ToneSlideFreq;
|
|
uint8_t ToneSlideOct;
|
|
uint8_t ToneSlideSpeed;
|
|
int8_t ToneSlideDir;
|
|
};
|
|
|
|
struct CChannel {
|
|
uint8_t LastInstrument;
|
|
CInstrument * Instrument;
|
|
uint8_t Volume;
|
|
uint8_t DetuneA;
|
|
uint8_t DetuneB;
|
|
uint8_t KeyFlags;
|
|
uint16_t CurrFreq;
|
|
int8_t CurrOctave;
|
|
CEffects FX;
|
|
struct CRiff {
|
|
CEffects FX;
|
|
uint8_t * Track;
|
|
uint8_t * TrackStart;
|
|
uint8_t Line;
|
|
uint8_t Speed;
|
|
uint8_t SpeedCnt;
|
|
int8_t TransposeOctave;
|
|
int8_t TransposeNote;
|
|
uint8_t LastInstrument;
|
|
} Riff, IRiff;
|
|
};
|
|
|
|
public:
|
|
RADPlayer() : Initialised(false) {}
|
|
void Init(const void *tune, void (*opl3)(void *, uint16_t, uint8_t), void *arg);
|
|
void Stop();
|
|
bool Update();
|
|
int GetHertz() const { return Hertz; }
|
|
int GetPlayTimeInSeconds() const { return PlayTime / Hertz; }
|
|
int GetTunePos() const { return Order; }
|
|
int GetTuneLength() const { return OrderListSize; }
|
|
int GetTuneLine() const { return Line; }
|
|
void SetMasterVolume(int vol) { MasterVol = vol; }
|
|
int GetMasterVolume() const { return MasterVol; }
|
|
int GetSpeed() const { return Speed; }
|
|
|
|
#if RAD_DETECT_REPEATS
|
|
uint32_t ComputeTotalTime();
|
|
#endif
|
|
|
|
private:
|
|
bool UnpackNote(uint8_t *&s, uint8_t &last_instrument);
|
|
uint8_t * GetTrack();
|
|
uint8_t * SkipToLine(uint8_t *trk, uint8_t linenum, bool chan_riff = false);
|
|
void PlayLine();
|
|
void PlayNote(int channum, int8_t notenum, int8_t octave, uint16_t instnum, uint8_t cmd = 0, uint8_t param = 0, e_Source src = SNone, int op = 0);
|
|
void LoadInstrumentOPL3(int channum);
|
|
void PlayNoteOPL3(int channum, int8_t octave, int8_t note);
|
|
void ResetFX(CEffects *fx);
|
|
void TickRiff(int channum, CChannel::CRiff &riff, bool chan_riff);
|
|
void ContinueFX(int channum, CEffects *fx);
|
|
void SetVolume(int channum, uint8_t vol);
|
|
void GetSlideDir(int channum, CEffects *fx);
|
|
void LoadInstMultiplierOPL3(int channum, int op, uint8_t mult);
|
|
void LoadInstVolumeOPL3(int channum, int op, uint8_t vol);
|
|
void LoadInstFeedbackOPL3(int channum, int which, uint8_t fb);
|
|
void Portamento(uint16_t channum, CEffects *fx, int8_t amount, bool toneslide);
|
|
void Transpose(int8_t note, int8_t octave);
|
|
void SetOPL3(uint16_t reg, uint8_t val) {
|
|
OPL3Regs[reg] = val;
|
|
OPL3(OPL3Arg, reg, val);
|
|
}
|
|
uint8_t GetOPL3(uint16_t reg) const {
|
|
return OPL3Regs[reg];
|
|
}
|
|
|
|
void (*OPL3)(void *, uint16_t, uint8_t);
|
|
void * OPL3Arg;
|
|
CInstrument Instruments[kInstruments];
|
|
CChannel Channels[kChannels];
|
|
uint32_t PlayTime;
|
|
#if RAD_DETECT_REPEATS
|
|
uint32_t OrderMap[4];
|
|
bool Repeating;
|
|
#endif
|
|
int16_t Hertz;
|
|
uint8_t * OrderList;
|
|
uint8_t * Tracks[kTracks];
|
|
uint8_t * Riffs[kRiffTracks][kChannels];
|
|
uint8_t * Track;
|
|
bool Initialised;
|
|
uint8_t Speed;
|
|
uint8_t OrderListSize;
|
|
uint8_t SpeedCnt;
|
|
uint8_t Order;
|
|
uint8_t Line;
|
|
int8_t Entrances;
|
|
uint8_t MasterVol;
|
|
int8_t LineJump;
|
|
uint8_t OPL3Regs[512];
|
|
|
|
// Values exported by UnpackNote()
|
|
int8_t NoteNum;
|
|
int8_t OctaveNum;
|
|
uint8_t InstNum;
|
|
uint8_t EffectNum;
|
|
uint8_t Param;
|
|
bool LastNote;
|
|
|
|
static const int8_t NoteSize[];
|
|
static const uint16_t ChanOffsets3[9], Chn2Offsets3[9];
|
|
static const uint16_t NoteFreq[];
|
|
static const uint16_t OpOffsets3[9][4];
|
|
static const bool AlgCarriers[7][4];
|
|
};
|
|
//--------------------------------------------------------------------------------------------------
|
|
const int8_t RADPlayer::NoteSize[] = { 0, 2, 1, 3, 1, 3, 2, 4 };
|
|
const uint16_t RADPlayer::ChanOffsets3[9] = { 0, 1, 2, 0x100, 0x101, 0x102, 6, 7, 8 }; // OPL3 first channel
|
|
const uint16_t RADPlayer::Chn2Offsets3[9] = { 3, 4, 5, 0x103, 0x104, 0x105, 0x106, 0x107, 0x108 }; // OPL3 second channel
|
|
const uint16_t RADPlayer::NoteFreq[] = { 0x16b,0x181,0x198,0x1b0,0x1ca,0x1e5,0x202,0x220,0x241,0x263,0x287,0x2ae };
|
|
const uint16_t RADPlayer::OpOffsets3[9][4] = {
|
|
{ 0x00B, 0x008, 0x003, 0x000 },
|
|
{ 0x00C, 0x009, 0x004, 0x001 },
|
|
{ 0x00D, 0x00A, 0x005, 0x002 },
|
|
{ 0x10B, 0x108, 0x103, 0x100 },
|
|
{ 0x10C, 0x109, 0x104, 0x101 },
|
|
{ 0x10D, 0x10A, 0x105, 0x102 },
|
|
{ 0x113, 0x110, 0x013, 0x010 },
|
|
{ 0x114, 0x111, 0x014, 0x011 },
|
|
{ 0x115, 0x112, 0x015, 0x012 }
|
|
};
|
|
const bool RADPlayer::AlgCarriers[7][4] = {
|
|
{ true, false, false, false }, // 0 - 2op - op < op
|
|
{ true, true, false, false }, // 1 - 2op - op + op
|
|
{ true, false, false, false }, // 2 - 4op - op < op < op < op
|
|
{ true, false, false, true }, // 3 - 4op - op < op < op + op
|
|
{ true, false, true, false }, // 4 - 4op - op < op + op < op
|
|
{ true, false, true, true }, // 5 - 4op - op < op + op + op
|
|
{ true, true, true, true }, // 6 - 4op - op + op + op + op
|
|
};
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Initialise a RAD tune for playback. This assumes the tune data is valid and does minimal data
|
|
// checking.
|
|
//==================================================================================================
|
|
void RADPlayer::Init(const void *tune, void (*opl3)(void *, uint16_t, uint8_t), void *arg) {
|
|
|
|
Initialised = false;
|
|
|
|
// Version check; we only support version 2.1 tune files
|
|
if (*((uint8_t *)tune + 0x10) != 0x21) {
|
|
Hertz = -1;
|
|
return;
|
|
}
|
|
|
|
// The OPL3 call-back
|
|
OPL3 = opl3;
|
|
OPL3Arg = arg;
|
|
|
|
for (int i = 0; i < kTracks; i++)
|
|
Tracks[i] = 0;
|
|
|
|
for (int i = 0; i < kRiffTracks; i++)
|
|
for (int j = 0; j < kChannels; j++)
|
|
Riffs[i][j] = 0;
|
|
|
|
uint8_t *s = (uint8_t *)tune + 0x11;
|
|
|
|
uint8_t flags = *s++;
|
|
Speed = flags & 0x1F;
|
|
|
|
// Is BPM value present?
|
|
Hertz = 50;
|
|
if (flags & 0x20) {
|
|
Hertz = (s[0] | (int(s[1]) << 8)) * 2 / 5;
|
|
s += 2;
|
|
}
|
|
|
|
// Slow timer tune? Return an approximate hz
|
|
if (flags & 0x40)
|
|
Hertz = 18;
|
|
|
|
// Skip any description
|
|
while (*s)
|
|
s++;
|
|
s++;
|
|
|
|
// Unpack the instruments
|
|
while (1) {
|
|
|
|
// Instrument number, 0 indicates end of list
|
|
uint8_t inst_num = *s++;
|
|
if (inst_num == 0)
|
|
break;
|
|
|
|
// Skip instrument name
|
|
s += *s++;
|
|
|
|
CInstrument &inst = Instruments[inst_num - 1];
|
|
|
|
uint8_t alg = *s++;
|
|
inst.Algorithm = alg & 7;
|
|
inst.Panning[0] = (alg >> 3) & 3;
|
|
inst.Panning[1] = (alg >> 5) & 3;
|
|
|
|
if (inst.Algorithm < 7) {
|
|
|
|
uint8_t b = *s++;
|
|
inst.Feedback[0] = b & 15;
|
|
inst.Feedback[1] = b >> 4;
|
|
|
|
b = *s++;
|
|
inst.Detune = b >> 4;
|
|
inst.RiffSpeed = b & 15;
|
|
|
|
inst.Volume = *s++;
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
uint8_t *op = inst.Operators[i];
|
|
for (int j = 0; j < 5; j++)
|
|
op[j] = *s++;
|
|
}
|
|
|
|
} else {
|
|
|
|
// Ignore MIDI instrument data
|
|
s += 6;
|
|
}
|
|
|
|
// Instrument riff?
|
|
if (alg & 0x80) {
|
|
int size = s[0] | (int(s[1]) << 8);
|
|
s += 2;
|
|
inst.Riff = s;
|
|
s += size;
|
|
} else
|
|
inst.Riff = 0;
|
|
}
|
|
|
|
// Get order list
|
|
OrderListSize = *s++;
|
|
OrderList = s;
|
|
s += OrderListSize;
|
|
|
|
// Locate the tracks
|
|
while (1) {
|
|
|
|
// Track number
|
|
uint8_t track_num = *s++;
|
|
if (track_num >= kTracks)
|
|
break;
|
|
|
|
// Track size in bytes
|
|
int size = s[0] | (int(s[1]) << 8);
|
|
s += 2;
|
|
|
|
Tracks[track_num] = s;
|
|
s += size;
|
|
}
|
|
|
|
// Locate the riffs
|
|
while (1) {
|
|
|
|
// Riff id
|
|
uint8_t riffid = *s++;
|
|
uint8_t riffnum = riffid >> 4;
|
|
uint8_t channum = riffid & 15;
|
|
if (riffnum >= kRiffTracks || channum > kChannels)
|
|
break;
|
|
|
|
// Track size in bytes
|
|
int size = s[0] | (int(s[1]) << 8);
|
|
s += 2;
|
|
|
|
Riffs[riffnum][channum - 1] = s;
|
|
s += size;
|
|
}
|
|
|
|
// Done parsing tune, now set up for play
|
|
for (int i = 0; i < 512; i++)
|
|
OPL3Regs[i] = 255;
|
|
Stop();
|
|
|
|
Initialised = true;
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Stop all sounds and reset the tune. Tune will play from the beginning again if you continue to
|
|
// Update().
|
|
//==================================================================================================
|
|
void RADPlayer::Stop() {
|
|
|
|
// Clear all registers
|
|
for (uint16_t reg = 0x20; reg < 0xF6; reg++) {
|
|
|
|
// Ensure envelopes decay all the way
|
|
uint8_t val = (reg >= 0x60 && reg < 0xA0) ? 0xFF : 0;
|
|
|
|
SetOPL3(reg, val);
|
|
SetOPL3(reg + 0x100, val);
|
|
}
|
|
|
|
// Configure OPL3
|
|
SetOPL3(1, 0x20); // Allow waveforms
|
|
SetOPL3(8, 0); // No split point
|
|
SetOPL3(0xbd, 0); // No drums, etc.
|
|
SetOPL3(0x104, 0); // Everything 2-op by default
|
|
SetOPL3(0x105, 1); // OPL3 mode on
|
|
|
|
#if RAD_DETECT_REPEATS
|
|
// The order map keeps track of which patterns we've played so we can detect when the tune
|
|
// starts to repeat. Jump markers can't be reliably used for this
|
|
PlayTime = 0;
|
|
Repeating = false;
|
|
for (int i = 0; i < 4; i++)
|
|
OrderMap[i] = 0;
|
|
#endif
|
|
|
|
// Initialise play values
|
|
SpeedCnt = 1;
|
|
Order = 0;
|
|
Track = GetTrack();
|
|
Line = 0;
|
|
Entrances = 0;
|
|
MasterVol = 64;
|
|
|
|
// Initialise channels
|
|
for (int i = 0; i < kChannels; i++) {
|
|
CChannel &chan = Channels[i];
|
|
chan.LastInstrument = 0;
|
|
chan.Instrument = 0;
|
|
chan.Volume = 0;
|
|
chan.DetuneA = 0;
|
|
chan.DetuneB = 0;
|
|
chan.KeyFlags = 0;
|
|
chan.Riff.SpeedCnt = 0;
|
|
chan.IRiff.SpeedCnt = 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Playback update. Call BPM * 2 / 5 times a second. Use GetHertz() for this number after the
|
|
// tune has been initialised. Returns true if tune is starting to repeat.
|
|
//==================================================================================================
|
|
bool RADPlayer::Update() {
|
|
|
|
if (!Initialised)
|
|
return false;
|
|
|
|
// Run riffs
|
|
for (int i = 0; i < kChannels; i++) {
|
|
CChannel &chan = Channels[i];
|
|
TickRiff(i, chan.IRiff, false);
|
|
TickRiff(i, chan.Riff, true);
|
|
}
|
|
|
|
// Run main track
|
|
PlayLine();
|
|
|
|
// Run effects
|
|
for (int i = 0; i < kChannels; i++) {
|
|
CChannel &chan = Channels[i];
|
|
ContinueFX(i, &chan.IRiff.FX);
|
|
ContinueFX(i, &chan.Riff.FX);
|
|
ContinueFX(i, &chan.FX);
|
|
}
|
|
|
|
// Update play time. We convert to seconds when queried
|
|
PlayTime++;
|
|
|
|
#if RAD_DETECT_REPEATS
|
|
return Repeating;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Unpacks a single RAD note.
|
|
//==================================================================================================
|
|
bool RADPlayer::UnpackNote(uint8_t *&s, uint8_t &last_instrument) {
|
|
|
|
uint8_t chanid = *s++;
|
|
|
|
InstNum = 0;
|
|
EffectNum = 0;
|
|
Param = 0;
|
|
|
|
// Unpack note data
|
|
uint8_t note = 0;
|
|
if (chanid & 0x40) {
|
|
uint8_t n = *s++;
|
|
note = n & 0x7F;
|
|
|
|
// Retrigger last instrument?
|
|
if (n & 0x80)
|
|
InstNum = last_instrument;
|
|
}
|
|
|
|
// Do we have an instrument?
|
|
if (chanid & 0x20) {
|
|
InstNum = *s++;
|
|
last_instrument = InstNum;
|
|
}
|
|
|
|
// Do we have an effect?
|
|
if (chanid & 0x10) {
|
|
EffectNum = *s++;
|
|
Param = *s++;
|
|
}
|
|
|
|
NoteNum = note & 15;
|
|
OctaveNum = note >> 4;
|
|
|
|
return ((chanid & 0x80) != 0);
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Get current track as indicated by order list.
|
|
//==================================================================================================
|
|
uint8_t *RADPlayer::GetTrack() {
|
|
|
|
// If at end of tune start again from beginning
|
|
if (Order >= OrderListSize)
|
|
Order = 0;
|
|
|
|
uint8_t track_num = OrderList[Order];
|
|
|
|
// Jump marker? Note, we don't recognise multiple jump markers as that could put us into an
|
|
// infinite loop
|
|
if (track_num & 0x80) {
|
|
Order = track_num & 0x7F;
|
|
track_num = OrderList[Order] & 0x7F;
|
|
}
|
|
|
|
#if RAD_DETECT_REPEATS
|
|
// Check for tune repeat, and mark order in order map
|
|
if (Order < 128) {
|
|
int byte = Order >> 5;
|
|
uint32_t bit = uint32_t(1) << (Order & 31);
|
|
if (OrderMap[byte] & bit)
|
|
Repeating = true;
|
|
else
|
|
OrderMap[byte] |= bit;
|
|
}
|
|
#endif
|
|
|
|
return Tracks[track_num];
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Skip through track till we reach the given line or the next higher one. Returns null if none.
|
|
//==================================================================================================
|
|
uint8_t *RADPlayer::SkipToLine(uint8_t *trk, uint8_t linenum, bool chan_riff) {
|
|
|
|
while (1) {
|
|
|
|
uint8_t lineid = *trk;
|
|
if ((lineid & 0x7F) >= linenum)
|
|
return trk;
|
|
if (lineid & 0x80)
|
|
break;
|
|
trk++;
|
|
|
|
// Skip channel notes
|
|
uint8_t chanid;
|
|
do {
|
|
chanid = *trk++;
|
|
trk += NoteSize[(chanid >> 4) & 7];
|
|
} while (!(chanid & 0x80) && !chan_riff);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Plays one line of current track and advances pointers.
|
|
//==================================================================================================
|
|
void RADPlayer::PlayLine() {
|
|
|
|
SpeedCnt--;
|
|
if (SpeedCnt > 0)
|
|
return;
|
|
SpeedCnt = Speed;
|
|
|
|
// Reset channel effects
|
|
for (int i = 0; i < kChannels; i++)
|
|
ResetFX(&Channels[i].FX);
|
|
|
|
LineJump = -1;
|
|
|
|
// At the right line?
|
|
uint8_t *trk = Track;
|
|
if (trk && (*trk & 0x7F) <= Line) {
|
|
uint8_t lineid = *trk++;
|
|
|
|
// Run through channels
|
|
bool last;
|
|
do {
|
|
int channum = *trk & 15;
|
|
CChannel &chan = Channels[channum];
|
|
last = UnpackNote(trk, chan.LastInstrument);
|
|
PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param);
|
|
} while (!last);
|
|
|
|
// Was this the last line?
|
|
if (lineid & 0x80)
|
|
trk = 0;
|
|
|
|
Track = trk;
|
|
}
|
|
|
|
// Move to next line
|
|
Line++;
|
|
if (Line >= kTrackLines || LineJump >= 0) {
|
|
|
|
if (LineJump >= 0)
|
|
Line = LineJump;
|
|
else
|
|
Line = 0;
|
|
|
|
// Move to next track in order list
|
|
Order++;
|
|
Track = GetTrack();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Play a single note. Returns the line number in the next pattern to jump to if a jump command was
|
|
// found, or -1 if none.
|
|
//==================================================================================================
|
|
void RADPlayer::PlayNote(int channum, int8_t notenum, int8_t octave, uint16_t instnum, uint8_t cmd, uint8_t param, e_Source src, int op) {
|
|
CChannel &chan = Channels[channum];
|
|
|
|
// Recursion detector. This is needed as riffs can trigger other riffs, and they could end up
|
|
// in a loop
|
|
if (Entrances >= 8)
|
|
return;
|
|
Entrances++;
|
|
|
|
// Select which effects source we're using
|
|
CEffects *fx = &chan.FX;
|
|
if (src == SRiff)
|
|
fx = &chan.Riff.FX;
|
|
else if (src == SIRiff)
|
|
fx = &chan.IRiff.FX;
|
|
|
|
bool transposing = false;
|
|
|
|
// For tone-slides the note is the target
|
|
if (cmd == cmToneSlide) {
|
|
if (notenum > 0 && notenum <= 12) {
|
|
fx->ToneSlideOct = octave;
|
|
fx->ToneSlideFreq = NoteFreq[notenum - 1];
|
|
}
|
|
goto toneslide;
|
|
}
|
|
|
|
// Playing a new instrument?
|
|
if (instnum > 0) {
|
|
CInstrument *oldinst = chan.Instrument;
|
|
CInstrument *inst = &Instruments[instnum - 1];
|
|
chan.Instrument = inst;
|
|
|
|
// Ignore MIDI instruments
|
|
if (inst->Algorithm == 7) {
|
|
Entrances--;
|
|
return;
|
|
}
|
|
|
|
LoadInstrumentOPL3(channum);
|
|
|
|
// Bounce the channel
|
|
chan.KeyFlags |= fKeyOff | fKeyOn;
|
|
|
|
ResetFX(&chan.IRiff.FX);
|
|
|
|
if (src != SIRiff || inst != oldinst) {
|
|
|
|
// Instrument riff?
|
|
if (inst->Riff && inst->RiffSpeed > 0) {
|
|
|
|
chan.IRiff.Track = chan.IRiff.TrackStart = inst->Riff;
|
|
chan.IRiff.Line = 0;
|
|
chan.IRiff.Speed = inst->RiffSpeed;
|
|
chan.IRiff.LastInstrument = 0;
|
|
|
|
// Note given with riff command is used to transpose the riff
|
|
if (notenum >= 1 && notenum <= 12) {
|
|
chan.IRiff.TransposeOctave = octave;
|
|
chan.IRiff.TransposeNote = notenum;
|
|
transposing = true;
|
|
} else {
|
|
chan.IRiff.TransposeOctave = 3;
|
|
chan.IRiff.TransposeNote = 12;
|
|
}
|
|
|
|
// Do first tick of riff
|
|
chan.IRiff.SpeedCnt = 1;
|
|
TickRiff(channum, chan.IRiff, false);
|
|
|
|
} else
|
|
chan.IRiff.SpeedCnt = 0;
|
|
}
|
|
}
|
|
|
|
// Starting a channel riff?
|
|
if (cmd == cmRiff || cmd == cmTranspose) {
|
|
|
|
ResetFX(&chan.Riff.FX);
|
|
|
|
uint8_t p0 = param / 10;
|
|
uint8_t p1 = param % 10;
|
|
chan.Riff.Track = p1 > 0 ? Riffs[p0][p1 - 1] : 0;
|
|
if (chan.Riff.Track) {
|
|
|
|
chan.Riff.TrackStart = chan.Riff.Track;
|
|
chan.Riff.Line = 0;
|
|
chan.Riff.Speed = Speed;
|
|
chan.Riff.LastInstrument = 0;
|
|
|
|
// Note given with riff command is used to transpose the riff
|
|
if (cmd == cmTranspose && notenum >= 1 && notenum <= 12) {
|
|
chan.Riff.TransposeOctave = octave;
|
|
chan.Riff.TransposeNote = notenum;
|
|
transposing = true;
|
|
} else {
|
|
chan.Riff.TransposeOctave = 3;
|
|
chan.Riff.TransposeNote = 12;
|
|
}
|
|
|
|
// Do first tick of riff
|
|
chan.Riff.SpeedCnt = 1;
|
|
TickRiff(channum, chan.Riff, true);
|
|
|
|
} else
|
|
chan.Riff.SpeedCnt = 0;
|
|
}
|
|
|
|
// Play the note
|
|
if (!transposing && notenum > 0) {
|
|
|
|
// Key-off?
|
|
if (notenum == 15)
|
|
chan.KeyFlags |= fKeyOff;
|
|
|
|
if (!chan.Instrument || chan.Instrument->Algorithm < 7)
|
|
PlayNoteOPL3(channum, octave, notenum);
|
|
}
|
|
|
|
// Process effect
|
|
switch (cmd) {
|
|
|
|
case cmSetVol:
|
|
SetVolume(channum, param);
|
|
break;
|
|
|
|
case cmSetSpeed:
|
|
if (src == SNone) {
|
|
Speed = param;
|
|
SpeedCnt = param;
|
|
} else if (src == SRiff) {
|
|
chan.Riff.Speed = param;
|
|
chan.Riff.SpeedCnt = param;
|
|
} else if (src == SIRiff) {
|
|
chan.IRiff.Speed = param;
|
|
chan.IRiff.SpeedCnt = param;
|
|
}
|
|
break;
|
|
|
|
case cmPortamentoUp:
|
|
fx->PortSlide = param;
|
|
break;
|
|
|
|
case cmPortamentoDwn:
|
|
fx->PortSlide = -int8_t(param);
|
|
break;
|
|
|
|
case cmToneVolSlide:
|
|
case cmVolSlide: {
|
|
int8_t val = param;
|
|
if (val >= 50)
|
|
val = -(val - 50);
|
|
fx->VolSlide = val;
|
|
if (cmd != cmToneVolSlide)
|
|
break;
|
|
}
|
|
// Fall through!
|
|
|
|
case cmToneSlide: {
|
|
toneslide:
|
|
uint8_t speed = param;
|
|
if (speed)
|
|
fx->ToneSlideSpeed = speed;
|
|
GetSlideDir(channum, fx);
|
|
break;
|
|
}
|
|
|
|
case cmJumpToLine: {
|
|
if (param >= kTrackLines)
|
|
break;
|
|
|
|
// Note: jump commands in riffs are checked for within TickRiff()
|
|
if (src == SNone)
|
|
LineJump = param;
|
|
|
|
break;
|
|
}
|
|
|
|
case cmMultiplier: {
|
|
if (src == SIRiff)
|
|
LoadInstMultiplierOPL3(channum, op, param);
|
|
break;
|
|
}
|
|
|
|
case cmVolume: {
|
|
if (src == SIRiff)
|
|
LoadInstVolumeOPL3(channum, op, param);
|
|
break;
|
|
}
|
|
|
|
case cmFeedback: {
|
|
if (src == SIRiff) {
|
|
uint8_t which = param / 10;
|
|
uint8_t fb = param % 10;
|
|
LoadInstFeedbackOPL3(channum, which, fb);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
Entrances--;
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Sets the OPL3 registers for a given instrument.
|
|
//==================================================================================================
|
|
void RADPlayer::LoadInstrumentOPL3(int channum) {
|
|
CChannel &chan = Channels[channum];
|
|
|
|
const CInstrument *inst = chan.Instrument;
|
|
if (!inst)
|
|
return;
|
|
|
|
uint8_t alg = inst->Algorithm;
|
|
chan.Volume = inst->Volume;
|
|
chan.DetuneA = (inst->Detune + 1) >> 1;
|
|
chan.DetuneB = inst->Detune >> 1;
|
|
|
|
// Turn on 4-op mode for algorithms 2 and 3 (algorithms 4 to 6 are simulated with 2-op mode)
|
|
if (channum < 6) {
|
|
uint8_t mask = 1 << channum;
|
|
SetOPL3(0x104, (GetOPL3(0x104) & ~mask) | (alg == 2 || alg == 3 ? mask : 0));
|
|
}
|
|
|
|
// Left/right/feedback/algorithm
|
|
SetOPL3(0xC0 + ChanOffsets3[channum], ((inst->Panning[1] ^ 3) << 4) | inst->Feedback[1] << 1 | (alg == 3 || alg == 5 || alg == 6 ? 1 : 0));
|
|
SetOPL3(0xC0 + Chn2Offsets3[channum], ((inst->Panning[0] ^ 3) << 4) | inst->Feedback[0] << 1 | (alg == 1 || alg == 6 ? 1 : 0));
|
|
|
|
// Load the operators
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
static const uint8_t blank[] = { 0, 0x3F, 0, 0xF0, 0 };
|
|
const uint8_t *op = (alg < 2 && i >= 2) ? blank : inst->Operators[i];
|
|
uint16_t reg = OpOffsets3[channum][i];
|
|
|
|
uint16_t vol = ~op[1] & 0x3F;
|
|
|
|
// Do volume scaling for carriers
|
|
if (AlgCarriers[alg][i]) {
|
|
vol = vol * inst->Volume / 64;
|
|
vol = vol * MasterVol / 64;
|
|
}
|
|
|
|
SetOPL3(reg + 0x20, op[0]);
|
|
SetOPL3(reg + 0x40, (op[1] & 0xC0) | ((vol ^ 0x3F) & 0x3F));
|
|
SetOPL3(reg + 0x60, op[2]);
|
|
SetOPL3(reg + 0x80, op[3]);
|
|
SetOPL3(reg + 0xE0, op[4]);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Play note on OPL3 hardware.
|
|
//==================================================================================================
|
|
void RADPlayer::PlayNoteOPL3(int channum, int8_t octave, int8_t note) {
|
|
CChannel &chan = Channels[channum];
|
|
|
|
uint16_t o1 = ChanOffsets3[channum];
|
|
uint16_t o2 = Chn2Offsets3[channum];
|
|
|
|
// Key off the channel
|
|
if (chan.KeyFlags & fKeyOff) {
|
|
chan.KeyFlags &= ~(fKeyOff | fKeyedOn);
|
|
SetOPL3(0xB0 + o1, GetOPL3(0xB0 + o1) & ~0x20);
|
|
SetOPL3(0xB0 + o2, GetOPL3(0xB0 + o2) & ~0x20);
|
|
}
|
|
|
|
if (note == 15)
|
|
return;
|
|
|
|
bool op4 = (chan.Instrument && chan.Instrument->Algorithm >= 2);
|
|
|
|
uint16_t freq = NoteFreq[note - 1];
|
|
uint16_t frq2 = freq;
|
|
|
|
chan.CurrFreq = freq;
|
|
chan.CurrOctave = octave;
|
|
|
|
// Detune. We detune both channels in the opposite direction so the note retains its tuning
|
|
freq += chan.DetuneA;
|
|
frq2 -= chan.DetuneB;
|
|
|
|
// Frequency low byte
|
|
if (op4)
|
|
SetOPL3(0xA0 + o1, frq2 & 0xFF);
|
|
SetOPL3(0xA0 + o2, freq & 0xFF);
|
|
|
|
// Frequency high bits + octave + key on
|
|
if (chan.KeyFlags & fKeyOn)
|
|
chan.KeyFlags = (chan.KeyFlags & ~fKeyOn) | fKeyedOn;
|
|
if (op4)
|
|
SetOPL3(0xB0 + o1, (frq2 >> 8) | (octave << 2) | ((chan.KeyFlags & fKeyedOn) ? 0x20 : 0));
|
|
else
|
|
SetOPL3(0xB0 + o1, 0);
|
|
SetOPL3(0xB0 + o2, (freq >> 8) | (octave << 2) | ((chan.KeyFlags & fKeyedOn) ? 0x20 : 0));
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Prepare FX for new line.
|
|
//==================================================================================================
|
|
void RADPlayer::ResetFX(CEffects *fx) {
|
|
fx->PortSlide = 0;
|
|
fx->VolSlide = 0;
|
|
fx->ToneSlideDir = 0;
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Tick the channel riff.
|
|
//==================================================================================================
|
|
void RADPlayer::TickRiff(int channum, CChannel::CRiff &riff, bool chan_riff) {
|
|
uint8_t lineid;
|
|
|
|
if (riff.SpeedCnt == 0) {
|
|
ResetFX(&riff.FX);
|
|
return;
|
|
}
|
|
|
|
riff.SpeedCnt--;
|
|
if (riff.SpeedCnt > 0)
|
|
return;
|
|
riff.SpeedCnt = riff.Speed;
|
|
|
|
uint8_t line = riff.Line++;
|
|
if (riff.Line >= kTrackLines)
|
|
riff.SpeedCnt = 0;
|
|
|
|
ResetFX(&riff.FX);
|
|
|
|
// Is this the current line in track?
|
|
uint8_t *trk = riff.Track;
|
|
if (trk && (*trk & 0x7F) == line) {
|
|
lineid = *trk++;
|
|
|
|
if (chan_riff) {
|
|
|
|
// Channel riff: play current note
|
|
UnpackNote(trk, riff.LastInstrument);
|
|
Transpose(riff.TransposeNote, riff.TransposeOctave);
|
|
PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param, SRiff);
|
|
|
|
} else {
|
|
|
|
// Instrument riff: here each track channel is an extra effect that can run, but is not
|
|
// actually a different physical channel
|
|
bool last;
|
|
do {
|
|
int col = *trk & 15;
|
|
last = UnpackNote(trk, riff.LastInstrument);
|
|
if (EffectNum != cmIgnore)
|
|
Transpose(riff.TransposeNote, riff.TransposeOctave);
|
|
PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param, SIRiff, col > 0 ? (col - 1) & 3 : 0);
|
|
} while (!last);
|
|
}
|
|
|
|
// Last line?
|
|
if (lineid & 0x80)
|
|
trk = 0;
|
|
|
|
riff.Track = trk;
|
|
}
|
|
|
|
// Special case; if next line has a jump command, run it now
|
|
if (!trk || (*trk++ & 0x7F) != riff.Line)
|
|
return;
|
|
|
|
UnpackNote(trk, lineid); // lineid is just a dummy here
|
|
if (EffectNum == cmJumpToLine && Param < kTrackLines) {
|
|
riff.Line = Param;
|
|
riff.Track = SkipToLine(riff.TrackStart, Param, chan_riff);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// This continues any effects that operate continuously (eg. slides).
|
|
//==================================================================================================
|
|
void RADPlayer::ContinueFX(int channum, CEffects *fx) {
|
|
CChannel &chan = Channels[channum];
|
|
|
|
if (fx->PortSlide)
|
|
Portamento(channum, fx, fx->PortSlide, false);
|
|
|
|
if (fx->VolSlide) {
|
|
int8_t vol = chan.Volume;
|
|
vol -= fx->VolSlide;
|
|
if (vol < 0)
|
|
vol = 0;
|
|
SetVolume(channum, vol);
|
|
}
|
|
|
|
if (fx->ToneSlideDir)
|
|
Portamento(channum, fx, fx->ToneSlideDir, true);
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Sets the volume of given channel.
|
|
//==================================================================================================
|
|
void RADPlayer::SetVolume(int channum, uint8_t vol) {
|
|
CChannel &chan = Channels[channum];
|
|
|
|
// Ensure volume is within range
|
|
if (vol > 64)
|
|
vol = 64;
|
|
|
|
chan.Volume = vol;
|
|
|
|
// Scale volume to master volume
|
|
vol = vol * MasterVol / 64;
|
|
|
|
CInstrument *inst = chan.Instrument;
|
|
if (!inst)
|
|
return;
|
|
uint8_t alg = inst->Algorithm;
|
|
|
|
// Set volume of all carriers
|
|
for (int i = 0; i < 4; i++) {
|
|
uint8_t *op = inst->Operators[i];
|
|
|
|
// Is this operator a carrier?
|
|
if (!AlgCarriers[alg][i])
|
|
continue;
|
|
|
|
uint8_t opvol = uint16_t((op[1] & 63) ^ 63) * vol / 64;
|
|
uint16_t reg = 0x40 + OpOffsets3[channum][i];
|
|
SetOPL3(reg, (GetOPL3(reg) & 0xC0) | (opvol ^ 0x3F));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Starts a tone-slide.
|
|
//==================================================================================================
|
|
void RADPlayer::GetSlideDir(int channum, CEffects *fx) {
|
|
CChannel &chan = Channels[channum];
|
|
|
|
int8_t speed = fx->ToneSlideSpeed;
|
|
if (speed > 0) {
|
|
uint8_t oct = fx->ToneSlideOct;
|
|
uint16_t freq = fx->ToneSlideFreq;
|
|
|
|
uint16_t oldfreq = chan.CurrFreq;
|
|
uint8_t oldoct = chan.CurrOctave;
|
|
|
|
if (oldoct > oct)
|
|
speed = -speed;
|
|
else if (oldoct == oct) {
|
|
if (oldfreq > freq)
|
|
speed = -speed;
|
|
else if (oldfreq == freq)
|
|
speed = 0;
|
|
}
|
|
}
|
|
|
|
fx->ToneSlideDir = speed;
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Load multiplier value into operator.
|
|
//==================================================================================================
|
|
void RADPlayer::LoadInstMultiplierOPL3(int channum, int op, uint8_t mult) {
|
|
uint16_t reg = 0x20 + OpOffsets3[channum][op];
|
|
SetOPL3(reg, (GetOPL3(reg) & 0xF0) | (mult & 15));
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Load volume value into operator.
|
|
//==================================================================================================
|
|
void RADPlayer::LoadInstVolumeOPL3(int channum, int op, uint8_t vol) {
|
|
uint16_t reg = 0x40 + OpOffsets3[channum][op];
|
|
SetOPL3(reg, (GetOPL3(reg) & 0xC0) | ((vol & 0x3F) ^ 0x3F));
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Load feedback value into instrument.
|
|
//==================================================================================================
|
|
void RADPlayer::LoadInstFeedbackOPL3(int channum, int which, uint8_t fb) {
|
|
|
|
if (which == 0) {
|
|
|
|
uint16_t reg = 0xC0 + Chn2Offsets3[channum];
|
|
SetOPL3(reg, (GetOPL3(reg) & 0x31) | ((fb & 7) << 1));
|
|
|
|
} else if (which == 1) {
|
|
|
|
uint16_t reg = 0xC0 + ChanOffsets3[channum];
|
|
SetOPL3(reg, (GetOPL3(reg) & 0x31) | ((fb & 7) << 1));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// This adjusts the pitch of the given channel's note. There may also be a limiting value on the
|
|
// portamento (for tone slides).
|
|
//==================================================================================================
|
|
void RADPlayer::Portamento(uint16_t channum, CEffects *fx, int8_t amount, bool toneslide) {
|
|
CChannel &chan = Channels[channum];
|
|
|
|
uint16_t freq = chan.CurrFreq;
|
|
uint8_t oct = chan.CurrOctave;
|
|
|
|
freq += amount;
|
|
|
|
if (freq < 0x156) {
|
|
|
|
if (oct > 0) {
|
|
oct--;
|
|
freq += 0x2AE - 0x156;
|
|
} else
|
|
freq = 0x156;
|
|
|
|
} else if (freq > 0x2AE) {
|
|
|
|
if (oct < 7) {
|
|
oct++;
|
|
freq -= 0x2AE - 0x156;
|
|
} else
|
|
freq = 0x2AE;
|
|
}
|
|
|
|
if (toneslide) {
|
|
|
|
if (amount >= 0) {
|
|
|
|
if (oct > fx->ToneSlideOct || (oct == fx->ToneSlideOct && freq >= fx->ToneSlideFreq)) {
|
|
freq = fx->ToneSlideFreq;
|
|
oct = fx->ToneSlideOct;
|
|
}
|
|
|
|
} else {
|
|
|
|
if (oct < fx->ToneSlideOct || (oct == fx->ToneSlideOct && freq <= fx->ToneSlideFreq)) {
|
|
freq = fx->ToneSlideFreq;
|
|
oct = fx->ToneSlideOct;
|
|
}
|
|
}
|
|
}
|
|
|
|
chan.CurrFreq = freq;
|
|
chan.CurrOctave = oct;
|
|
|
|
// Apply detunes
|
|
uint16_t frq2 = freq - chan.DetuneB;
|
|
freq += chan.DetuneA;
|
|
|
|
// Write value back to OPL3
|
|
uint16_t chan_offset = Chn2Offsets3[channum];
|
|
SetOPL3(0xA0 + chan_offset, freq & 0xFF);
|
|
SetOPL3(0xB0 + chan_offset, (freq >> 8 & 3) | oct << 2 | (GetOPL3(0xB0 + chan_offset) & 0xE0));
|
|
|
|
chan_offset = ChanOffsets3[channum];
|
|
SetOPL3(0xA0 + chan_offset, frq2 & 0xFF);
|
|
SetOPL3(0xB0 + chan_offset, (frq2 >> 8 & 3) | oct << 2 | (GetOPL3(0xB0 + chan_offset) & 0xE0));
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Transpose the note returned by UnpackNote().
|
|
// Note: due to RAD's wonky legacy middle C is octave 3 note number 12.
|
|
//==================================================================================================
|
|
void RADPlayer::Transpose(int8_t note, int8_t octave) {
|
|
|
|
if (NoteNum >= 1 && NoteNum <= 12) {
|
|
|
|
int8_t toct = octave - 3;
|
|
if (toct != 0) {
|
|
OctaveNum += toct;
|
|
if (OctaveNum < 0)
|
|
OctaveNum = 0;
|
|
else if (OctaveNum > 7)
|
|
OctaveNum = 7;
|
|
}
|
|
|
|
int8_t tnot = note - 12;
|
|
if (tnot != 0) {
|
|
NoteNum += tnot;
|
|
if (NoteNum < 1) {
|
|
NoteNum += 12;
|
|
if (OctaveNum > 0)
|
|
OctaveNum--;
|
|
else
|
|
NoteNum = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//==================================================================================================
|
|
// Compute total time of tune if it didn't repeat. Note, this stops the tune so should only be done
|
|
// prior to initial playback.
|
|
//==================================================================================================
|
|
#if RAD_DETECT_REPEATS
|
|
static void RADPlayerDummyOPL3(void *arg, uint16_t reg, uint8_t data) {}
|
|
//--------------------------------------------------------------------------------------------------
|
|
uint32_t RADPlayer::ComputeTotalTime() {
|
|
|
|
Stop();
|
|
void (*old_opl3)(void *, uint16_t, uint8_t) = OPL3;
|
|
OPL3 = RADPlayerDummyOPL3;
|
|
|
|
while (!Update())
|
|
;
|
|
uint32_t total = PlayTime;
|
|
|
|
Stop();
|
|
OPL3 = old_opl3;
|
|
|
|
return total / Hertz;
|
|
}
|
|
#endif
|