Apologies for a very newbie question. I’ve been poking around the examples/forums and doing some experimentation on my Daisy Seed the past two days but I’ve realized I’m a bit in over my head trying to make this a reality.
With the Daisy Seed I thought I might be able to drive a small I2C 128x32 OLED screen to draw an oscilloscope visualizer of incoming audio. After all, I saw a video of an Oopsy example doing just that with Max/MSP on the Daisy Patch.
I’d hoped I could maybe cobble this together in Arduino using the U8G2 library, but I’m having trouble figuring out how to utilize the callback routine’s InputBuffer for this - if that’s even a possibility (I’m beginning to think it isn’t). I thought the answer may lie in using ADC instead of audio in, but I’m a bit confused because I don’t see any DaisyDuino examples that use ADC, so I don’t really know how to start? (Possible I’m misidentifying what arduino ADC examples look like).
I realize this is an odd use-case for the Daisy since I’m not even trying to do any DSP, I’d figured the embedded audio would make this simpler than trying to do something like this from scratch with a teensy or something .
A) If you can envision a way to let a Daisy Seed drive an I2C OLED to be an audio-in oscilloscope (preferably in Arduino but open to any answers) how would you start?
or B) Is this a completely foolish idea to pursue because this is not an intended use-case?
Thanks in advance! Sorry for an open-ended newbie question!
I checked this out before, it’s cool that it has the code for updating the oled, however all it’s doing is running two identical oscillators, one that’s operating in the audio callback and the other for driving the oscilloscope. It’s doing nothing with an input signal which is what I’m stuck on.
I’m wondering if it’s possible to, say, write values from the InputBuffer to a variable that can get accessed in loop() or main() in some code that would be updating the oled display, or if there’s some other way to do this, or if it isn’t possible.
You’d need a ring buffer to collect the data (either raw, or processed). Then, you just need to draw the contents of the buffer. I guess the tricky part would be to map samples to pixels.
UPDATE: When you’re trying to experiment with stuff and you’re looking for an expected outcome, it’s probably a good idea to correctly write your code. Still open to suggestions but I’ve gotten past the barrier I was behind!
For those curious, my problem was basically I was missing the line
while (true)
draw pixels where Y = (buffer[i] + 1)/2 * (height of OLED in pixels)
I don’t know if this is any help or if I am just stating the obvious (or too simple). BTW I think there is a Daisy ringbuffer implementation in some file in libdaisy.
Thanks @StaffanMelin and @brbrr! Truth be told this is the first I’m learning about ring buffers but y’all are totally right that’s the way to go! I see the ringbuffer class in libdaisy Think there’s an example that uses it anywhere? I think I mostly get how it works but would like to see an example just in case.
When I first ran into problems with this project, and with my limited experience with microcontrollers, I didn’t think I would even get this far. I’m thrilled that this even at least looks like an oscilloscope, but I’ve kinda reached my limit of knowledge on how to troubleshoot its jumpiness and the tearing.
The CircularBuffer size is kind of arbitrary as I tweaked it many times to see how it affected the visuals. I feel like at least one part of many of my problem has to do with the discrepancy between the refresh rate of the Callback vs the refresh rate of Loop vs the refresh rate of the OLED?
Is there a way to access the InputBuffer outside of the callback? I feel like I’d rather write the audio_in to the buffer within loop() instead of MyCallback? I don’t know if that would make it better or worse.
I’m using this library so I don’t have to write my own ringbuffer
@donutshoes, great, you are really moving forward!
The ringbuffer is what lets you access the audio_in outside of the audiocallback (ACB).
Now you have to synchronize the drawing of the OLED. You are filling the buffer at 48K, so the distance between each value is 1/48k s. How fast is the OLED? If it is “slow” you might have to skip values from the buffer.
In eaither case, I think I would decide upon an update speed of the OLED. Say like 1000 Hz. Then you have a small timing loop in main() where you every 1/1000 sec take 48 values and take the mean of them and use as the Y value.
You can use:
/** \return a uint32_t of microseconds within the internal timer. */
static uint32_t GetUs();
Ok so this is definitely performing better aside from an obvious bug that I cannot identify. As you can see, the oscilloscope works for a portion of the OLED screen and then seemingly uses a single y value for the rest of it.
Here’s the code:
#include <CircularBuffer.h>
#include <AudioClass.h>
#include <DaisyDSP.h>
#include <DaisyDuino.h>
#include <hal_conf_extra.h>
#include <U8g2lib.h>
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL, /* data=*/ SDA); // pin remapping with ESP8266 HW I2C
DaisyHardware hw;
CircularBuffer<float, 48> buf;
const unsigned long period = 1000; //period of time to perform an OLED update, in microseconds
const unsigned long countPeriod = 48; //averaging period, aka the number of incoming values to average
unsigned long previousTime, count, previousCount; //pieces of timer code
float avg; //average of samples
int lastSmpl; //used in OLED draw
size_t num_channels;
int displayWidth, displayHeight;
void MyCallback(float **in, float **out, size_t size) {
for (size_t i = 0; i < size; i++) {
for (size_t chn = 0; chn < num_channels; chn++) {
out[chn][i] = in[chn][i]; //bypass audio
avg += in[0][i]; //continuously add up incoming values
if (count - previousCount >= countPeriod) { //after 48 callback loops
avg /= countPeriod; //average the added values
buf.push(avg*2); //push to CircularBuffer, amplified
avg = 0; //reset to zero
previousCount = count; //reset timer
}
count++;
}
}
}
void setup(void) {
float samplerate;
// Serial.begin();
hw = DAISY.init(DAISY_SEED, AUDIO_SR_48K);
num_channels = hw.num_channels;
samplerate = DAISY.get_samplerate();
displayWidth = 128;
displayHeight = 32;
lastSmpl = 16; //keeps edge pixel in one place
u8g2.begin();
DAISY.begin(MyCallback);
}
void loop(void) {
unsigned long currentTime = micros();
if (currentTime - previousTime >= period) { //when the period has elapsed
lastSmpl = 16; //keeps edge pixel in one place
u8g2.clearBuffer();
for (int i = 0; i < displayWidth; i++) { //for every pixel the display is wide
updateOLED(i, buf.shift()); //shift() means get first value in buffer and remove from buffer, pass i and that value into updateOLED()
}
u8g2.sendBuffer();
previousTime = currentTime; //reset timer
}
}
void updateOLED(int x, float val) {
float y = (val + 1) / 2 * displayHeight; //map the incoming averaged value from -1, 1 to 0, displayHeight
float currentSmpl = constrain(y, 0, displayHeight); //create currentSmpl value from y and constrain to displayHeight
u8g2.drawLine(x, lastSmpl, x+1, currentSmpl); //draw a line from last sample to current sample
lastSmpl = currentSmpl; //reassign last sample as the current one
}
The OLED I’m using is 128x32
Again I am using the CircularBuffer library I linked above
Bear in mind some value types might be weird as I was testing some very large numbers to see how it reacted
Here are some observations while messing around with some values.
A) The CircularBuffer size correlates to a delay between when the working part of the oscilloscope moves and when the straight line moves. The larger the buffer is, the longer the delay.
B) The working portion of the oscilloscope lengthens the smaller the Averaging Period is, with some kind of limit. Bringing the Averaging Period down to 2 lengthens the working portion of the oscilloscope to a little less than half the OLED screen.
I’m guessing this is still some kind of sync issue between the callback loop and the OLED framerate? I tried messing around with the period of OLED updates but nothing significant seemed to change that I could tell.
Well hot damn, that’s an oscilloscope!!! Thanks so much for all your help with this, I learned a lot, both in coding fundamentals and how the Daisy works.
Here’s the final code I’m running:
#include <AudioClass.h>
#include <DaisyDSP.h>
#include <DaisyDuino.h>
#include <hal_conf_extra.h>
#include <U8g2lib.h>
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 32
#define CHUNK 6
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ SCL, /* data=*/ SDA); // pin remapping with ESP8266 HW I2C
DaisyHardware hw;
const unsigned long period = 1000; //period of time to perform an OLED update, in microseconds
unsigned long previousTime, currentTime;
float avg; //average of samples
int lastSmpl; //used in OLED draw
size_t num_channels;
float buf[DISPLAY_WIDTH];
uint8_t readptr, writeptr;
bool draw;
void AudioCallback(float **in, float **out, size_t size) {
for (size_t i = 0; i < size; i++) {
for (size_t chn = 0; chn < num_channels; chn++) {
out[chn][i] = in[chn][i]; //bypass audio
avg += in[chn][i]; //continuously add up incoming values
}
}
avg /= CHUNK;
buf[writeptr] = avg * 2;
writeptr++;
writeptr %= DISPLAY_WIDTH;
avg = 0; //reset to zero
draw = true;
}
void setup(void) {
float samplerate;
// Serial.begin();
hw = DAISY.init(DAISY_SEED, AUDIO_SR_48K);
num_channels = hw.num_channels;
samplerate = DAISY.get_samplerate();
readptr = 0;
writeptr = 0;
lastSmpl = 16;
draw = false;
u8g2.begin();
DAISY.SetAudioBlockSize(CHUNK);
DAISY.begin(AudioCallback);
}
void loop(void) {
unsigned long currentTime = micros();
if (draw) {
if (currentTime - previousTime >= period) {
u8g2.clearBuffer();
for (int i = 0; i < DISPLAY_WIDTH; i++) {
float y = buf[readptr];
int x = readptr;
updateOLED(x, y);
readptr += 1 % DISPLAY_WIDTH;
}
u8g2.sendBuffer();
draw = false;
previousTime = currentTime;
}
}
}
void updateOLED(int x, float val) {
float y = (val + 1) / 2 * DISPLAY_HEIGHT; //map the incoming averaged value from -1, 1 to 0, displayHeight
float currentSmpl = constrain(y, 0, DISPLAY_HEIGHT-1); //create currentSmpl value from y and constrain to displayHeight
u8g2.drawLine(x, lastSmpl, x + 1, currentSmpl); //draw a line from last sample to current sample
lastSmpl = currentSmpl; //reassign last sample as the current one
}
When I first ran this with a CHUNK size of 48, the values appeared to be updating quite slowly. Decreasing the CHUNK size sped it up. I don’t know if there’s a different way I could be doing this? I’m not really questioning it since it’s working
My next step is to somehow figure out how to implement this functionality, would love to have a pot I can turn to alter the scale. I’m somewhat confident (potentially overconfident) in my ability to figure it out, but any suggestions are welcome (I don’t mean to exploit folks’ generous brainstorming on here)