27e8a6ff46
also shown in https://youtu.be/PKQmOsJ-g1I
531 lines
11 KiB
Arduino
531 lines
11 KiB
Arduino
/*
|
|
* this file contains the code of the multitrack looper
|
|
*
|
|
* Author: Marcel Licence
|
|
*/
|
|
|
|
|
|
/*
|
|
* MAX_LOOP can only increased by decreasing TRACK_CNT (track count)
|
|
*
|
|
* for example using only 2 tracks will allow MAX_LOOP to be set to 1026492
|
|
* all PSRAM memory will be used
|
|
* if values are set to big the startup of the firmware will fail
|
|
*/
|
|
#define MAX_LOOP 513246
|
|
#define TRACK_CNT 4
|
|
|
|
int16_t *loopLine[TRACK_CNT];
|
|
float loopGainOut[TRACK_CNT];
|
|
float loopGainIn;
|
|
float loopPanLCH[TRACK_CNT];
|
|
float loopPanRCH[TRACK_CNT];
|
|
bool loopActive[TRACK_CNT];
|
|
bool loopContainsData[TRACK_CNT];
|
|
float loopMeter[TRACK_CNT + 2];
|
|
uint32_t loopErase[TRACK_CNT] ;
|
|
|
|
uint8_t loopInCh = 0xFF;
|
|
uint32_t loopMax = MAX_LOOP;
|
|
uint32_t loopMaxCalc = MAX_LOOP;
|
|
|
|
/*
|
|
* init of the multitrack looper
|
|
*/
|
|
void Loop_init(void)
|
|
{
|
|
psramInit();
|
|
Serial.printf("Total PSRAM: %d\n", ESP.getPsramSize());
|
|
Serial.printf("Free PSRAM: %d\n", ESP.getFreePsram());
|
|
|
|
for (int n = 0; n < TRACK_CNT; n++)
|
|
{
|
|
loopLine[n] = (int16_t *)ps_malloc(sizeof(int16_t) * MAX_LOOP);
|
|
|
|
if (loopLine[n] == NULL)
|
|
{
|
|
/* when you see the message the count of TRACK_CNT is too high or MAX_LOOP is too big */
|
|
Serial.printf("not enough memory!\n");
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < MAX_LOOP; i++)
|
|
{
|
|
loopLine[n][i] = 0.0f;
|
|
}
|
|
Serial.printf("done!..\n");
|
|
}
|
|
loopGainOut[n] = 1.0f;
|
|
loopPanLCH[n] = 1.0f;
|
|
loopPanRCH[n] = 1.0f;
|
|
|
|
Serial.printf("- Free PSRAM: %d\n", ESP.getFreePsram());
|
|
}
|
|
|
|
Serial.printf("Total PSRAM: %d\n", ESP.getPsramSize());
|
|
Serial.printf("Free PSRAM: %d\n", ESP.getFreePsram());
|
|
|
|
loopGainIn = 0.0f;
|
|
loopInCh = 0;
|
|
|
|
Serial.printf("Loop Max Length: %0.0f seconds\n", ((float)MAX_LOOP) / 44100.0f);
|
|
|
|
Loop_CompleteReset();
|
|
}
|
|
|
|
uint32_t loopLen = (MAX_LOOP - 2);
|
|
uint32_t loopIn = 0;
|
|
float loop_f = 0;
|
|
float loopSpeed = 1.0f;
|
|
|
|
uint32_t loopSubCnt = 0;
|
|
|
|
#define absf(a) (a>0)?a:(-a)
|
|
|
|
/*
|
|
* this function processes the input audio and mixes also the audio output to the signal
|
|
*/
|
|
void Loop_Process(float *signal_l, float *signal_r)
|
|
{
|
|
float tempL = 0.0f, tempR = 0.0f;
|
|
|
|
loopMeter[TRACK_CNT] = max(absf(*signal_l), loopMeter[TRACK_CNT] );
|
|
loopMeter[TRACK_CNT] = max(absf(*signal_r), loopMeter[TRACK_CNT] );
|
|
|
|
loopSubCnt++;
|
|
|
|
if (Loop_AnyTrackActive() == false)
|
|
{
|
|
loopLen = (MAX_LOOP - 2);
|
|
loopIn = 0;
|
|
loop_f = 0.0f;
|
|
}
|
|
|
|
/*
|
|
* audio through
|
|
*/
|
|
*signal_l *= loopGainIn;
|
|
*signal_r *= loopGainIn;
|
|
|
|
tempL = *signal_l;
|
|
tempR = *signal_r;
|
|
|
|
for (int n = 0; n < TRACK_CNT; n++)
|
|
{
|
|
/*
|
|
* do erasing first if active
|
|
* to avoid getting any signal out of the track
|
|
*/
|
|
if (loopErase[n] != 0xFFFFFFFFUL)
|
|
{
|
|
loopLine[n][loopIn] = 0;
|
|
}
|
|
|
|
if (n == loopInCh)
|
|
{
|
|
loopContainsData[loopInCh] = true;
|
|
loopLine[loopInCh][loopIn] += (((float)0x8000) * *signal_l * loopPanLCH[loopInCh]);
|
|
loopLine[loopInCh][loopIn] += (((float)0x8000) * *signal_r * loopPanRCH[loopInCh]);
|
|
}
|
|
|
|
/*
|
|
* calc float value for later use
|
|
*/
|
|
float loopOut = ((float)loopLine[n][loopIn]) / ((float)0x8000);
|
|
|
|
/*
|
|
* update the vu meter value
|
|
*/
|
|
loopMeter[n] = max(absf(loopOut), loopMeter[n]);
|
|
|
|
/*
|
|
* playback only if active
|
|
*/
|
|
if (loopActive[n])
|
|
{
|
|
tempL += loopGainOut[n] * loopOut * loopPanLCH[n];
|
|
tempR += loopGainOut[n] * loopOut * loopPanRCH[n];
|
|
}
|
|
|
|
#if 0 /* I don't remember the use of this */
|
|
delayLine_l[delayIn] += delayLine_l[loopOut] * delayFeedback;
|
|
delayLine_r[delayIn] += delayLine_r[loopOut] * delayFeedback;
|
|
#endif
|
|
}
|
|
|
|
loop_f += loopSpeed;
|
|
|
|
if (loop_f >= loopMax)
|
|
{
|
|
loop_f -= loopMax;
|
|
}
|
|
|
|
loopIn = loop_f;
|
|
|
|
for (int n = 0; n < TRACK_CNT; n++)
|
|
{
|
|
/* stop erasing when final position has been reached */
|
|
if (loopErase[n] == loopIn)
|
|
{
|
|
loopErase[n] = 0xFFFFFFFFUL;
|
|
}
|
|
}
|
|
|
|
*signal_l = tempL;
|
|
*signal_r = tempR;
|
|
|
|
/*
|
|
* get max value of left and right channel
|
|
*/
|
|
loopMeter[TRACK_CNT + 1] = max(absf(*signal_l), loopMeter[TRACK_CNT + 1] );
|
|
loopMeter[TRACK_CNT + 1] = max(absf(*signal_r), loopMeter[TRACK_CNT + 1] );
|
|
|
|
Loop_ProcessButton();
|
|
}
|
|
|
|
bool reset_active = false;
|
|
uint32_t reset_active_time = 0;
|
|
|
|
/*
|
|
* reset the looper to initial state
|
|
*/
|
|
void Loop_CompleteReset(void)
|
|
{
|
|
loopMax = MAX_LOOP;
|
|
loopIn = 0;
|
|
loop_f = 0;
|
|
loopSpeed = 1.0f;
|
|
loopInCh = 0xFF;
|
|
|
|
for (int channel = 0; channel < TRACK_CNT; channel ++)
|
|
{
|
|
Status_ValueChangedInt("LoopTrackErase", channel);
|
|
for (int n = 0; n < MAX_LOOP; n++)
|
|
{
|
|
loopLine[channel][n] = 0.0f;
|
|
}
|
|
loopActive[channel] = false;
|
|
loopContainsData[channel] = false;
|
|
}
|
|
|
|
memset(loopErase, 0xFF, sizeof(loopErase));
|
|
}
|
|
|
|
void Loop_ProcessButton(void)
|
|
{
|
|
if (reset_active)
|
|
{
|
|
reset_active_time++;
|
|
if (reset_active_time > 44100 * 3)
|
|
{
|
|
Loop_CompleteReset();
|
|
reset_active = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
reset_active_time = 0;
|
|
}
|
|
}
|
|
|
|
void Loop_ResetToStart(uint8_t channel, float value)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
reset_active = true;
|
|
loop_f = 0;
|
|
}
|
|
else
|
|
{
|
|
reset_active = 0;
|
|
}
|
|
}
|
|
|
|
void Loop_PlayNormal(uint8_t channel, float value)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
loop_f = loopIn;
|
|
loopSpeed = 1.0f;
|
|
}
|
|
}
|
|
|
|
float getLoopSpeed(void)
|
|
{
|
|
return loopSpeed;
|
|
}
|
|
|
|
void Loop_SetLength(uint8_t channel, float value)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
loopMax = loop_f;
|
|
}
|
|
}
|
|
|
|
void Loop_SetEndByTempo(void)
|
|
{
|
|
loopMaxCalc = loop_f;
|
|
loop_f = 0;
|
|
}
|
|
|
|
void Loop_SetSpeed(uint8_t channel, float value)
|
|
{
|
|
value = pow(2.0f, 4.0f * (value - 0.5));
|
|
loopSpeed = value;
|
|
Status_ValueChangedFloat("LoopSpeed", loopSpeed);
|
|
}
|
|
|
|
void Loop_StartAll(uint8_t channel, float value)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
for (int n = 0; n < TRACK_CNT; n++)
|
|
{
|
|
if (loopContainsData[n])
|
|
{
|
|
loopActive[n] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* this function will activate recording
|
|
* or playback
|
|
* or toggle between both modes
|
|
*/
|
|
void Loop_SelectTrack(uint8_t channel, float value)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
/*
|
|
* count in if all empty
|
|
*/
|
|
if (Loop_AnyTrackActive() == false)
|
|
{
|
|
Click_StartFirst();
|
|
}
|
|
|
|
if (loopContainsData[channel] == false)
|
|
{
|
|
/*
|
|
* in case of selecting an empty channel we switch always to record mode
|
|
*/
|
|
loopActive[channel] = true;
|
|
loopInCh = channel;
|
|
}
|
|
else
|
|
{
|
|
if (loopActive[channel] == false)
|
|
{
|
|
/*
|
|
* if we have data in it but track is not active we go into playback
|
|
*/
|
|
loopInCh = 0xFF;
|
|
loopActive[channel] = true;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* if still active we switch between playback and overdub/record
|
|
*/
|
|
if (loopInCh == channel)
|
|
{
|
|
/*
|
|
* still selected going to playback
|
|
*/
|
|
loopInCh = 0xFF;
|
|
}
|
|
else
|
|
{
|
|
loopInCh = channel;
|
|
Status_ValueChangedInt("LoopRecTrack", loopInCh);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
void Loop_StopChannel(uint8_t channel, float value)
|
|
{
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
loopActive[channel] = false;
|
|
|
|
/* stop recording if rec is active */
|
|
if (channel == loopInCh)
|
|
{
|
|
loopInCh = 0xFF;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Loop_Stop(uint8_t channel, float value)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
for (int n = 0; n < TRACK_CNT; n ++)
|
|
{
|
|
loopActive[n] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* erase track will be activated
|
|
* a complete clear in this call was too slow
|
|
*/
|
|
void Loop_EraseTrack(uint8_t channel, float value)
|
|
{
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
Status_ValueChangedInt("LoopEraseTrack", channel);
|
|
|
|
loopErase[channel] = loop_f;
|
|
loopContainsData[channel] = false;
|
|
if (channel != loopInCh)
|
|
{
|
|
loopActive[channel] = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Loop_SetChannelGainIn(uint8_t channel, float value)
|
|
{
|
|
loopGainIn = value;
|
|
Status_ValueChangedFloat("LoopGainIn", loopGainIn);
|
|
}
|
|
|
|
void Loop_SetChannelGainOut(uint8_t channel, float value)
|
|
{
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
loopGainOut[channel] = value;
|
|
//Serial.printf("Ch[%d].outLevel: %0.2f\n", channel, value);
|
|
Status_ValueChangedFloat("LoopTrackLevel", value); // todo track number is missing
|
|
}
|
|
}
|
|
|
|
void Loop_SetChannelPan(uint8_t channel, float value)
|
|
{
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
loopPanLCH[channel] = value < 0.5 ? 1 : 2 * (0.5 - (value - 0.5));
|
|
loopPanRCH[channel] = value > 0.5 ? 1 : 2 * (value);
|
|
//Serial.printf("Ch[%d].Pan: %0.2f:%0.2f\n", channel, loopPanLCH[channel], loopPanRCH[channel]);
|
|
Status_ValueChangedInt("LoopTrackPan", channel); // todo track number is missing
|
|
}
|
|
}
|
|
|
|
void Loop_JumpPosQuarter(uint8_t quarter, float value)
|
|
{
|
|
if (value > 0)
|
|
{
|
|
if (loopMax < loopMaxCalc)
|
|
{
|
|
loop_f = ((float)loopMax) * ((float)quarter) * (0.25f);
|
|
}
|
|
else
|
|
{
|
|
loop_f = ((float)loopMaxCalc) * ((float)quarter) * (0.25f);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Loop_NoteOn(uint8_t ch, uint8_t note)
|
|
{
|
|
if (ch == 15)
|
|
{
|
|
loop_f = 0;
|
|
loopSpeed = pow(2.0f, 1.0f / 12.0f * (note - 69)); /* this would be the a as middle */
|
|
}
|
|
}
|
|
|
|
float *Loop_GetMeterValues(void)
|
|
{
|
|
return loopMeter;
|
|
}
|
|
|
|
float Loop_GetRelPos(void)
|
|
{
|
|
return loop_f / ((float)MAX_LOOP);
|
|
}
|
|
|
|
float Loop_GetRelLen(void)
|
|
{
|
|
return loopMax / ((float)MAX_LOOP);
|
|
}
|
|
|
|
float Loop_GetMaxRecLengthSeconds(void)
|
|
{
|
|
return ((float)MAX_LOOP) / 44100.0f;
|
|
}
|
|
|
|
uint8_t Loop_GetRecTrack(void)
|
|
{
|
|
return loopInCh;
|
|
}
|
|
|
|
bool Loop_IsTrackActive(uint8_t channel)
|
|
{
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
return loopActive[channel];
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Loop_IsUnderErase(uint8_t channel)
|
|
{
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
return loopErase[channel] != 0xFFFFFFFFUL;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Loop_TrackContainsData(uint8_t channel)
|
|
{
|
|
if (channel < TRACK_CNT)
|
|
{
|
|
return loopContainsData[channel];
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Loop_AnyTrackActive(void)
|
|
{
|
|
for (int n = 0; n < TRACK_CNT; n++)
|
|
{
|
|
if (loopActive[n])
|
|
{
|
|
return true;
|
|
}
|
|
if (loopErase[n] != 0xFFFFFFFFUL)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|