arduino2/libraries/XT_DAC_Audio/XT_DAC_Audio.cpp
2020-11-06 13:17:55 +01:00

1295 lines
40 KiB
C++

// XTronical DAC Audio Library, currently supporting ESP32
// May work with other processors and/or DAC's with or without modifications
// (c) XTronical 2018, Licensed under GNU GPL 3.0 and later, under this license absolutely no warranty given
// See www.xtronical.com for documentation
// This software is currently under active development (Jan 2018) and may change and break your code with new versions
// No responsibility is taken for this. Stick with the version that works for you, if you need newer commands from later versions
// you may have to alter your original code
#include "esp32-hal-timer.h"
#include "XT_DAC_Audio.h"
#include "HardwareSerial.h"
/* Every variable that is used in the mainline code and in the onTImer interrupt code
* must be declared volatile.
* Also, variables used by onTimer must be regular variables and cannot be
* the CLASSES,because it is an interrupt routine and accessing variables
* belonging to objects was causing crashing in the ISR
* To avoid re-entrancy problems, the ISR onTimer() must change only the "play" index and the
* mainline code must change only the "fill" index. Otherwise we can get corruption if
* onTimer() does a pull while FillBuffer() is putting data in.
* Each can _look_ at the other's variables, but both cannot _change_ the same variable.
*/
volatile int32_t NextPlayPos=0; // position in buffer of next byte to play
volatile uint8_t *Buffer; // The buffer to store the data that will be sent to the
volatile uint16_t BufferSize; // Actual size if buffer used (default can be overridden in function call)
volatile uint8_t DacPin; // pin to send DAC data to, presumably one of the DAC pins!
uint8_t LastDacValue; // Last value sent to DAC, if next is same we don't write to DAC again
XT_PlayListItem_Class *FirstPlayListItem=0; // first play list item to play in linked list
XT_PlayListItem_Class *LastPlayListItem=0; // last play list item to play in linked list
XT_Instrument_Class DEFAULT_INSTRUMENT;
int BufferUsed = 0; // NB : Not working yet. Amount of bytes sent out to play since last buffer fill, this value
// can help you determine an optimum buffer size if you don't want to
// stick with the default set in BUFFER_SIZE_DEFAULT
// interrupt stuff
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// The FNOTE "defines" below contain actual frequencies for notes which range from a few Hz (around 30) to around 4000Hz
// But in essence there are only 89 different notes , we collect them into an array so that we can store a note as
// a number from 0 to 90, (0 being no 0Hz, or silence). In this way we can store musical notes in single bytes.
// To benefit from this the music data in your code should be greater than 90 bytes otherwise your technically using more
// memory than you would if you used the raw frequencies (which use 2 bytes per note). It is estimated in simple projects you
// would not gain but generally a simple project would have more spare memory anyway. For larger projects where memory could
// be more of an issue you will actually save memory, so hopefully a win!
uint16_t XT_Notes[90]={1,FNOTE_B0,FNOTE_C1,FNOTE_CS1,FNOTE_D1,FNOTE_DS1,FNOTE_E1,FNOTE_F1,FNOTE_FS1,FNOTE_G1,FNOTE_GS1,FNOTE_A1,FNOTE_AS1,FNOTE_B1,FNOTE_C2,FNOTE_CS2,FNOTE_D2,FNOTE_DS2,FNOTE_E2,FNOTE_F2,FNOTE_FS2,FNOTE_G2,FNOTE_GS2,FNOTE_A2,FNOTE_AS2,FNOTE_B2,FNOTE_C3,FNOTE_CS3,FNOTE_D3,FNOTE_DS3,FNOTE_E3,FNOTE_F3,FNOTE_FS3,FNOTE_G3,FNOTE_GS3,FNOTE_A3,FNOTE_AS3,FNOTE_B3,FNOTE_C4,FNOTE_CS4,FNOTE_D4,FNOTE_DS4,FNOTE_E4,FNOTE_F4,FNOTE_FS4,FNOTE_G4,FNOTE_GS4,FNOTE_A4,FNOTE_AS4,FNOTE_B4,FNOTE_C5,FNOTE_CS5,FNOTE_D5,FNOTE_DS5,FNOTE_E5,FNOTE_F5,FNOTE_FS5,FNOTE_G5,FNOTE_GS5,FNOTE_A5,FNOTE_AS5,FNOTE_B5,FNOTE_C6,FNOTE_CS6,FNOTE_D6,FNOTE_DS6,FNOTE_E6,FNOTE_F6,FNOTE_FS6,FNOTE_G6,FNOTE_GS6,FNOTE_A6,FNOTE_AS6,FNOTE_B6,FNOTE_C7,FNOTE_CS7,FNOTE_D7,FNOTE_DS7,FNOTE_E7,FNOTE_F7,FNOTE_FS7,FNOTE_G7,FNOTE_GS7,FNOTE_A7,FNOTE_AS7,FNOTE_B7,FNOTE_C8,FNOTE_CS8,FNOTE_D8,FNOTE_DS8
};
// Precalculate sine values in an 8 bit range (i.e. usually 0 to 360Deg), we want a new measure of a Degree that
// defines a circle as 256 parts. We'll call these 8Bit Degrees!
// As we're using an 8 bit value for the DAC 0-255 (256 parts) that means for us
// there are 256 'bits' to a complete circle not 360 (as in degrees)
int SineValues[256]; // an array to store our values for sine
void InitSineValues()
{
float ConversionFactor=(2*3.142)/256; // convert my 0-255 bits in a circle to radians
// there are 2 x PI radians in a circle hence the 2*PI
// Then divide by 256 to get the value in radians
// for one of my 0-255 bits.
float RadAngle; // Angle in Radians, have to have this as computers love radians!
// calculate sine values
for(int MyAngle=0;MyAngle<256;MyAngle++) {
RadAngle=MyAngle*ConversionFactor; // 8 bit angle converted to radians
SineValues[MyAngle]=(sin(RadAngle)*127)+128; // get the sine of this angle and 'shift' up so
// there are no negative values in the data
// as the DAC does not understand them and would
// convert to positive values.
}
}
uint8_t SetVolume(uint8_t Value,uint8_t Volume)
{
// returns the sound byte value adjusted for the volume passed, 0 min, 127 max
// a value of 127 will not boost the sound higher than the original, it will
// mean the value returned is the original, and any other value below 127 will
// be a proportional fraction of the original
// remember that the mid point ( no sound, speaker at rest) is 7f not 0
uint8_t AdjustedValue;
if(Volume>127) // Max is 127
Volume=127;
if(Value==0x7f)
return Value; // a speaker at mid point, cannot change the volume of that, it is effectively 0
if(Value>0x7f)
{
// +ve part of wave above 0, runs 1 to 128
// value will actually be in range 128 to 255,
AdjustedValue=Value-127;
// Now adjust the volume be the ratio passed in
AdjustedValue=AdjustedValue*(float(Volume)/127);
// finally put back in range 1 to 128
return AdjustedValue+127;
}
// if we get this far then wave below 0, range 127 to 0 (127 being mid of wave, 0 nearest bottom(trough))
// just flip the value so it's in range 1 to 127 with 1 being the low point
AdjustedValue=0x7f-Value;
// Then add in volume adjustment
AdjustedValue=AdjustedValue*(float(Volume)/127);
// adjust back to correct range before returning
return 0x7f-AdjustedValue;
}
// The main interrupt routine called "BytesPerSec" times per second (132300 BPS)
void IRAM_ATTR onTimer()
{
// Sound playing code, plays whatever's in the buffer continuously. Big change from previous versions
if(LastDacValue!=Buffer[NextPlayPos]) // Send value to DAC only of changed since last value else no need
{
// value to DAC has changed, send to actual hardware, else we just leave setting as is as it's not changed
LastDacValue=Buffer[NextPlayPos];
dacWrite(DacPin,LastDacValue); // write out the data
}
Buffer[NextPlayPos]=0; // Reset this buffer byte back to silence
NextPlayPos++; // Move play pos to next byte in buffer
if(NextPlayPos==BufferSize) // If gone past end of buffer,
NextPlayPos=0; // set back to beginning
}
int XT_DAC_Audio_Class::BufferUsage()
{
return BufferUsed;
}
void XT_DAC_Audio_Class::FillBuffer()
{
// Fill the buffer, another routine that has changed almost completely since versions before 3.2
// This is because of the requirement to mix sounds together so we can emit more than one at once
// For each play item mix its next byte into the correct place in the buffer. Remember on a large
// buffer the playing pos may be some distance from where the last sound is filling. Filling at
// a position far ahead of the play pos would give an inaccurate and unknown mix effect
// Note mixing always occurs even if just 1 sound being played as it will be effectively
// mixed with the silence in the buffer.
// To mix waves you add the waves together.... That is presuming they are "correct" waves
// that have both positive and negative peaks and troughs. However these sounds produce
// as 8bit unsigned value, i.e. everything above 0,
// so therefore there is no negative, we need to adjust them back to having
// correct positive and negative peaks and troughs first before doing the "mixing",
// and then convert back to a wave with values 0 - 255.
XT_PlayListItem_Class *PlayItem,*NextPlayItem;
int16_t ByteToPlay,BufferByteToMixWith; // Need 16 bit signed so we can have a range of
// -255 to +256
uint8_t NextByte,BufferByte; // raw unsigned values of naext byte to plat and mix with
uint32_t avail; // # of bytes that we can put into the buffer.
bool LogAvail=true;
static uint16_t LastPlayPos=0;
PlayItem=FirstPlayListItem;
while(PlayItem!=0)
{
if(PlayItem->NewSound) // A new unplayed sound, set initial fill buffer position
{
PlayItem->NextFillPos=NextPlayPos+1; // to one in front of actual playpos
PlayItem->NewSound=false; // No longer a new sound now.
LogAvail=false;
}
avail = BufferSize- (PlayItem->NextFillPos - NextPlayPos); // Work out how much buffer we can fill
if (avail >=4000) // This indicates the next fill pos was behind play pos,
avail -= BufferSize; // bring into correct range
if(LogAvail) // calc how much of the play buffer was used since the
{ // Last call to FillBuffer
BufferUsed=NextPlayPos-LastPlayPos;
if(BufferUsed<0)
BufferUsed=BufferUsed+BufferSize;
}
while((PlayItem->Playing)&(--avail > 0)) // while space in buffer and item still active
{
NextByte=PlayItem->NextByte(); // Get next byte from this sound to play
if(PlayItem->Filter!=0)
NextByte=PlayItem->Filter->FilterWave(NextByte); // Adjust the byte by any set filter, a Work in Progress
ByteToPlay=(NextByte-127); // -127 to make it's a wave that swings above and below 0
BufferByteToMixWith=Buffer[PlayItem->NextFillPos]-127; // -127 to make it's a wave that swings above and below 0
ByteToPlay=BufferByteToMixWith+ByteToPlay; // Mix them together and store back in ByteToPlay
if(ByteToPlay>128)
ByteToPlay=128; // Sound has topped out, adjust to max top out
if(ByteToPlay<-127)
ByteToPlay=-127; // Sound has bottomed out, adjust to min bottom out
Buffer[PlayItem->NextFillPos]=uint8_t(ByteToPlay+127); // Store back in buffer adjusting so back in 0-255 range
// Move the fill position for this sound item to next pos
PlayItem->NextFillPos++; // move to next buffer position
if(PlayItem->NextFillPos==BufferSize) // Set to start of buffer if end reached
PlayItem->NextFillPos=0;
}
NextPlayItem=PlayItem->NextItem; // move to next play item
if(PlayItem->Playing==false) // If this play item completed
{
if(PlayItem->RepeatForever)
{
PlayItem->Init(); // initialise for playing again
PlayItem->Playing=true;
}
else
{
if(PlayItem->RepeatIdx>0) // Repeat sound X amount of times
{
PlayItem->RepeatIdx--;
PlayItem->Init(); // initialise for playing again
PlayItem->Playing=true;
}
else
RemoveFromPlayList(PlayItem); // no repeats, remove from play list
}
}
PlayItem=NextPlayItem; // Set to next item
}
}
XT_PlayListItem_Class::XT_PlayListItem_Class()
{
RepeatForever=false;
Repeat=0;
RepeatIdx=0;
}
// Despite being marked virtual and being overridden in desendents without these I get vtable error
// issues which I really don't understand, sl I put these in even though never called and all is well
uint8_t XT_PlayListItem_Class::NextByte()
{
return 0;
}
void XT_PlayListItem_Class::Init()
{
}
XT_DAC_Audio_Class::XT_DAC_Audio_Class(uint8_t TheDacPin, uint8_t TimerNo):XT_DAC_Audio_Class(TheDacPin,TimerNo,BUFFER_SIZE_DEFAULT)
{
// calls other constructor as shown in function definition
}
XT_DAC_Audio_Class::XT_DAC_Audio_Class(uint8_t TheDacPin, uint8_t TimerNo,uint16_t PassedBufferSize)
{
// Using a prescaler of 80 gives a counting frequency of 1,000,000 (1MHz) and using
// and calling the function every 20 counts of the frequency (the 20 below) means
// that we will call our onTimer function 50,000 times a second
// reserve some memory for the buffer
Buffer=new uint8_t[PassedBufferSize];
BufferSize=PassedBufferSize; // Buffer Size is global var used in other routines that need to know size
for(uint32_t i=0;i<PassedBufferSize;i++) // Clear all bytes in buffer to 0 (silence)
Buffer[i]=0;
FirstPlayListItem=0;
DacPin=TheDacPin; // set dac pin to use
LastDacValue=0; // set to mid point
dacWrite(DacPin,LastDacValue); // Set speaker to mid point, stops click at start of first sound
InitSineValues(); // create our table of sine values for our special circular
// angles of 0 - 255 "DAC Degrees". Speeds things up to if
// you pre-calculate
// Set up interrupt routine
timer = timerBegin(TimerNo, 80, true); // use timer TimerNo, pre-scaler is 80 (divide by 8000), count up
timerAttachInterrupt(timer, &onTimer, true); // P3= edge triggered
timerAlarmWrite(timer, 20, true); // will trigger 250,000 times per second,
timerAlarmEnable(timer); // enable
delay(1); // Allow system to settle, otherwise garbage can play for first second
}
void XT_DAC_Audio_Class::PrintPlayList()
{
// For debugging purposes, lists addresses of all items in playlist
Serial.println("Current play list");
XT_PlayListItem_Class *PlayItem;
PlayItem=FirstPlayListItem;
while(PlayItem!=0)
{
Serial.print(PlayItem->Name);
Serial.print(" @ ");
Serial.print((unsigned long)PlayItem, HEX);
PlayItem=PlayItem->NextItem;
Serial.print("->");
}
Serial.println();
}
void XT_DAC_Audio_Class::RemoveFromPlayList(XT_PlayListItem_Class *ItemToRemove)
{
// removes a play item from the play list
if(ItemToRemove->PreviousItem!=0)
ItemToRemove->PreviousItem->NextItem=ItemToRemove->NextItem;
else
FirstPlayListItem=ItemToRemove->NextItem;
if(ItemToRemove->NextItem!=0)
ItemToRemove->NextItem->PreviousItem=ItemToRemove->PreviousItem;
else
LastPlayListItem=ItemToRemove->PreviousItem;
ItemToRemove->PreviousItem=0;
ItemToRemove->NextItem=0;
}
void XT_DAC_Audio_Class::Play(XT_PlayListItem_Class *Sound)
{
Play(Sound,true); // Sounds mix by default
}
void XT_DAC_Audio_Class::Play(XT_PlayListItem_Class *Sound,bool Mix)
{
// check if this sound is already playing, if so it is removed first and will be re-played
// This limitation will be removed in later version so that multiple versions of the same
// sound can be played at once. Trying to do that now will corrupt the list of items to play
if(AlreadyPlaying(Sound))
RemoveFromPlayList(Sound);
if(Mix==false) // stop all currently playing sounds and just have this one
StopAllSounds();
Sound->NewSound=true; // Flags to fill buffer routine that this is brand new sound
// with nothing yet put into buffer for playing
Sound->RepeatIdx=Sound->Repeat; // Initialise any repeats
// set up this sound to play, different types of sound may initialise differently
Sound->Init();
// add to list of currently playing items
// create a new play list entry
if(FirstPlayListItem==0) // no items to play in list yet
{
FirstPlayListItem=Sound;
LastPlayListItem=Sound;
}
else{
// add to end of list
LastPlayListItem->NextItem=Sound;
Sound->PreviousItem=LastPlayListItem;
LastPlayListItem=Sound;
}
Sound->Playing=true; // Will start it playing
}
bool XT_DAC_Audio_Class::AlreadyPlaying(XT_PlayListItem_Class *Item)
{
// returns true if sound already in list of items to play else false
XT_PlayListItem_Class* PlayItem;
PlayItem=FirstPlayListItem;
while(PlayItem!=0)
{
if(PlayItem==Item)
return true;
PlayItem=PlayItem->NextItem;
}
return false;
}
void XT_DAC_Audio_Class::StopAllSounds()
{
// stop all sounds and clear the play list
XT_PlayListItem_Class* PlayItem;
PlayItem=FirstPlayListItem;
while(PlayItem!=0)
{
PlayItem->Playing=false;
RemoveFromPlayList(PlayItem);
PlayItem=FirstPlayListItem;
}
FirstPlayListItem=0;
}
// Wav Class functions
#define DATA_CHUNK_ID 0x61746164
#define FMT_CHUNK_ID 0x20746d66
// Convert 4 byte little-endian to a long.
#define longword(bfr, ofs) (bfr[ofs+3] << 24 | bfr[ofs+2] << 16 |bfr[ofs+1] << 8 |bfr[ofs+0])
XT_Wav_Class::XT_Wav_Class(const unsigned char *WavData)
{
// create a new wav class object
unsigned long ofs, siz;
/* Process the chunks. "fmt " is format, "data" is the samples, ignore all else. */
ofs = 12;
siz = longword(WavData, 4);
SampleRate = DataStart = 0;
while (ofs < siz) {
if (longword(WavData, ofs) == DATA_CHUNK_ID) {
DataSize = longword(WavData, ofs+4);
DataIdx = DataStart = ofs +8;
}
if (longword(WavData, ofs) == FMT_CHUNK_ID) {
SampleRate = longword(WavData, ofs+12);
}
ofs += longword(WavData, ofs+4) + 8;
}
IncreaseBy=float(SampleRate)/BytesPerSec;
PlayingTime = 1000. * DataSize / SampleRate;
Data=WavData;
Speed=1.0;
}
void XT_Wav_Class::Init()
{
LastIntCount=0;
DataIdx=DataStart;
Count=0;
SpeedUpCount=0;
TimeElapsed = 0;
TimeLeft = PlayingTime;
}
uint8_t XT_Wav_Class::NextByte()
{
// Returns the next byte to be played, note that this routine will return values suitable to
// be played back at 50,000Hz. Even if this sample is at a lesser rate than that it will be
// padded out as required so that it will appear to have a 50Khz sample rate
uint32_t IntPartOfCount;
uint8_t ReturnValue;
float ActualIncreaseBy;
// increase the counter, if it goes to a new integer digit then write to DAC
ActualIncreaseBy=IncreaseBy; // default if not playing slower than normal
if(Speed<=1) // manipulate IncreaseBy
ActualIncreaseBy=IncreaseBy*Speed;
Count+=ActualIncreaseBy;
IntPartOfCount=floor(Count);
ReturnValue=Data[DataIdx]; // by default we return previous value;
if(IntPartOfCount>LastIntCount)
{
if(Speed>1)
{
// for speeding up we need to basically go through the data quicker as upping the frequency
// that this routine is called could put too much strain on the CPU.
// First we subtract 1 from IncreaseBy as code below will handling the basic increment through
// the data.
// we now get the integer and decimal parts of this number and move the DataIdx on by "int" amount first
double IntPartAsFloat,DecimalPart,TempSpeed;
TempSpeed=Speed-1;
DecimalPart=modf(TempSpeed,&IntPartAsFloat);
DataIdx+=int(IntPartAsFloat); // always increase by the integer part
SpeedUpCount+=DecimalPart;
// If SpeedUpCount >1 then add this extra "1" to the DataIdx too and subtract 1 from SpeedUpCount
// This allows us "apparently" increment the DataIdx by a decimal amount
if(SpeedUpCount>1)
{
DataIdx++; // move another pos into data
SpeedUpCount--; // Take it off SpeedUpCount
}
}
// gone to a new integer of count, we need to send a new value to the DAC next time
// update the DataIDx counter
LastIntCount=IntPartOfCount;
DataIdx++;
TimeElapsed = 1000 * DataIdx / SampleRate;
TimeLeft = PlayingTime - TimeElapsed;
if(DataIdx>=DataSize) // end of data, flag end
{
Count=0; // reset frequency counter
DataIdx=DataStart; // reset data pointer back to beginning of WAV data
Playing=false; // mark as completed
TimeLeft = 0;
}
}
return ReturnValue;
}
// Instrument class routines
XT_Instrument_Class::XT_Instrument_Class():XT_Instrument_Class(INSTRUMENT_PIANO,127)
{
// See main constructor routine for description of passed parameters
// and defaults
}
XT_Instrument_Class::XT_Instrument_Class(int16_t InstrumentID):XT_Instrument_Class(InstrumentID,127)
{
// See main constructor routine for description of passed parameters
// and defaults
}
XT_Instrument_Class::XT_Instrument_Class(int16_t InstrumentID, uint8_t Volume)
{
// Volume : Volume of sound 0- 127 (max)
this->Note=abs(NOTE_C4); // default note
this->SoundDuration=1000; // default note length, ignored if using envelopes
this->Duration=1000; // default length of entire play action (i.e. after any decay) ignored if using envelopes
this->Volume=Volume;
SetInstrument(InstrumentID); // The default
}
void XT_Instrument_Class::SetNote(int8_t Note)
{
this->Note=abs(Note);
}
void XT_Instrument_Class::SetInstrument(uint16_t Instrument)
{
// To create your new "permanent" instrument to the code, add a line here that calls its
// set up routine and add this as a private function to the header file for the
// Instrument class. Also add a suitable # define to the Instruments.h header file
// in the instruments section.
// Any envelopes and waveforms will have their memory cleared here, no need to worry!
delete(FirstEnvelope); // remove old envelope definition
FirstEnvelope=0;
switch (Instrument)
{
case(INSTRUMENT_NONE) : SetDefaultInstrument();break;
case(INSTRUMENT_PIANO) : SetPianoInstrument();break;
case(INSTRUMENT_HARPSICHORD) : SetHarpsichordInstrument();break;
case(INSTRUMENT_ORGAN) : SetOrganInstrument();break;
case(INSTRUMENT_SAXOPHONE) : SetSaxophoneInstrument();break;
default: // compilation error, default to just square wave
SetDefaultInstrument();break;
}
}
void XT_Instrument_Class::SetDefaultInstrument()
{
SetWaveForm(WAVE_SQUARE);
}
void XT_Instrument_Class::SetPianoInstrument()
{
SetWaveForm(WAVE_TRIANGLE);
FirstEnvelope=new XT_Envelope_Class();
FirstEnvelope->AddPart(25,127);
FirstEnvelope->AddPart(20,20);
FirstEnvelope->AddPart(15,75);
FirstEnvelope->AddPart(10,15);
FirstEnvelope->AddPart(5,50);
FirstEnvelope->AddPart(300,0);
}
void XT_Instrument_Class::SetHarpsichordInstrument()
{
SetWaveForm(WAVE_SAWTOOTH);
FirstEnvelope=new XT_Envelope_Class();
FirstEnvelope->AddPart(15,127);
FirstEnvelope->AddPart(80,100);
FirstEnvelope->AddPart(300,0);
}
void XT_Instrument_Class::SetOrganInstrument()
{
SetWaveForm(WAVE_SINE);
FirstEnvelope=new XT_Envelope_Class();
FirstEnvelope->AddPart(15,127);
FirstEnvelope->AddPart(3000,0); // An organ maintains until key released
}
void XT_Instrument_Class::SetSaxophoneInstrument()
{
SetWaveForm(WAVE_SQUARE);
FirstEnvelope=new XT_Envelope_Class();
FirstEnvelope->AddPart(15,127);
FirstEnvelope->AddPart(3000,0); // An organ maintains until key released
}
void XT_Instrument_Class::SetWaveForm(uint8_t WaveFormType)
{
// Sets the wave form for this instrument
delete (WaveForm); // free any previous memory for a previous waveform
switch (WaveFormType)
{
case WAVE_SQUARE : WaveForm=new XT_SquareWave_Class();break;
case WAVE_TRIANGLE : WaveForm=new XT_TriangleWave_Class();break;
case WAVE_SAWTOOTH : WaveForm=new XT_SawToothWave_Class();break;
case WAVE_SINE : WaveForm=new XT_SineWave_Class();break;
default: // compilation error, default to square
WaveForm=new XT_SquareWave_Class();break;
}
}
void XT_Instrument_Class::SetFrequency(uint16_t Freq)
{
// only if not using note property
Note=-1;
WaveForm->Frequency=Freq;
WaveForm->Init(-1);
}
void XT_Instrument_Class::Init()
{
CurrentByte=0;
WaveForm->Init(Note);
// Note : These next two lines calculating the counters for the note duration are not used
// if there are envelopes attached to this instrument, the timings of the envelopes taken
// priority
SoundDurationCounter=SoundDuration*50;
DurationCounter=Duration*50; // converts to how many samples to return before stopping
if(FirstEnvelope!=0)
{
CurrentEnvelope=FirstEnvelope;
CurrentEnvelope->Init();
}
}
uint8_t XT_Instrument_Class::NextByte()
{
uint8_t ByteToPlay;
// If no envelope then use normal duration settings
if(CurrentEnvelope==0)
{
if(DurationCounter==0) // If completed mark as so and return no sound
{
Playing=false;
return 0;
}
DurationCounter--;
if(SoundDurationCounter==0) // If sound completed return no sound
return 0;
SoundDurationCounter--;
}
if(WaveForm->Frequency==0) // If no frequency then no sound
return 0;
// This next bit handles basic wave form if a sound is being produced
ByteToPlay=WaveForm->NextByte();
// Next add in envelope control if there is one
if(CurrentEnvelope!=0)
{
ByteToPlay=CurrentEnvelope->NextByte(ByteToPlay);
if(CurrentEnvelope->EnvelopeCompleted)
{
// check if another envelope in chain
if(CurrentEnvelope->NextEnvelope!=0)
{
CurrentEnvelope=CurrentEnvelope->NextEnvelope;
CurrentEnvelope->Init();
}
else
{
Playing=false; // All envelopes played
}
}
}
// adjust for current volume of this sound
return SetVolume(ByteToPlay,Volume);
}
XT_Envelope_Class *XT_Instrument_Class::AddEnvelope()
{
// Adds am envelope to this instrument, you will populate the envelope with it's parts
// using the Envelope class AddPart function
XT_Envelope_Class* Env=new XT_Envelope_Class();
XT_Envelope_Class* ThisEnv,PreviousEnv;
if(FirstEnvelope==0)
{
// no envelopes at all yet, set to first and return
FirstEnvelope=Env;
return Env;
}
// find last envelope and add another to the chain
ThisEnv=FirstEnvelope;
while(ThisEnv->NextEnvelope!=0)
ThisEnv=ThisEnv->NextEnvelope;
ThisEnv->NextEnvelope=Env;
return Env;
}
void XT_Instrument_Class::SetDuration(uint32_t Length) // in millis
{
// If you are using a default instrument then this routine need not be set
// will set duration of sound and duration that it plays to same value, i.e. when
// sound physical sound ends the note is deemed finished
SoundDuration=Length;
Duration=Length;
}
//Music score class
XT_MusicScore_Class::XT_MusicScore_Class(int8_t* Score):XT_MusicScore_Class(Score,TEMPO_ALLEGRO,&DEFAULT_INSTRUMENT)
{
// set tempo and instrument to default, number of plays to 1 (once)
}
XT_MusicScore_Class::XT_MusicScore_Class(int8_t* Score,uint16_t Tempo):XT_MusicScore_Class(Score,Tempo,&DEFAULT_INSTRUMENT)
{
// set instrument to default and plays to 1
}
XT_MusicScore_Class::XT_MusicScore_Class(int8_t* Score,uint16_t Tempo,XT_Instrument_Class* Instrument)
{
// Create music score
this->Score=Score;
this->Tempo=Tempo;
this->Instrument=Instrument;
}
XT_MusicScore_Class::XT_MusicScore_Class(int8_t* Score,uint16_t Tempo,uint16_t InstrumentID)
{
// Create music score
this->Score=Score;
this->Tempo=Tempo;
this->Instrument=&DEFAULT_INSTRUMENT;
Instrument->SetInstrument(InstrumentID);
}
void XT_MusicScore_Class::Init()
{
// Called before as score starts playing
Instrument->Init(); // Initialise instrument
// convert the tempo in BPM into a value that counts down in 50,000's of a second as this is the sample
// rate sent to the DAC
ChangeNoteEvery=(60/float(Tempo))*BytesPerSec;
ChangeNoteCounter=0; // ensures starts by playing first note with no delay
ScoreIdx=0; // set position to first no
}
uint8_t XT_MusicScore_Class::NextByte()
{
// returns next byte for the DAC
// are we ready to play another note?
if(ChangeNoteCounter==0) {
// Yes, new note
if(Score[ScoreIdx]==SCORE_END) // end of data
{
Playing=false;
return 0; // return silence
}
Instrument->Note=abs(Score[ScoreIdx]); // convert the negative value to positive index.
ScoreIdx++; // move to next note
// set length of play for instrument
// Check next data value to see if it is a beat value
if(Score[ScoreIdx]>0) // positive value, therefore not default beat length
{
// Set the duration, beat value of 1=0.25 beat, 2 0.5 beat etc. So just divide by 4
// to get the real beat length,
Instrument->Duration=(ChangeNoteEvery*(float(Score[ScoreIdx])/4));
// Then times by 0.8 to allow for natural movement
// of players finger on the instrument.
Instrument->SoundDuration=Instrument->Duration*0.8;
ChangeNoteCounter=Instrument->Duration; // set back to start of count ready for next note
ScoreIdx++; // point to next note, ready for next time
}
else
{
// default single beat values
Instrument->Duration=ChangeNoteEvery;
Instrument->SoundDuration=Instrument->Duration*0.8; // By default a note plays for 80% of tempo
// this allows for the natural movement of the
// player performing the next note
ChangeNoteCounter=ChangeNoteEvery; // set back to start of count ready for next note
}
// Note that we do not call DacAudio.Play() here as it's already been done for this music score
// and it's this music score that effectively controls the note playing, however we still need
// to initialise the instrument for each new note played
Instrument->Init();
}
ChangeNoteCounter--;
// return the next byte for this instrument
return Instrument->NextByte();
}
void XT_MusicScore_Class::SetInstrument(uint16_t InstrumentID)
{
Instrument->SetInstrument(InstrumentID);
}
// Envelope class, see www.xtronical.com for description of how to use envelopes with
// the instrument class
XT_Envelope_Class::XT_Envelope_Class()
{
// nothing yet
}
XT_Envelope_Class::~XT_Envelope_Class()
{
// delete any further linked envelopes and associated envelope parts for all envelopes found
// Note if you delete from an envelope that it is not the start envelope then the previous
// envelopes will still exist and in fact the last one before the one you delete will still
// point to this one. If for some reason you wish to delete a none first envelope then you
// must tidy up the last of the previous envelopes yourself.
XT_EnvelopePart_Class* Part,*NextPart;
if(NextEnvelope!=0)
delete(NextEnvelope); // if another envelope in the chain delete this also
// delete the parts for this envelope
Part=FirstPart;
while(Part!=0)
{
NextPart=Part->NextPart;
delete(Part);
Part=NextPart;
}
}
void XT_Envelope_Class::Init()
{
// Reset envelope ready for next note to play
CurrentEnvelopePart=0;
EnvelopeCompleted=false;
RepeatCounter=Repeats;
DurationCounter=1;
}
uint8_t XT_Envelope_Class::NextByte(uint8_t ByteToPlay)
{
// are their any parts, if not do nothing
if(FirstPart!=0)
{
// OK we have some envelope parts, manipulate the wave form volume according to the
// current envelope part in use for this instrument
if(EnvelopeCompleted==false)
{
if(CurrentEnvelopePart==0) // start of envelope
{
CurrentEnvelopePart=FirstPart;
InitEnvelopePart(CurrentEnvelopePart,CurrentVolume);
}
if(CurrentEnvelopePart->Completed)
{
CurrentEnvelopePart=CurrentEnvelopePart->NextPart;
if(CurrentEnvelopePart==0) // no more parts, do we need to repeat the envelope
{
if(RepeatCounter>0)
RepeatCounter--;
else
EnvelopeCompleted=true;
}
else
InitEnvelopePart(CurrentEnvelopePart,CurrentVolume);
}
if(CurrentEnvelopePart!=0)
{
CurrentVolume+=VolumeIncrement;
ByteToPlay=SetVolume(ByteToPlay,int(CurrentVolume));
DurationCounter--;
if(DurationCounter==0)
CurrentEnvelopePart->Completed=true;
}
}
}
return ByteToPlay;
}
void XT_Envelope_Class::InitEnvelopePart(XT_EnvelopePart_Class* EPart,uint8_t LastVolume)
{
// initialises the properties in preparation to starting to use this envelope object
// in note production
if(EPart->StartVolume!=-1) // do we have a start vol
LastVolume=EPart->StartVolume; // yes , set to this
DurationCounter=EPart->RawDuration;
// calculate how much the volume should increment per sample, this depends on what
// the last volume reached was for the last envelope part, initially for the first
// envelope part this would be 0. Volume is in the range 0-127
VolumeIncrement=float((EPart->TargetVolume-LastVolume))/float(DurationCounter);
CurrentVolume=LastVolume;
EPart->Completed=false;
}
XT_EnvelopePart_Class* XT_Envelope_Class::AddPart(uint16_t Duration,uint16_t StartVolume,uint16_t TargetVolume)
{
XT_EnvelopePart_Class* EPart;
EPart=AddPart(Duration,TargetVolume);
EPart->StartVolume=StartVolume;
return EPart;
}
XT_EnvelopePart_Class* XT_Envelope_Class::AddPart(uint16_t Duration,uint16_t TargetVolume)
{
// creates and adds an envelope part to this current envelope
XT_EnvelopePart_Class* EPart=new XT_EnvelopePart_Class();
EPart->SetDuration(Duration);
EPart->TargetVolume=TargetVolume;
if(FirstPart==0) // First in list of envelope parts
{
FirstPart=EPart;
LastPart=EPart;
}
else // Add to end of list of envelope parts
{
LastPart->NextPart=EPart;
LastPart=EPart;
}
return EPart;
}
// Envelope part class
void XT_EnvelopePart_Class::SetDuration(uint16_t Duration)
{
this->Duration=Duration;
this->RawDuration=50*Duration;
}
uint16_t XT_EnvelopePart_Class::GetDuration()
{
return Duration;
}
// Wave type classes
// Square wave
uint8_t XT_SquareWave_Class::NextByte()
{
// returns the next byte for this frequency of a square wave
Counter--;
if(Counter<0)
{
Counter+=CounterStartValue; // as this can be a decimal, any amount extra below zero
// is taken away from next starting value, in this way
// for higher frequencies we do not lose as much
// accuracy over a few waves, they average out to be
// about right "on average"
CurrentByte^=255;
return CurrentByte;
}
else
return CurrentByte;
}
void XT_SquareWave_Class::Init(int8_t Note)
{
if(Note!=-1) // use the note not a raw frequency
Frequency=XT_Notes[Note]; // if -1 then use the raw frequency
if(Frequency>25000)
Frequency=25000;
if(Frequency!=0) // avoid divide by 0, a freq of 0 means no sound
{
CounterStartValue=(25000/float(Frequency));
Counter=CounterStartValue;
}
else
Counter=0;
ChangeAmplitudeBy=255; // amount to add on to change wave every time waveform needs changing
// this value effectivly swaps the value from 0 to 255 and then 255 to 0
}
// Triangle wave functions
// Note that their
uint8_t XT_TriangleWave_Class::NextByte()
{
// returns the next byte for this frequency of a square wave
// called at the raw sample rate
NextAmplitude+=ChangeAmplitudeBy;
if(NextAmplitude>255)
{ // top of a peak, reverse direction
ChangeAmplitudeBy=-ChangeAmplitudeBy; // reverse direction
NextAmplitude=255;
}
else
{
if(NextAmplitude<0)
{ // bottom of a trough, reverse direction
ChangeAmplitudeBy=-ChangeAmplitudeBy; // reverse direction
NextAmplitude=0;
}
}
return int(NextAmplitude);
}
void XT_TriangleWave_Class::Init(int8_t Note)
{
NextAmplitude=127;
if(Note!=-1) // use the note not a raw frequency
Frequency=XT_Notes[Note]; // if -1 then use the raw frequency
if(Frequency>25000)
Frequency=25000;
if(Frequency!=0) // avoid divide by 0, a freq of 0 means no sound
{
ChangeAmplitudeBy=256/(25000/float(Frequency));// has to be 25K as has to ramp up and down to return to peak
}
}
uint8_t XT_SawToothWave_Class::NextByte()
{
// returns the next byte for this frequency of a square wave
NextAmplitude+=ChangeAmplitudeBy;
if(NextAmplitude>255) // top of a peak, right down to bottom again
NextAmplitude=0;
return int(NextAmplitude);
}
void XT_SawToothWave_Class::Init(int8_t Note)
{
NextAmplitude=0;
if(Note!=-1) // use the note not a raw frequency
Frequency=XT_Notes[Note]; // if -1 then use the raw frequency
if(Frequency>25000)
Frequency=25000;
if(Frequency!=0) // avoid divide by 0, a freq of 0 means no sound
ChangeAmplitudeBy=256/(BytesPerSec/float(Frequency)); // determines amplitude change per sample
}
///////////////////////////////////////////////
uint8_t XT_SineWave_Class::NextByte()
{
// returns the next byte for this frequency of a sine wave
CurrentAngle+=AngleIncrement;
if(CurrentAngle>255)
CurrentAngle=0; // Set to start
return SineValues[int(CurrentAngle)];
}
void XT_SineWave_Class::Init(int8_t Note)
{
CurrentAngle=0;
if(Note!=-1) // use the note not a raw frequency
Frequency=XT_Notes[Note]; // if -1 then use the raw frequency
if(Frequency>25000)
Frequency=25000;
if(Frequency!=0) // avoid divide by 0, a freq of 0 means no sound
AngleIncrement=256/(BytesPerSec/float(Frequency)); // determines frequency
}
//////////////////////////////////////////////
void XT_Sequence_Class::Init()
{
if(FirstItem!=0)
CurrentItem=FirstItem;
else
{
CurrentItem=0;
return;
}
while(CurrentItem!=0)
{
CurrentItem->PlayItem->RepeatIdx=CurrentItem->PlayItem->Repeat; // Initialise any repeats
CurrentItem->PlayItem->Init();
CurrentItem=CurrentItem->NextItem;
}
CurrentItem=FirstItem; // set back to start
CurrentItem->PlayItem->Playing=true; // mark as playing, init is called just prior to
// first play
}
void XT_Sequence_Class::AddPlayItem(XT_PlayListItem_Class *PlayItem)
{
// we copy the item so there are no conflicts with the item being played by the main Play routine
// which also uses the linked lists and other properties of the play object etc. This will only
// be a problem when we implement mixing of sounds.
XT_SequenceItem_Class *SequenceItem;
// create a new sequence item
SequenceItem=new XT_SequenceItem_Class();
SequenceItem->PlayItem=PlayItem;
if(FirstItem==0) // no items to play in list yet
{
FirstItem=SequenceItem;
LastItem=SequenceItem;
}
else
{
// add to end of list
LastItem->NextItem=SequenceItem;
LastItem=SequenceItem;
}
}
void XT_Sequence_Class::RemoveAllPlayItems()
{
// Remove all items in linked list and free associated memory
XT_SequenceItem_Class *ThisItem,*NextItem;
// Ensure not currently playing
Playing=false;
ThisItem=FirstItem;
while(ThisItem!=0)
{
NextItem=ThisItem->NextItem; // Get Next Item in list before removing this one
delete(ThisItem);
ThisItem=NextItem;
}
FirstItem=0;
LastItem=0;
}
uint8_t XT_Sequence_Class::NextByte()
{
if(CurrentItem==0)
return 0;
if(CurrentItem->PlayItem->Playing)
return CurrentItem->PlayItem->NextByte();
// If we get this far then the current item has stopped playing, time to play next
// item if this one does not repeat, note RepeatForever is NOT ignored, as although
// it would make no sense in most parts of a sequence as it would prevent any
// more from playing, it does make sense if used on the last item where you may
// want the last item to repeat forever, so it's up to the coder to not use
// forever on anything but the last item
// Check if set to play forever
if(CurrentItem->PlayItem->RepeatForever)
{
CurrentItem->PlayItem->Init();
CurrentItem->PlayItem->Playing=true;
return CurrentItem->PlayItem->NextByte();
}
// check if any repeats
if(CurrentItem->PlayItem->RepeatIdx>0)
{
CurrentItem->PlayItem->RepeatIdx--; // decrement number left
// repeat this item
CurrentItem->PlayItem->Init();
CurrentItem->PlayItem->Playing=true;
return CurrentItem->PlayItem->NextByte();
}
// If got this far then current item stopped playing and no repeats, move to next item
CurrentItem=CurrentItem->NextItem;
if(CurrentItem!=0)
{
CurrentItem->PlayItem->Playing=true;
return CurrentItem->PlayItem->NextByte();
}
else
Playing=false; // completed
}
// Filter class routines
XT_FilterNoise_Class::XT_FilterNoise_Class(int8_t AmountOfNoise)
{
this->Min=-AmountOfNoise;
this->Max=AmountOfNoise;
}
XT_FilterNoise_Class::XT_FilterNoise_Class(int8_t Min,int8_t Max)
{
this->Min=Min;
this->Max=Max;
}
uint8_t XT_FilterNoise_Class::FilterWave(uint8_t TheByte)
{
// returns TheByte adjusted either up or down by a random amount, determined
// by the min and max properties
// using random gives the effect of noise
int8_t TheRand=(rand()%((Max+1)-Min))+Min;
int16_t TheResult=int16_t(TheByte)+int16_t(TheRand);
if(TheResult>255)
TheResult=255;
else
if(TheResult<0)
TheResult=0;
return uint8_t(TheResult);
}