ERIS CORE
appCQT.h
Go to the documentation of this file.
1 /**
2  * @file appCQT.h
3  * @author Brian Monkaba (brian.monkaba@gmail.com)
4  * @brief
5  * @version 0.1
6  * @date 2021-08-08
7  *
8  * @copyright Copyright (c) 2021
9  *
10  */
11 
12 #include <float.h>
13 #include "AudioUtilities.h"
14 #include "AppManager.h"
15 
16 /**
17  * @brief CQT_HIGHRANGE_SPLIT_AT_BIN defines the cqt bin at which the high range fft will begin to be read from
18  *
19  */
20 
21 #define CQT_HIGHRANGE_SPLIT_AT_BIN 58
22 
23 /**
24  * @brief OSC_BANK_SIZE defins the MAX number of 'voices' used to resynthesize the input signal (LIMIT OF 16!)
25  *
26  */
27 #define OSC_BANK_SIZE 10
28 
29 /**
30  * @brief periodically transmit the fft output buffer to the serial port
31  *
32  */
33 #define TX_PERIODIC_FFT
34 
35 
36 //periods below selected from co-primes https://en.wikipedia.org/wiki/Periodical_cicadas
37 /*
38 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
39 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257,
40 263, 269, 271, 277, 281, 283, 293
41 
42 does it matter? no. just trying to load balance serial useage
43 */
44 
45 //transmit period in msec
46 #define TX_PERIOD 157
47 
48 /**
49  * @brief the period at which the some quantized voice data is sent to the serial port
50  *
51  */
52 #define TX_CQT_PERIOD 163
53 
54 // Constant Q Transform App
55 //
56 
57 
58 /**
59  * @brief Implements the AppCQT class
60  * Constant Q Transform Application
61  */
62 class AppCQT:public AppBaseClass {
63  public:
65  init();
66  }
67  protected:
68  bool is_active;
69  double rt_calls;
70  double update_calls;
74  int16_t osc_bank_size;
75  uint16_t high_range;
76  float64_t pll_p;
77  float64_t pll_f;
78  erisAudioSynthWaveform* osc[OSC_BANK_SIZE];
81  FFTReadRange fftRVal;
82  FFTReadRange fftHighRR[NOTE_ARRAY_LENGTH];
83  FFTReadRange fftLowRR[NOTE_ARRAY_LENGTH];
84  FFTReadRange oscBank[OSC_BANK_SIZE];
85 
86  void FLASHMEM init(){
87  update_priority = 0;
88  osc_bank_size = OSC_BANK_SIZE;
89  char buffer[32]; //used to build the stream names
90  sprintf(name, "AppCQT"); //set the applications name
91  if (ad == NULL || am== NULL) return;
92  AudioNoInterrupts();
93  for (int16_t i=0; i < osc_bank_size; i++){
94  sprintf(buffer, "waveform:%d", i+1);
95  //request the object from the audio director
97  //init the object to the default state
98  if (osc[i]!=NULL) osc[i]->begin(0.0, 0, WAVEFORM_SINE);
99  }
100  AudioInterrupts();
101  //take care to downcast fetched objects to the correct type!
104  if(fft == NULL || fft2 == NULL) return;
105  fft2->toggleActiveRange(); //switch to low range
106  //zero out the data variables
107  memset(&fftRVal,0,sizeof(FFTReadRange));
108  memset(&oscBank,0,sizeof(FFTReadRange)* OSC_BANK_SIZE);
109  //init the data dictionary value(s)
110  am->data->create(OCTAVE_DOWN_INTERVAL,(int32_t)0);
111  //init the QCT bins by loading the frequency ranges for each music note using a look up table
112  float flow;
113  float fhigh;
114  high_range = CQT_HIGHRANGE_SPLIT_AT_BIN;
115  for (uint16_t i=0;i < NOTE_ARRAY_LENGTH;i++){
116  flow = 0;
117  fhigh = 0;
118  if (i > 0 && i < NOTE_ARRAY_LENGTH-1){
119  //calculate the high and low frequencies for the given note
120  //this is done by splitting the frequency differences from the music note above and below
121  flow = note_freq[i] - (note_freq[i] - note_freq[i-1])/2.0;
122  fhigh = note_freq[i] + (note_freq[i+1] - note_freq[i])/2.0;
123  }else{
124  flow = 0;
125  fhigh = 0;
126  }
127  //zero out the destination within the array
128  memset(&fftHighRR[i],0,sizeof(FFTReadRange));
129  memset(&fftLowRR[i],0,sizeof(FFTReadRange));
130  //write the ranges to the bins, split between the high and low range fft
131  if (i >= high_range){
132  fftHighRR[i].cqtBin =i;
133  fftLowRR[i].cqtBin =i;
134 
135  fftHighRR[i].startFrequency = flow;
136  fftLowRR[i].startFrequency =0;
137 
138  fftHighRR[i].stopFrequency =fhigh;
139  fftLowRR[i].stopFrequency =0;
140 
141  }
142  else{
143  fftHighRR[i].cqtBin =i;
144  fftLowRR[i].cqtBin =i;
145 
146  fftHighRR[i].startFrequency =0;
147  fftLowRR[i].startFrequency =flow;
148 
149  fftHighRR[i].stopFrequency =0;
150  fftLowRR[i].stopFrequency =fhigh;
151  }
152  }
153 
154  rt_calls = 0;
155  update_calls = 0;
156  pll_p=0.0;
157  pll_f=1.0;
158  //enable the fft blocks
162  is_active = true;
163  am->data->create(FFT_AGE_THRESHOLD,(int32_t)6);
164  AudioNoInterrupts();
165  fft->enableFFT(true);
166  fft2->enableFFT(true);
167  AudioInterrupts();
168  };
169 
170  void FASTRUN render(){
171  if (!is_active || draw == NULL) return;
172  update_calls++;
173  #ifdef TX_PERIODIC_FFT
174  if (fft_buffer_serial_transmit_elapsed > TX_PERIOD && Serial.availableForWrite() > 5900){
175  if(sci==NULL) return;
179  sci->printf(F("S 512"));
180  for(int i = 0; i < 512; i+=1){
181  //transmitt low range then high range
182  //if(Serial.availableForWrite() < 1000){delayMicroseconds(10000);}
183  if(fft_buffer_select_for_serial_transmit == 0) sci->printf(F(",%d"),(int)(100.0*fft2->output[i]));
184  if(fft_buffer_select_for_serial_transmit == 1) sci->printf(F(",%d"),(int)(100.0*fft->output[i]));
185  //if ((i % 127)==0) Serial.flush();
186  }
187  sci->println("");
188  }
191  {
193  sci->println(F("S FIN")); // end of series data
194  }
195  sci->sendLZ4Message();
196  }
197  }
198  #endif
199  if (cqt_serial_transmit_elapsed > TX_CQT_PERIOD){
201  for (uint16_t i=0;i < osc_bank_size;i++){
202  if (oscBank[i].cqtBin < high_range) sci->printf(F("CQT_L %d,%s,%.0f,%.0f,%.2f,%.3f,%.3f\n"),oscBank[i].cqtBin,note_name[oscBank[i].cqtBin],oscBank[i].peakFrequency,note_freq[oscBank[i].cqtBin],oscBank[i].phase,oscBank[i].avgValueFast,oscBank[i].avgValueSlow*1000.0,oscBank[i].transientValue*100.0);
203  if (oscBank[i].cqtBin >= high_range) sci->printf(F("CQT_H %d,%s,%.0f,%.0f,%.2f,%.3f,%.3f\n"),oscBank[i].cqtBin,note_name[oscBank[i].cqtBin],oscBank[i].peakFrequency,note_freq[oscBank[i].cqtBin],oscBank[i].phase,oscBank[i].avgValueFast,oscBank[i].avgValueSlow*1000.0,oscBank[i].transientValue*100.0);
204  }
205  sci->printf(F("CQT_EOF\n"));
206  sci->sendLZ4Message();
207  }
208  //Serial.flush();
210  }
211 
215  if (draw == NULL) return;
216  if (has_pop) draw->fillRoundRect(x,y,w,h,3,CL(0x07,0x00,0x10));
217  //draw the scale lines
218  draw->setFont(Arial_8);
219  draw->setTextColor(ILI9341_DARKGREY);
220  for(uint8_t i=1;i < 18; i++){
221  draw->drawLine(x,y + (h - (log1p((0.1 * i))) * h),x+w,y + (h - (log1p((0.1 * i))) * h),ILI9341_DARKCYAN);
222  if (has_pop){
223  draw->setCursor(x + i * 17,y+ (h - (log1p((0.1 * i))) * h) - 9);
224  if (i < 17) draw->print((log1p((0.1 * i))));
225  }
226  }
227 
228  //draw the cqt bins
229  const float im = (w-1)/(float)(sizeof(note_freq)/sizeof(note_freq[0]));
230  uint16_t nx;
231  float signal;
232  float amp;
233 
234  for (uint16_t i=0;i< sizeof(note_freq)/sizeof(note_freq[0])-1;i++){
235  amp = fftHighRR[i].peakValue;//fft->read(&fftHighRR[i]);
236  signal = (log1p(amp)) * h;
237  if (signal>(h-1)) signal = h-1;
238  if (signal<0) signal = 0;
239  nx = (uint16_t)(im*fftHighRR[i].cqtBin);
240  //draw->fillRoundRect(x+nx,y,2,(uint16_t)signal,1,ILI9341_ORANGE);
241  draw->fillRoundRect(x+nx,y+h - (uint16_t)signal,2,(uint16_t)signal,1,ILI9341_CYAN);
242  amp = fftLowRR[i].avgValueFast;//fft2->read(&fftLowRR[i]);
243  signal = (log1p(amp)) * h;
244  if (signal>(h-1)) signal = h-1;
245  if (signal<0) signal = 0;
246  nx = (uint16_t)(im*fftLowRR[i].cqtBin);
247  draw->fillRoundRect(x+nx,y+h - (uint16_t)signal,2,(uint16_t)signal,1,ILI9341_MAGENTA);
248  }
249 
250  //make a second pass but only draw the oscillators
251  draw->setTextColor(ILI9341_GREENYELLOW);
252  for (uint16_t i=0;i < osc_bank_size;i++){
253  amp = oscBank[i].avgValueFast;//fft->read(&fftHighRR[i]);
254  signal = (log1p(amp)) * h;
255  if (signal<0) signal = 0;
256  if (signal>(h-1)) signal = h-1;
257  nx = (uint16_t)(im*oscBank[i].cqtBin);
258  if ((y+h - (uint16_t)signal) < h && signal > 1) draw->fillRoundRect(x+nx,y+h - (uint16_t)signal,2,4,1,CL(0xFF,0xA0,(uint8_t)(300*oscBank[i].transientValue)));
259  if ((y+h - (uint16_t)signal) < h && has_pop){
260  draw->setCursor(x+nx - 5,y+h - (uint16_t)signal - 35);
261  draw->print(note_name[oscBank[i].cqtBin]);
262  draw->setCursor(x+nx - 5,y+h - (uint16_t)signal - 50);
263  draw->printf("%.0f",oscBank[i].peakFrequency);
264  }
265  }
266 
267  //draw the border
268  draw->drawRoundRect(x,y,w,h,4,ILI9341_MAGENTA);
269 
270  if (fftHighRR[0].peakValue > fftLowRR[0].peakValue) {
271  fftRVal = fftHighRR[0];
272  draw->setTextColor(ILI9341_DARKCYAN);
273  } else{
274  fftRVal = fftLowRR[0];
275  draw->setTextColor(ILI9341_MAGENTA);
276  }
277  }; //called only when the app is active
278 
279  void FLASHMEM update(){
280  if (!is_active) return;
281  rt_calls++;
282  //if (rt_calls < 10000) return;
283  if (fft == NULL || fft2 == NULL) return;
284 
285  if (fft2->capture() && fft->capture()){
286  //AudioNoInterrupts();
287  fft2->analyze();
288  fft->analyze();
289  //AudioInterrupts();
290  updateOscillatorBank(true);
291  updateOscillatorBank(false);
292  am->data->increment(CQT_UPDATE_COUNT);
293  }
294  //Serial.flush();
295  }; //allways called even if app is not active
296 
297  void FLASHMEM onFocus(){ //called when given focus
298  if (fft == NULL || fft2 == NULL) return;
299  fft->enableFFT(true);
300  fft2->enableFFT(true);
301  is_active = true;
302  };
303 
304  void FLASHMEM onFocusLost(){ //called when focus is taken
305  if (fft == NULL || fft2 == NULL) return;
306  fft->enableFFT(false);
307  fft2->enableFFT(false);
308  is_active = false;
309  };
310 
311  void FLASHMEM onTouch(uint16_t t_x, uint16_t t_y){
312  //check if touch point is within the application bounding box
313  if (t_x > x && t_x < (x + w) && t_y > y && t_y < (y + h)){
314  //is touched
315  if(!has_pop){
316  //getFocus();
317  requestPopUp(true);
318 
319  }else{
320  //returnFocus();
321  releasePopUp();
322  }
323  }
324  };
325 
326  void onTouchRelease(uint16_t x, uint16_t y){
327  };
328 
329  void FASTRUN updateOscillatorBank(bool low_range_switch){
330  bool found;
331  float peak_read=-1000;
332  float peak;
333 
334  if(fft==NULL || fft2==NULL) return;
335  //sort the FFTReadRange array by cqt bin number
336  if (low_range_switch) {erisAudioAnalyzeFFT1024::sort_fftrr_by_cqt_bin(fftLowRR,NOTE_ARRAY_LENGTH);}
338  //read the fft ranges, which stores the results into the FFTReadRange array
339  for (uint16_t i=0; i < NOTE_ARRAY_LENGTH; i++){
340  if (low_range_switch) peak = fft2->read(&fftLowRR[i]);
341  else peak = fft->read(&fftHighRR[i]);
342  if (peak>peak_read){
343  peak_read = peak;
344  }
345  }
346  //update the oscillator bank with the current values
347  for (uint16_t i=0; i < osc_bank_size; i++){
348  if (oscBank[i].cqtBin >= high_range){
349  if (low_range_switch==false){
350  oscBank[i] = fftHighRR[oscBank[i].cqtBin];
351  }
352  }else if (low_range_switch==true){
353  oscBank[i] = fftLowRR[oscBank[i].cqtBin];
354  }
355  }
356  //sort the updated cqt bins by peakValue
357  if (low_range_switch){erisAudioAnalyzeFFT1024::sort_fftrr_by_value(fftLowRR,NOTE_ARRAY_LENGTH);
359 
360  //Add the new osc settings
361  //low range
362  if (low_range_switch){
363  for(uint16_t i=0; i < osc_bank_size; i++){
364  found = false;
365  for (uint16_t j=0; j < osc_bank_size; j++){
366  if (oscBank[j].cqtBin == fftLowRR[i].cqtBin){
367  //oscBank[j] = fftLowRR[i];
368  found = true;break;
369  }
370  }
371  if(!found){
372  for(int16_t k= osc_bank_size-1; k >= 0;k--){
373  if ((oscBank[k].avgValueFast) < (fftLowRR[i].avgValueFast)){
374  //fftLowRR[i].phase = 0;
375  pll_p = 0.0;
376  pll_f = 1.0;
377  oscBank[k] = fftLowRR[i];
378  break;
379  }
380  }
381  }
382  }
383  } else for(uint16_t i=0; i < osc_bank_size; i++){ //high range
384  found = false;
385  for (uint16_t j=0; j < osc_bank_size; j++){
386  if (oscBank[j].cqtBin == fftHighRR[i].cqtBin){
387  //oscBank[j] = fftHighRR[i];
388  found = true;break;
389  }
390  }
391  if(!found){
392  for(int16_t k= osc_bank_size-1; k >= 0;k--){
393  if ((oscBank[k].avgValueFast) < (fftHighRR[i].avgValueFast)){
394  //fftHighRR[i].phase = 0;
395  pll_p = 0.0;
396  pll_f = 1.0;
397  oscBank[k] = fftHighRR[i];
398  break;
399  }
400  }
401  }
402  }
403 
404  pll_p = 0.0;
405  pll_f = 1.0;
406 
407  //sort the updated cqt bins by peakValue
408  if (low_range_switch){erisAudioAnalyzeFFT1024::sort_fftrr_by_value(fftLowRR,NOTE_ARRAY_LENGTH);
410 
411  //take the phase from the dominant frequency component
412  float dominantPhase = 0;
413  if (low_range_switch){
414  dominantPhase = fftLowRR[0].phase;
415  } else dominantPhase = fftHighRR[0].phase;;
416 
417  //actually update the oscillators -- IF the data is not TOO old
418  //if (updateRT_call_period > am->data->read(FFT_AGE_THRESHOLD)) return;
419 
420  for(int16_t i=0; i < osc_bank_size; i++){
421  float a,f;
422  float64_t phase_aligner;
423  if( ( (oscBank[i].cqtBin <= high_range) && (low_range_switch == true)) || ((oscBank[i].cqtBin > high_range) && (low_range_switch == false))){
424  if (oscBank[i].peakFrequency > 30.0){
425  f = oscBank[i].peakFrequency;
426  a = sqrt(oscBank[i].avgValueFast)/OSC_BANK_SIZE;
427  if(!isnan(a)){
428  phase_aligner = ((dominantPhase - oscBank[i].phase)/dominantPhase);
429  phase_aligner = (phase_aligner * (float64_t)osc[i]->getPhase()) + pll_p;
430  if(!isnan(phase_aligner)) phase_aligner=0;
431  f = (pll_f * f * octave_down[am->data->read(OCTAVE_DOWN_INTERVAL)]);
432  if (f < 20) f = 20;
433  if (f > 20000) f = 20000;
434  if(osc[i] != NULL){
435  osc[i]->frequency(f);
436  osc[i]->amplitude(a);
437  osc[i]->phase(phase_aligner);
438  }
439  };
440  }
441  }
442  }
443 
444  return;
445  }
446 
447  void messageHandler(AppBaseClass *sender, const char *message){
448  if(strcmp(message,ENABLE)==0){
449  onFocus();//reusing the onFocus function to ENABLE processing
450  }else if(strcmp(message,DISABLE)==0){
451  onFocusLost();//reusing the onFocusLost function to DISABLE processing
452  }
453 
454  if(sender->isName("SCI")){
455  Serial.print(F("M appCQT::MessageHandler SCI param: "));
456  Serial.println(message);
457  if (strcmp(message,CQT_INFO)==0){
458  //sort the bins to transmit them in order
461  for (uint16_t i=1;i< NOTE_ARRAY_LENGTH - 1;i++){
463  sci->printf(F("M L %d,%f,%f,%d,%d,%d\t\t\t"),fftLowRR[i].cqtBin,fftLowRR[i].startFrequency,fftLowRR[i].stopFrequency,fftLowRR[i].startBin,fftLowRR[i].stopBin,fftLowRR[i].stopBin-fftLowRR[i].startBin);
465  sci->sendLZ4Message();
466  }
467  }
468  }
469  }
470  }
471 };
static const char * note_name[]
Definition: Eris.h:744
const float octave_down[]
Definition: Eris.h:739
static const float note_freq[]
Definition: Eris.h:745
SvcSerialCommandInterface * sci
Definition: AppBaseClass.h:39
void requestPopUp(bool exclusive=false)
request popup from the AppManager will be activated by the next render loop applications in popup m...
AppManager * am
Definition: AppBaseClass.h:38
void releasePopUp()
gives up popup
ILI9341_t3_ERIS * draw
Definition: AppBaseClass.h:41
bool isName(const char *name_string)
Compares the name_string to the app class instance name (string)
Definition: AppBaseClass.h:93
char name[MAX_NAME_LENGTH]
Definition: AppBaseClass.h:74
uint16_t update_priority
Definition: AppBaseClass.h:52
AudioDirector * ad
Definition: AppBaseClass.h:37
base class definition / implementation from which all app classes will be derived and override
Definition: AppBaseClass.h:34
float64_t pll_p
Definition: appCQT.h:76
void FLASHMEM onFocus()
Event handler called when the app gains focus.
Definition: appCQT.h:297
void FLASHMEM onFocusLost()
Event handler called when the app loses focus.
Definition: appCQT.h:304
erisAudioSynthWaveform * osc[OSC_BANK_SIZE]
Definition: appCQT.h:78
float64_t pll_f
Definition: appCQT.h:77
uint16_t high_range
Definition: appCQT.h:75
void FLASHMEM update()
update loop
Definition: appCQT.h:279
double rt_calls
Definition: appCQT.h:69
int16_t osc_bank_size
Definition: appCQT.h:74
bool is_active
Definition: appCQT.h:68
void onTouchRelease(uint16_t x, uint16_t y)
Event handler for touch release.
Definition: appCQT.h:326
AppCQT()
Definition: appCQT.h:64
void FLASHMEM init()
Definition: appCQT.h:86
void messageHandler(AppBaseClass *sender, const char *message)
receiver method for inter-app string based communication
Definition: appCQT.h:447
uint8_t fft_buffer_select_for_serial_transmit
Definition: appCQT.h:71
FFTReadRange fftLowRR[NOTE_ARRAY_LENGTH]
Definition: appCQT.h:83
void FLASHMEM onTouch(uint16_t t_x, uint16_t t_y)
Event handler called on touch.
Definition: appCQT.h:311
erisAudioAnalyzeFFT1024 * fft
Definition: appCQT.h:79
FFTReadRange fftHighRR[NOTE_ARRAY_LENGTH]
Definition: appCQT.h:82
elapsedMillis fft_buffer_serial_transmit_elapsed
Definition: appCQT.h:72
FFTReadRange oscBank[OSC_BANK_SIZE]
Definition: appCQT.h:84
void FASTRUN render()
render loop
Definition: appCQT.h:170
elapsedMillis cqt_serial_transmit_elapsed
Definition: appCQT.h:73
double update_calls
Definition: appCQT.h:70
FFTReadRange fftRVal
Definition: appCQT.h:81
erisAudioAnalyzeFFT1024 * fft2
Definition: appCQT.h:80
void FASTRUN updateOscillatorBank(bool low_range_switch)
Definition: appCQT.h:329
Implements the AppCQT class Constant Q Transform Application.
Definition: appCQT.h:62
SvcDataDictionary * data
Definition: AppManager.h:63
AudioStream * getAudioStreamObjByName(const char *AudioStreamObjName)
void frequency(float freq)
void begin(short t_type)
void phase(float angle)
int32_t read(const char *key)
returns the int32_t value of the record
bool create(const char *key, int32_t val, uint32_t *owner)
create a record with ownership
bool increment(const char *key)
increments the value of a global record creates a new record if one does not exist and initializes ...
bool requestStartLZ4Message()
request to start a lz4 compressed message starts the message and returns true if available returns f...
void sendLZ4Message()
Calling this function signals the end of a compressed message. The txBuffer contents are lz4 compres...
void enableFFT(bool enable_state)
static void sort_fftrr_by_cqt_bin(FFTReadRange *a, size_t n)
static void sort_fftrr_by_value(FFTReadRange *a, size_t n)
float read(unsigned int binNumber)
float peakValue
float transientValue
float startFrequency
float phase
float stopFrequency
float avgValueSlow
uint16_t stopBin
uint16_t cqtBin
float avgValueFast
float peakFrequency
uint16_t startBin