mirror of https://github.com/arendst/Tasmota.git
877 lines
28 KiB
C++
877 lines
28 KiB
C++
/*
|
|
AudioGeneratorMOD
|
|
Audio output generator that plays Amiga MOD tracker files
|
|
|
|
Copyright (C) 2017 Earle F. Philhower, III
|
|
|
|
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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#define PGM_READ_UNALIGNED 0
|
|
|
|
#include "AudioGeneratorMOD.h"
|
|
|
|
/*
|
|
Ported/hacked out from STELLARPLAYER by Ronen K.
|
|
http://mobile4dev.blogspot.com/2012/11/stellaris-launchpad-mod-player.html
|
|
A version exists in GitHub at https://github.com/steveway/stellarplayer
|
|
and also at https://github.com/MikesModz/StellarPlayer
|
|
Both which were themselves a port of the PIC32 MOD player
|
|
https://www.youtube.com/watch?v=i3Yl0TISQBE (seems to no longer be available.)
|
|
|
|
Most changes involved reducing memory usage by changing data structures,
|
|
moving constants to PROGMEM and minor tweaks to allow non pow2 buffer sizes.
|
|
*/
|
|
|
|
#pragma GCC optimize ("O3")
|
|
|
|
#define NOTE(r, c) (Player.currentPattern.note8[r][c]==NONOTE8?NONOTE:8*Player.currentPattern.note8[r][c])
|
|
|
|
#ifndef min
|
|
#define min(X,Y) ((X) < (Y) ? (X) : (Y))
|
|
#endif
|
|
|
|
AudioGeneratorMOD::AudioGeneratorMOD()
|
|
{
|
|
sampleRate = 44100;
|
|
fatBufferSize = 6 * 1024;
|
|
stereoSeparation = 32;
|
|
mixerTick = 0;
|
|
usePAL = false;
|
|
UpdateAmiga();
|
|
running = false;
|
|
file = NULL;
|
|
output = NULL;
|
|
}
|
|
|
|
AudioGeneratorMOD::~AudioGeneratorMOD()
|
|
{
|
|
// Free any remaining buffers
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
FatBuffer.channels[i] = NULL;
|
|
}
|
|
}
|
|
|
|
bool AudioGeneratorMOD::stop()
|
|
{
|
|
// We may be stopping because of allocation failures, so always deallocate
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
free(FatBuffer.channels[i]);
|
|
FatBuffer.channels[i] = NULL;
|
|
}
|
|
if (file) file->close();
|
|
running = false;
|
|
output->stop();
|
|
return true;
|
|
}
|
|
|
|
bool AudioGeneratorMOD::loop()
|
|
{
|
|
if (!running) goto done; // Easy-peasy
|
|
|
|
// First, try and push in the stored sample. If we can't, then punt and try later
|
|
if (!output->ConsumeSample(lastSample)) goto done; // FIFO full, wait...
|
|
|
|
// Now advance enough times to fill the i2s buffer
|
|
do {
|
|
if (mixerTick == 0) {
|
|
running = RunPlayer();
|
|
if (!running) {
|
|
stop();
|
|
goto done;
|
|
}
|
|
mixerTick = Player.samplesPerTick;
|
|
}
|
|
GetSample( lastSample );
|
|
mixerTick--;
|
|
} while (output->ConsumeSample(lastSample));
|
|
|
|
done:
|
|
file->loop();
|
|
output->loop();
|
|
|
|
// We'll be left with one sample still in our buffer because it couldn't fit in the FIFO
|
|
return running;
|
|
}
|
|
|
|
bool AudioGeneratorMOD::begin(AudioFileSource *source, AudioOutput *out)
|
|
{
|
|
if (running) stop();
|
|
|
|
if (!source) return false;
|
|
file = source;
|
|
if (!out) return false;
|
|
output = out;
|
|
|
|
if (!file->isOpen()) return false; // Can't read the file!
|
|
|
|
// Set the output values properly
|
|
if (!output->SetRate(sampleRate)) return false;
|
|
if (!output->SetBitsPerSample(16)) return false;
|
|
if (!output->SetChannels(2)) return false;
|
|
if (!output->begin()) return false;
|
|
|
|
UpdateAmiga();
|
|
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
FatBuffer.channels[i] = reinterpret_cast<uint8_t*>(calloc(fatBufferSize, 1));
|
|
if (!FatBuffer.channels[i]) {
|
|
stop();
|
|
return false;
|
|
}
|
|
}
|
|
if (!LoadMOD()) {
|
|
stop();
|
|
return false;
|
|
}
|
|
running = true;
|
|
return true;
|
|
}
|
|
|
|
// Sorted Amiga periods
|
|
static const uint16_t amigaPeriods[296] PROGMEM = {
|
|
907, 900, 894, 887, 881, 875, 868, 862, // -8 to -1
|
|
856, 850, 844, 838, 832, 826, 820, 814, // C-1 to +7
|
|
808, 802, 796, 791, 785, 779, 774, 768, // C#1 to +7
|
|
762, 757, 752, 746, 741, 736, 730, 725, // D-1 to +7
|
|
720, 715, 709, 704, 699, 694, 689, 684, // D#1 to +7
|
|
678, 675, 670, 665, 660, 655, 651, 646, // E-1 to +7
|
|
640, 636, 632, 628, 623, 619, 614, 610, // F-1 to +7
|
|
604, 601, 597, 592, 588, 584, 580, 575, // F#1 to +7
|
|
570, 567, 563, 559, 555, 551, 547, 543, // G-1 to +7
|
|
538, 535, 532, 528, 524, 520, 516, 513, // G#1 to +7
|
|
508, 505, 502, 498, 494, 491, 487, 484, // A-1 to +7
|
|
480, 477, 474, 470, 467, 463, 460, 457, // A#1 to +7
|
|
453, 450, 447, 444, 441, 437, 434, 431, // B-1 to +7
|
|
428, 425, 422, 419, 416, 413, 410, 407, // C-2 to +7
|
|
404, 401, 398, 395, 392, 390, 387, 384, // C#2 to +7
|
|
381, 379, 376, 373, 370, 368, 365, 363, // D-2 to +7
|
|
360, 357, 355, 352, 350, 347, 345, 342, // D#2 to +7
|
|
339, 337, 335, 332, 330, 328, 325, 323, // E-2 to +7
|
|
320, 318, 316, 314, 312, 309, 307, 305, // F-2 to +7
|
|
302, 300, 298, 296, 294, 292, 290, 288, // F#2 to +7
|
|
285, 284, 282, 280, 278, 276, 274, 272, // G-2 to +7
|
|
269, 268, 266, 264, 262, 260, 258, 256, // G#2 to +7
|
|
254, 253, 251, 249, 247, 245, 244, 242, // A-2 to +7
|
|
240, 238, 237, 235, 233, 232, 230, 228, // A#2 to +7
|
|
226, 225, 223, 222, 220, 219, 217, 216, // B-2 to +7
|
|
214, 212, 211, 209, 208, 206, 205, 203, // C-3 to +7
|
|
202, 200, 199, 198, 196, 195, 193, 192, // C#3 to +7
|
|
190, 189, 188, 187, 185, 184, 183, 181, // D-3 to +7
|
|
180, 179, 177, 176, 175, 174, 172, 171, // D#3 to +7
|
|
170, 169, 167, 166, 165, 164, 163, 161, // E-3 to +7
|
|
160, 159, 158, 157, 156, 155, 154, 152, // F-3 to +7
|
|
151, 150, 149, 148, 147, 146, 145, 144, // F#3 to +7
|
|
143, 142, 141, 140, 139, 138, 137, 136, // G-3 to +7
|
|
135, 134, 133, 132, 131, 130, 129, 128, // G#3 to +7
|
|
127, 126, 125, 125, 123, 123, 122, 121, // A-3 to +7
|
|
120, 119, 118, 118, 117, 116, 115, 114, // A#3 to +7
|
|
113, 113, 112, 111, 110, 109, 109, 108 // B-3 to +7
|
|
};
|
|
#define ReadAmigaPeriods(a) (uint16_t)pgm_read_word(amigaPeriods + (a))
|
|
|
|
static const uint8_t sine[64] PROGMEM = {
|
|
0, 24, 49, 74, 97, 120, 141, 161,
|
|
180, 197, 212, 224, 235, 244, 250, 253,
|
|
255, 253, 250, 244, 235, 224, 212, 197,
|
|
180, 161, 141, 120, 97, 74, 49, 24
|
|
};
|
|
#define ReadSine(a) pgm_read_byte(sine + (a))
|
|
|
|
|
|
static inline uint16_t MakeWord(uint8_t h, uint8_t l) { return h << 8 | l; }
|
|
|
|
bool AudioGeneratorMOD::LoadHeader()
|
|
{
|
|
uint8_t i;
|
|
uint8_t temp[4];
|
|
uint8_t junk[22];
|
|
|
|
if (20 != file->read(/*Mod.name*/junk, 20)) return false; // Skip MOD name
|
|
for (i = 0; i < SAMPLES; i++) {
|
|
if (22 != file->read(junk /*Mod.samples[i].name*/, 22)) return false; // Skip sample name
|
|
if (2 != file->read(temp, 2)) return false;
|
|
Mod.samples[i].length = MakeWord(temp[0], temp[1]) * 2;
|
|
if (1 != file->read(reinterpret_cast<uint8_t*>(&Mod.samples[i].fineTune), 1)) return false;
|
|
if (Mod.samples[i].fineTune > 7) Mod.samples[i].fineTune -= 16;
|
|
if (1 != file->read(&Mod.samples[i].volume, 1)) return false;
|
|
if (2 != file->read(temp, 2)) return false;
|
|
Mod.samples[i].loopBegin = MakeWord(temp[0], temp[1]) * 2;
|
|
if (2 != file->read(temp, 2)) return false;
|
|
Mod.samples[i].loopLength = MakeWord(temp[0], temp[1]) * 2;
|
|
if (Mod.samples[i].loopBegin + Mod.samples[i].loopLength > Mod.samples[i].length)
|
|
Mod.samples[i].loopLength = Mod.samples[i].length - Mod.samples[i].loopBegin;
|
|
}
|
|
|
|
if (1 != file->read(&Mod.songLength, 1)) return false;
|
|
if (1 != file->read(temp, 1)) return false; // Discard this byte
|
|
|
|
Mod.numberOfPatterns = 0;
|
|
for (i = 0; i < 128; i++) {
|
|
if (1 != file->read(&Mod.order[i], 1)) return false;
|
|
if (Mod.order[i] > Mod.numberOfPatterns)
|
|
Mod.numberOfPatterns = Mod.order[i];
|
|
}
|
|
Mod.numberOfPatterns++;
|
|
|
|
// Offset 1080
|
|
if (4 != file->read(temp, 4)) return false;;
|
|
if (!strncmp(reinterpret_cast<const char*>(temp + 1), "CHN", 3))
|
|
Mod.numberOfChannels = temp[0] - '0';
|
|
else if (!strncmp(reinterpret_cast<const char*>(temp + 2), "CH", 2))
|
|
Mod.numberOfChannels = (temp[0] - '0') * 10 + temp[1] - '0';
|
|
else
|
|
Mod.numberOfChannels = 4;
|
|
|
|
return true;
|
|
}
|
|
|
|
void AudioGeneratorMOD::LoadSamples()
|
|
{
|
|
uint8_t i;
|
|
uint32_t fileOffset = 1084 + Mod.numberOfPatterns * ROWS * Mod.numberOfChannels * 4 - 1;
|
|
|
|
for (i = 0; i < SAMPLES; i++) {
|
|
|
|
if (Mod.samples[i].length) {
|
|
Mixer.sampleBegin[i] = fileOffset;
|
|
Mixer.sampleEnd[i] = fileOffset + Mod.samples[i].length;
|
|
if (Mod.samples[i].loopLength > 2) {
|
|
Mixer.sampleloopBegin[i] = fileOffset + Mod.samples[i].loopBegin;
|
|
Mixer.sampleLoopLength[i] = Mod.samples[i].loopLength;
|
|
Mixer.sampleLoopEnd[i] = Mixer.sampleloopBegin[i] + Mixer.sampleLoopLength[i];
|
|
} else {
|
|
Mixer.sampleloopBegin[i] = 0;
|
|
Mixer.sampleLoopLength[i] = 0;
|
|
Mixer.sampleLoopEnd[i] = 0;
|
|
}
|
|
fileOffset += Mod.samples[i].length;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bool AudioGeneratorMOD::LoadPattern(uint8_t pattern)
|
|
{
|
|
uint8_t row;
|
|
uint8_t channel;
|
|
uint8_t i;
|
|
uint8_t temp[4];
|
|
uint16_t amigaPeriod;
|
|
|
|
if (!file->seek(1084 + pattern * ROWS * Mod.numberOfChannels * 4, SEEK_SET)) return false;
|
|
|
|
for (row = 0; row < ROWS; row++) {
|
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
|
|
|
if (4 != file->read(temp, 4)) return false;
|
|
|
|
Player.currentPattern.sampleNumber[row][channel] = (temp[0] & 0xF0) + (temp[2] >> 4);
|
|
|
|
amigaPeriod = ((temp[0] & 0xF) << 8) + temp[1];
|
|
// Player.currentPattern.note[row][channel] = NONOTE;
|
|
Player.currentPattern.note8[row][channel] = NONOTE8;
|
|
for (i = 1; i < 37; i++)
|
|
if (amigaPeriod > ReadAmigaPeriods(i * 8) - 3 &&
|
|
amigaPeriod < ReadAmigaPeriods(i * 8) + 3)
|
|
Player.currentPattern.note8[row][channel] = i;
|
|
|
|
Player.currentPattern.effectNumber[row][channel] = temp[2] & 0xF;
|
|
Player.currentPattern.effectParameter[row][channel] = temp[3];
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AudioGeneratorMOD::Portamento(uint8_t channel)
|
|
{
|
|
if (Player.lastAmigaPeriod[channel] < Player.portamentoNote[channel]) {
|
|
Player.lastAmigaPeriod[channel] += Player.portamentoSpeed[channel];
|
|
if (Player.lastAmigaPeriod[channel] > Player.portamentoNote[channel])
|
|
Player.lastAmigaPeriod[channel] = Player.portamentoNote[channel];
|
|
}
|
|
if (Player.lastAmigaPeriod[channel] > Player.portamentoNote[channel]) {
|
|
Player.lastAmigaPeriod[channel] -= Player.portamentoSpeed[channel];
|
|
if (Player.lastAmigaPeriod[channel] < Player.portamentoNote[channel])
|
|
Player.lastAmigaPeriod[channel] = Player.portamentoNote[channel];
|
|
}
|
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
|
}
|
|
|
|
void AudioGeneratorMOD::Vibrato(uint8_t channel)
|
|
{
|
|
uint16_t delta;
|
|
uint16_t temp;
|
|
|
|
temp = Player.vibratoPos[channel] & 31;
|
|
|
|
switch (Player.waveControl[channel] & 3) {
|
|
case 0:
|
|
delta = ReadSine(temp);
|
|
break;
|
|
case 1:
|
|
temp <<= 3;
|
|
if (Player.vibratoPos[channel] < 0)
|
|
temp = 255 - temp;
|
|
delta = temp;
|
|
break;
|
|
case 2:
|
|
delta = 255;
|
|
break;
|
|
case 3:
|
|
delta = rand() & 255;
|
|
break;
|
|
}
|
|
|
|
delta *= Player.vibratoDepth[channel];
|
|
delta >>= 7;
|
|
|
|
if (Player.vibratoPos[channel] >= 0)
|
|
Mixer.channelFrequency[channel] = Player.amiga / (Player.lastAmigaPeriod[channel] + delta);
|
|
else
|
|
Mixer.channelFrequency[channel] = Player.amiga / (Player.lastAmigaPeriod[channel] - delta);
|
|
|
|
Player.vibratoPos[channel] += Player.vibratoSpeed[channel];
|
|
if (Player.vibratoPos[channel] > 31) Player.vibratoPos[channel] -= 64;
|
|
}
|
|
|
|
void AudioGeneratorMOD::Tremolo(uint8_t channel)
|
|
{
|
|
uint16_t delta;
|
|
uint16_t temp;
|
|
|
|
temp = Player.tremoloPos[channel] & 31;
|
|
|
|
switch (Player.waveControl[channel] & 3) {
|
|
case 0:
|
|
delta = ReadSine(temp);
|
|
break;
|
|
case 1:
|
|
temp <<= 3;
|
|
if (Player.tremoloPos[channel] < 0)
|
|
temp = 255 - temp;
|
|
delta = temp;
|
|
break;
|
|
case 2:
|
|
delta = 255;
|
|
break;
|
|
case 3:
|
|
delta = rand() & 255;
|
|
break;
|
|
}
|
|
|
|
delta *= Player.tremoloDepth[channel];
|
|
delta >>= 6;
|
|
|
|
if (Player.tremoloPos[channel] >= 0) {
|
|
if (Player.volume[channel] + delta > 64) delta = 64 - Player.volume[channel];
|
|
Mixer.channelVolume[channel] = Player.volume[channel] + delta;
|
|
} else {
|
|
if (Player.volume[channel] - delta < 0) delta = Player.volume[channel];
|
|
Mixer.channelVolume[channel] = Player.volume[channel] - delta;
|
|
}
|
|
|
|
Player.tremoloPos[channel] += Player.tremoloSpeed[channel];
|
|
if (Player.tremoloPos[channel] > 31) Player.tremoloPos[channel] -= 64;
|
|
}
|
|
|
|
bool AudioGeneratorMOD::ProcessRow()
|
|
{
|
|
bool jumpFlag;
|
|
bool breakFlag;
|
|
uint8_t channel;
|
|
uint8_t sampleNumber;
|
|
uint16_t note;
|
|
uint8_t effectNumber;
|
|
uint8_t effectParameter;
|
|
uint8_t effectParameterX;
|
|
uint8_t effectParameterY;
|
|
uint16_t sampleOffset;
|
|
|
|
if (!running) return false;
|
|
|
|
Player.lastRow = Player.row++;
|
|
jumpFlag = false;
|
|
breakFlag = false;
|
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
|
|
|
sampleNumber = Player.currentPattern.sampleNumber[Player.lastRow][channel];
|
|
note = NOTE(Player.lastRow, channel);
|
|
effectNumber = Player.currentPattern.effectNumber[Player.lastRow][channel];
|
|
effectParameter = Player.currentPattern.effectParameter[Player.lastRow][channel];
|
|
effectParameterX = effectParameter >> 4;
|
|
effectParameterY = effectParameter & 0xF;
|
|
sampleOffset = 0;
|
|
|
|
if (sampleNumber) {
|
|
Player.lastSampleNumber[channel] = sampleNumber - 1;
|
|
if (!(effectParameter == 0xE && effectParameterX == NOTEDELAY))
|
|
Player.volume[channel] = Mod.samples[Player.lastSampleNumber[channel]].volume;
|
|
}
|
|
|
|
if (note != NONOTE) {
|
|
Player.lastNote[channel] = note;
|
|
Player.amigaPeriod[channel] = ReadAmigaPeriods(note + Mod.samples[Player.lastSampleNumber[channel]].fineTune);
|
|
|
|
if (effectNumber != TONEPORTAMENTO && effectNumber != PORTAMENTOVOLUMESLIDE)
|
|
Player.lastAmigaPeriod[channel] = Player.amigaPeriod[channel];
|
|
|
|
if (!(Player.waveControl[channel] & 0x80)) Player.vibratoPos[channel] = 0;
|
|
if (!(Player.waveControl[channel] & 0x08)) Player.tremoloPos[channel] = 0;
|
|
}
|
|
|
|
switch (effectNumber) {
|
|
case TONEPORTAMENTO:
|
|
if (effectParameter) Player.portamentoSpeed[channel] = effectParameter;
|
|
Player.portamentoNote[channel] = Player.amigaPeriod[channel];
|
|
note = NONOTE;
|
|
break;
|
|
|
|
case VIBRATO:
|
|
if (effectParameterX) Player.vibratoSpeed[channel] = effectParameterX;
|
|
if (effectParameterY) Player.vibratoDepth[channel] = effectParameterY;
|
|
break;
|
|
|
|
case PORTAMENTOVOLUMESLIDE:
|
|
Player.portamentoNote[channel] = Player.amigaPeriod[channel];
|
|
note = NONOTE;
|
|
break;
|
|
|
|
case TREMOLO:
|
|
if (effectParameterX) Player.tremoloSpeed[channel] = effectParameterX;
|
|
if (effectParameterY) Player.tremoloDepth[channel] = effectParameterY;
|
|
break;
|
|
|
|
case SETCHANNELPANNING:
|
|
Mixer.channelPanning[channel] = effectParameter >> 1;
|
|
break;
|
|
|
|
case SETSAMPLEOFFSET:
|
|
sampleOffset = effectParameter << 8;
|
|
if (sampleOffset > Mod.samples[Player.lastSampleNumber[channel]].length)
|
|
sampleOffset = Mod.samples[Player.lastSampleNumber[channel]].length;
|
|
break;
|
|
|
|
case JUMPTOORDER:
|
|
Player.orderIndex = effectParameter;
|
|
if (Player.orderIndex >= Mod.songLength)
|
|
Player.orderIndex = 0;
|
|
Player.row = 0;
|
|
jumpFlag = true;
|
|
break;
|
|
|
|
case SETVOLUME:
|
|
if (effectParameter > 64) Player.volume[channel] = 64;
|
|
else Player.volume[channel] = effectParameter;
|
|
break;
|
|
|
|
case BREAKPATTERNTOROW:
|
|
Player.row = effectParameterX * 10 + effectParameterY;
|
|
if (Player.row >= ROWS)
|
|
Player.row = 0;
|
|
if (!jumpFlag && !breakFlag) {
|
|
Player.orderIndex++;
|
|
if (Player.orderIndex >= Mod.songLength)
|
|
Player.orderIndex = 0;
|
|
}
|
|
breakFlag = true;
|
|
break;
|
|
|
|
case 0xE:
|
|
switch (effectParameterX) {
|
|
case FINEPORTAMENTOUP:
|
|
Player.lastAmigaPeriod[channel] -= effectParameterY;
|
|
break;
|
|
|
|
case FINEPORTAMENTODOWN:
|
|
Player.lastAmigaPeriod[channel] += effectParameterY;
|
|
break;
|
|
|
|
case SETVIBRATOWAVEFORM:
|
|
Player.waveControl[channel] &= 0xF0;
|
|
Player.waveControl[channel] |= effectParameterY;
|
|
break;
|
|
|
|
case SETFINETUNE:
|
|
Mod.samples[Player.lastSampleNumber[channel]].fineTune = effectParameterY;
|
|
if (Mod.samples[Player.lastSampleNumber[channel]].fineTune > 7)
|
|
Mod.samples[Player.lastSampleNumber[channel]].fineTune -= 16;
|
|
break;
|
|
|
|
case PATTERNLOOP:
|
|
if (effectParameterY) {
|
|
if (Player.patternLoopCount[channel])
|
|
Player.patternLoopCount[channel]--;
|
|
else
|
|
Player.patternLoopCount[channel] = effectParameterY;
|
|
if (Player.patternLoopCount[channel])
|
|
Player.row = Player.patternLoopRow[channel] - 1;
|
|
} else
|
|
Player.patternLoopRow[channel] = Player.row;
|
|
break;
|
|
|
|
case SETTREMOLOWAVEFORM:
|
|
Player.waveControl[channel] &= 0xF;
|
|
Player.waveControl[channel] |= effectParameterY << 4;
|
|
break;
|
|
|
|
case FINEVOLUMESLIDEUP:
|
|
Player.volume[channel] += effectParameterY;
|
|
if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
|
break;
|
|
|
|
case FINEVOLUMESLIDEDOWN:
|
|
Player.volume[channel] -= effectParameterY;
|
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
|
break;
|
|
|
|
case NOTECUT:
|
|
note = NONOTE;
|
|
break;
|
|
|
|
case PATTERNDELAY:
|
|
Player.patternDelay = effectParameterY;
|
|
break;
|
|
|
|
case INVERTLOOP:
|
|
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case SETSPEED:
|
|
if (effectParameter < 0x20)
|
|
Player.speed = effectParameter;
|
|
else
|
|
Player.samplesPerTick = sampleRate / (2 * effectParameter / 5);
|
|
break;
|
|
}
|
|
|
|
if (note != NONOTE || (Player.lastAmigaPeriod[channel] &&
|
|
effectNumber != VIBRATO && effectNumber != VIBRATOVOLUMESLIDE &&
|
|
!(effectNumber == 0xE && effectParameterX == NOTEDELAY)))
|
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
|
|
|
if (note != NONOTE)
|
|
Mixer.channelSampleOffset[channel] = sampleOffset << FIXED_DIVIDER;
|
|
|
|
if (sampleNumber)
|
|
Mixer.channelSampleNumber[channel] = Player.lastSampleNumber[channel];
|
|
|
|
if (effectNumber != TREMOLO)
|
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
|
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool AudioGeneratorMOD::ProcessTick()
|
|
{
|
|
uint8_t channel;
|
|
uint8_t sampleNumber;
|
|
uint16_t note;
|
|
uint8_t effectNumber;
|
|
uint8_t effectParameter;
|
|
uint8_t effectParameterX;
|
|
uint8_t effectParameterY;
|
|
uint16_t tempNote;
|
|
|
|
if (!running) return false;
|
|
|
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
|
|
|
if (Player.lastAmigaPeriod[channel]) {
|
|
|
|
sampleNumber = Player.currentPattern.sampleNumber[Player.lastRow][channel];
|
|
// note = Player.currentPattern.note[Player.lastRow][channel];
|
|
note = NOTE(Player.lastRow, channel);
|
|
effectNumber = Player.currentPattern.effectNumber[Player.lastRow][channel];
|
|
effectParameter = Player.currentPattern.effectParameter[Player.lastRow][channel];
|
|
effectParameterX = effectParameter >> 4;
|
|
effectParameterY = effectParameter & 0xF;
|
|
|
|
switch (effectNumber) {
|
|
case ARPEGGIO:
|
|
if (effectParameter)
|
|
switch (Player.tick % 3) {
|
|
case 0:
|
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
|
break;
|
|
case 1:
|
|
tempNote = Player.lastNote[channel] + effectParameterX * 8 + Mod.samples[Player.lastSampleNumber[channel]].fineTune;
|
|
if (tempNote < 296) Mixer.channelFrequency[channel] = Player.amiga / ReadAmigaPeriods(tempNote);
|
|
break;
|
|
case 2:
|
|
tempNote = Player.lastNote[channel] + effectParameterY * 8 + Mod.samples[Player.lastSampleNumber[channel]].fineTune;
|
|
if (tempNote < 296) Mixer.channelFrequency[channel] = Player.amiga / ReadAmigaPeriods(tempNote);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case PORTAMENTOUP:
|
|
Player.lastAmigaPeriod[channel] -= effectParameter;
|
|
if (Player.lastAmigaPeriod[channel] < 113) Player.lastAmigaPeriod[channel] = 113;
|
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
|
break;
|
|
|
|
case PORTAMENTODOWN:
|
|
Player.lastAmigaPeriod[channel] += effectParameter;
|
|
if (Player.lastAmigaPeriod[channel] > 856) Player.lastAmigaPeriod[channel] = 856;
|
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
|
break;
|
|
|
|
case TONEPORTAMENTO:
|
|
Portamento(channel);
|
|
break;
|
|
|
|
case VIBRATO:
|
|
Vibrato(channel);
|
|
break;
|
|
|
|
case PORTAMENTOVOLUMESLIDE:
|
|
Portamento(channel);
|
|
Player.volume[channel] += effectParameterX - effectParameterY;
|
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
|
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
|
break;
|
|
|
|
case VIBRATOVOLUMESLIDE:
|
|
Vibrato(channel);
|
|
Player.volume[channel] += effectParameterX - effectParameterY;
|
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
|
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
|
break;
|
|
|
|
case TREMOLO:
|
|
Tremolo(channel);
|
|
break;
|
|
|
|
case VOLUMESLIDE:
|
|
Player.volume[channel] += effectParameterX - effectParameterY;
|
|
if (Player.volume[channel] < 0) Player.volume[channel] = 0;
|
|
else if (Player.volume[channel] > 64) Player.volume[channel] = 64;
|
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
|
break;
|
|
|
|
case 0xE:
|
|
switch (effectParameterX) {
|
|
case RETRIGGERNOTE:
|
|
if (!effectParameterY) break;
|
|
if (!(Player.tick % effectParameterY)) {
|
|
Mixer.channelSampleOffset[channel] = 0;
|
|
}
|
|
break;
|
|
|
|
case NOTECUT:
|
|
if (Player.tick == effectParameterY)
|
|
Mixer.channelVolume[channel] = Player.volume[channel] = 0;
|
|
break;
|
|
|
|
case NOTEDELAY:
|
|
if (Player.tick == effectParameterY) {
|
|
if (sampleNumber) Player.volume[channel] = Mod.samples[Player.lastSampleNumber[channel]].volume;
|
|
if (note != NONOTE) Mixer.channelSampleOffset[channel] = 0;
|
|
Mixer.channelFrequency[channel] = Player.amiga / Player.lastAmigaPeriod[channel];
|
|
Mixer.channelVolume[channel] = Player.volume[channel];
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool AudioGeneratorMOD::RunPlayer()
|
|
{
|
|
if (!running) return false;
|
|
|
|
if (Player.tick == Player.speed) {
|
|
Player.tick = 0;
|
|
|
|
if (Player.row == ROWS) {
|
|
Player.orderIndex++;
|
|
if (Player.orderIndex == Mod.songLength)
|
|
{
|
|
//Player.orderIndex = 0;
|
|
// No loop, just say we're done!
|
|
return false;
|
|
}
|
|
Player.row = 0;
|
|
}
|
|
|
|
if (Player.patternDelay) {
|
|
Player.patternDelay--;
|
|
} else {
|
|
if (Player.orderIndex != Player.oldOrderIndex)
|
|
if (!LoadPattern(Mod.order[Player.orderIndex])) return false;
|
|
Player.oldOrderIndex = Player.orderIndex;
|
|
if (!ProcessRow()) return false;
|
|
}
|
|
|
|
} else {
|
|
if (!ProcessTick()) return false;
|
|
}
|
|
Player.tick++;
|
|
return true;
|
|
}
|
|
|
|
void AudioGeneratorMOD::GetSample(int16_t sample[2])
|
|
{
|
|
int16_t sumL;
|
|
int16_t sumR;
|
|
uint8_t channel;
|
|
uint32_t samplePointer;
|
|
int8_t current;
|
|
int8_t next;
|
|
int16_t out;
|
|
|
|
if (!running) return;
|
|
|
|
sumL = 0;
|
|
sumR = 0;
|
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
|
|
|
if (!Mixer.channelFrequency[channel] ||
|
|
!Mod.samples[Mixer.channelSampleNumber[channel]].length) continue;
|
|
|
|
Mixer.channelSampleOffset[channel] += Mixer.channelFrequency[channel];
|
|
|
|
if (!Mixer.channelVolume[channel]) continue;
|
|
|
|
samplePointer = Mixer.sampleBegin[Mixer.channelSampleNumber[channel]] +
|
|
(Mixer.channelSampleOffset[channel] >> FIXED_DIVIDER);
|
|
|
|
if (Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]]) {
|
|
|
|
if (samplePointer >= Mixer.sampleLoopEnd[Mixer.channelSampleNumber[channel]]) {
|
|
Mixer.channelSampleOffset[channel] -= Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]] << FIXED_DIVIDER;
|
|
samplePointer -= Mixer.sampleLoopLength[Mixer.channelSampleNumber[channel]];
|
|
}
|
|
|
|
} else {
|
|
|
|
if (samplePointer >= Mixer.sampleEnd[Mixer.channelSampleNumber[channel]]) {
|
|
Mixer.channelFrequency[channel] = 0;
|
|
samplePointer = Mixer.sampleEnd[Mixer.channelSampleNumber[channel]];
|
|
}
|
|
|
|
}
|
|
|
|
if (samplePointer < FatBuffer.samplePointer[channel] ||
|
|
samplePointer >= FatBuffer.samplePointer[channel] + fatBufferSize - 1 ||
|
|
Mixer.channelSampleNumber[channel] != FatBuffer.channelSampleNumber[channel]) {
|
|
|
|
uint16_t toRead = Mixer.sampleEnd[Mixer.channelSampleNumber[channel]] - samplePointer + 1;
|
|
if (toRead > fatBufferSize) toRead = fatBufferSize;
|
|
|
|
if (!file->seek(samplePointer, SEEK_SET)) {
|
|
stop();
|
|
return;
|
|
}
|
|
if (toRead != file->read(FatBuffer.channels[channel], toRead)) {
|
|
stop();
|
|
return;
|
|
}
|
|
|
|
FatBuffer.samplePointer[channel] = samplePointer;
|
|
FatBuffer.channelSampleNumber[channel] = Mixer.channelSampleNumber[channel];
|
|
}
|
|
|
|
current = FatBuffer.channels[channel][(samplePointer - FatBuffer.samplePointer[channel]) /*& (FATBUFFERSIZE - 1)*/];
|
|
next = FatBuffer.channels[channel][(samplePointer + 1 - FatBuffer.samplePointer[channel]) /*& (FATBUFFERSIZE - 1)*/];
|
|
|
|
out = current;
|
|
|
|
// Integer linear interpolation
|
|
out += (next - current) * (Mixer.channelSampleOffset[channel] & ((1 << FIXED_DIVIDER) - 1)) >> FIXED_DIVIDER;
|
|
|
|
// Upscale to BITDEPTH
|
|
out <<= BITDEPTH - 8;
|
|
|
|
// Channel volume
|
|
out = out * Mixer.channelVolume[channel] >> 6;
|
|
|
|
// Channel panning
|
|
sumL += out * min(128 - Mixer.channelPanning[channel], 64) >> 6;
|
|
sumR += out * min(Mixer.channelPanning[channel], 64) >> 6;
|
|
}
|
|
|
|
// Downscale to BITDEPTH
|
|
sumL /= Mod.numberOfChannels;
|
|
sumR /= Mod.numberOfChannels;
|
|
|
|
// Fill the sound buffer with unsigned values
|
|
sample[AudioOutput::LEFTCHANNEL] = sumL + (1 << (BITDEPTH - 1));
|
|
sample[AudioOutput::RIGHTCHANNEL] = sumR + (1 << (BITDEPTH - 1));
|
|
}
|
|
|
|
bool AudioGeneratorMOD::LoadMOD()
|
|
{
|
|
uint8_t channel;
|
|
|
|
if (!LoadHeader()) return false;
|
|
LoadSamples();
|
|
|
|
Player.amiga = AMIGA;
|
|
Player.samplesPerTick = sampleRate / (2 * 125 / 5); // Hz = 2 * BPM / 5
|
|
Player.speed = 6;
|
|
Player.tick = Player.speed;
|
|
Player.row = 0;
|
|
|
|
Player.orderIndex = 0;
|
|
Player.oldOrderIndex = 0xFF;
|
|
Player.patternDelay = 0;
|
|
|
|
for (channel = 0; channel < Mod.numberOfChannels; channel++) {
|
|
Player.patternLoopCount[channel] = 0;
|
|
Player.patternLoopRow[channel] = 0;
|
|
|
|
Player.lastAmigaPeriod[channel] = 0;
|
|
|
|
Player.waveControl[channel] = 0;
|
|
|
|
Player.vibratoSpeed[channel] = 0;
|
|
Player.vibratoDepth[channel] = 0;
|
|
Player.vibratoPos[channel] = 0;
|
|
|
|
Player.tremoloSpeed[channel] = 0;
|
|
Player.tremoloDepth[channel] = 0;
|
|
Player.tremoloPos[channel] = 0;
|
|
|
|
FatBuffer.samplePointer[channel] = 0;
|
|
FatBuffer.channelSampleNumber[channel] = 0xFF;
|
|
|
|
Mixer.channelSampleOffset[channel] = 0;
|
|
Mixer.channelFrequency[channel] = 0;
|
|
Mixer.channelVolume[channel] = 0;
|
|
switch (channel % 4) {
|
|
case 0:
|
|
case 3:
|
|
Mixer.channelPanning[channel] = stereoSeparation;
|
|
break;
|
|
default:
|
|
Mixer.channelPanning[channel] = 128 - stereoSeparation;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|