Creating arbitrary waveform

mb1234

Member
Hi all,

I am trying to feed arbitrary waveforms via USB audio to a Mac.
The USB interface and everything works. I am attempting to use the AudioSynthWaveformDc module to set the amplitude of a computed arbitrary waveform.
This also works, but the amplitude only updates every 3ms when setting it via dc1.amplitude().

Here's the code I am using:

Code:
#include <Audio.h>

// GUItool: begin automatically generated code
AudioOutputI2S i2s1;  // IMPORTANT! If this is removed then no audio will be output via USB. No idea why?!
AudioSynthWaveformDc     dc1;            //xy=196,184
AudioOutputUSB           usb1;           //xy=371,185
AudioConnection          patchCord1(dc1, 0, usb1, 0);
AudioConnection          patchCord2(dc1, 0, usb1, 1);
// GUItool: end automatically generated code

void setup() {
  AudioMemory(12);
}

uint8_t count = 0;

void loop() {
  dc1.amplitude(float(count)/256);   
  count++;
  
  delay(1);
}

As expected I get a sawtooth at roughly 4 Hz (1000/256). But when I zoom in I see, that the amplitude of the signal only changes every 3ms.
Not every 1ms as I would expect from the code (due to the delay(1);).
It appears, that values set using dc1.amplitude() only update once every 3ms.
Is this the intended behavior?
Is there a better way to achieve what I am trying to do? (I am not actually trying to generate a sawtooth. I want to need able to manually set the output level to arbitrary values.)

Thanks in advance.
Mike
 

Attachments

  • dcGeneration1.jpg
    dcGeneration1.jpg
    158.8 KB · Views: 2
  • dcGeneration2.jpg
    dcGeneration2.jpg
    135.8 KB · Views: 3
Depends what your aim is, but either AudioPlayQueue or AudioSynthWaveform using the arbitraryWaveform option may suit your needs. They’re documented in the Design Tool. Do come back if you can’t see how to do what you need, though we’ll probably need a bit more info on your aims in order to help effectively.
 
Thanks for pointing me to the AudioPlayQueue. This should do what I want.
Unfortunately I haven't gotten it working yet.

I reduced the "PlayQueueDemo.ino" to its basics. I appears to be working correctly when I use a AudioOutputPWM as output.
However, if I use AudioOutputUSB as output it does not. The "nulls" counter increases on every loop and it looks like there's no Audio buffer available.

Code:
/*
 * A simple queue test which generates a sine wave in the
 * application and sends it to headphone jack. The results
 * should always be a glitch-free wave, but various options
 * can be changed to modify the usage of audio blocks, and an
 * ability to execute the loop() function more slowly, with
 * simple waveform generation, or more efficiently / faster,
 * but requring some programmer effort to re-try if sending
 * waveform data fails due to a lack of buffer or queue space.
 *
 * This example code is in the public domain.
 *
 * Jonathan Oakley, November 2021
 */
 
 #include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioPlayQueue           queue1;         //xy=917,329
AudioOutputUSB           usb1;           //xy=1121,330
AudioOutputPWM           pwm1;
AudioConnection          patchCord1(queue1, 0, usb1, 0);
AudioConnection          patchCord2(queue1, 0, usb1, 1);
// GUItool: end automatically generated code

#define COUNT_OF(a) (sizeof a / sizeof a[0])

uint32_t next;
void setup() {
  pinMode(13,OUTPUT);
 
  Serial.begin(115200);
  while (!Serial)
    ;

  if (CrashReport)
  {
    Serial.println(CrashReport);
    CrashReport.clear();
  }

  AudioMemory(10);

  next = millis();
 
  // Comment the following out (or set to ORIGINAL) for old stall behaviour;
  // set to NON_STALLING for return with status if audio blocks not available,
  // or no room in queue for another audio block.
  queue1.setBehaviour(AudioPlayQueue::NON_STALLING);

  queue1.setMaxBuffers(4);
}


/*
 * Generate one sample of a waveform.
 * Currently 220Hz sine wave, but could make it more complex.
 */
uint32_t genLen;
int16_t nextSample()
{
  static float phas = 0.f;
  int16_t result = (int16_t) (sin(phas)*8000.);
  genLen++;
 
  phas += 220./AUDIO_SAMPLE_RATE_EXACT*TWO_PI;
  if (phas > TWO_PI)
    phas -= TWO_PI;

  return result;
}


int loops;
int nulls,nulls2;
int testMode = 2; // 1: getBuffer / playBuffer; 2: play(), mix of samples and buffers
int playMode; // 1: generate individual samples and send; 2: generate buffer of samples and send
int16_t samples[512],*sptr; // space for samples when using play()
uint32_t len; // number of buffered samples (remaining)

void loop() {
  len = 10;
  while (len > 0)
  {
    if (0 == queue1.play(samples[0])) { // sent a sample...
//      Serial.println(samples[0]);
      len--;        // count down
      if (len > 0)  // if more to send...
        samples[0] = nextSample(); // ...create the next one  
    }
    else
    {
      nulls++; // count up the re-tries
      break;
    }
  }

  loops++;
  if (millis() > next)
  {
    next += 100; // aim to output every 100ms
   
    // In NON_STALLING mode this loops really fast, and the millis() value goes up by
    // 100 on every output line. In ORIGINAL mode the loop is slow, and the internal
    // stall results in slightly unpredictable timestamps.
    Serial.printf("%d: millis = %d, loops = %d, nulls = %u, nulls2 = %u, samples = %u\n",
                  playMode,millis(),loops,nulls,nulls2,genLen);
  }
}


Any idea what I'm doing wrong? Can the AudioOutputUSB not keep up?
 
Actually my previous post was not quite right.
There seems to be some other bug, that if you ONLY have a AudioOutputUSB no output will be created.
You need to have some other output instantiated.
Which is why I added
Code:
AudioOutputI2S i2s1;  // IMPORTANT! If this is removed then no audio will be output via USB. No idea why?!
to my previous code.

If I add that here (as I did above) I get the following on the USB output
 

Attachments

  • brokenSine.jpg
    brokenSine.jpg
    43.3 KB · Views: 2
There seems to be some other bug, that if you ONLY have a AudioOutputUSB no output will be created.

Please check which version of Teensyduino you have installed. If using Arduino IDE 2.x.x, click Boards Manager and type "teensy" in search. If using Arduino IDE 1.8.x, click Help > About.


AudioOutputI2S i2s1; // IMPORTANT! If this is removed then no audio will be output via USB. No idea why?!

Indeed this was needed with older versions.

Before anyone pours a lot of time into investigating the problems you're finding, please at least confirm you're using the latest version. Today that's 1.59.
 
Thanks Paul.

Good point.
After updating to the latest I no longer need the other output device.
While things have improved, there are still some gaps.

I have simplified the code even more:
Code:
#include <Audio.h>

AudioPlayQueue           queue1;        
AudioOutputUSB           usb1;          
AudioConnection          patchCord1(queue1, 0, usb1, 0);
AudioConnection          patchCord2(queue1, 0, usb1, 1);

void setup() {
 
  Serial.begin(115200);
  AudioMemory(10);

  queue1.setBehaviour(AudioPlayQueue::NON_STALLING
);
  queue1.setMaxBuffers(4);
}
int16_t nextSample()
{
  static float phas = 0.f;
  int16_t result = (int16_t) (sin(phas)*8000.);
 
  phas += 220./AUDIO_SAMPLE_RATE_EXACT*TWO_PI;
  if (phas > TWO_PI)
    phas -= TWO_PI;

  return result;
}

int16_t sample = 0;
void loop() {
  sample = nextSample();
  queue1.play(sample);
}
 

Attachments

  • screenshot.png
    screenshot.png
    914.6 KB · Views: 3
Have you tried to generate a whole buffer (4*128) or even a single 128 words block of samples at once and only then play it?
 
I have not.I will be getting sensor data in at around 4kHz.
I was hoping there's an easy way to "just pipe" that into the USB audio output device in order to capture it on the host side.
I suppose I could buffer up the incoming data until I have enough for a whole buffer.
But in that case I would need to get the timing just right and repeat the last incoming sample n times until I receive a new one.
 
Which Teensy model are you using?

I see you're doing float math as 64 bit double, even though you store the results into 32 bit float. Especially if using Teensy 3.2 which lacks FPU, use sinf() rather than sin().
 
I was using a 3.2
I now switched over to using a 4.0
The same code now gives me a clean sine wave
 

Attachments

  • screenshot.png
    screenshot.png
    874 KB · Views: 6
I have not.I will be getting sensor data in at around 4kHz.
I was hoping there's an easy way to "just pipe" that into the USB audio output device in order to capture it on the host side.
I suppose I could buffer up the incoming data until I have enough for a whole buffer.
But in that case I would need to get the timing just right and repeat the last incoming sample n times until I receive a new one.
There's no avoiding your having to do some work in your code to match your incoming rate of "around" 4kHz to the audio system's "around" 44.1kHz (nothing will be exact...). You'll need to put 11 copies of each sample into the audio queue, except occasionally it'll be 12, in order to match incoming and outgoing rates.

I'm slightly amazed you're now getting a clean sine wave, based on your code in post #7. The NON_STALLING return from AudioPlayQueue::play(sample) will be non-zero if you have filled the queue with data - see the Design Tool documentation. As your loop() is running as fast as possible, and not checking the return value, I'd expect it to rapidly fill up the 4 blocks / 512 samples the queue is allowed, then create (and lose) samples it can't queue until a block is taken from the queue, upon which the next 128 samples will be queued and another interval of lost samples will occur.

For a sample rate of 4kHz, you might as well use the USB serial and ASCII coding - the Teensy and USB are easily fast enough to cope.
 
I think it's pure coincidence that the timing of calculating the next sample happens to work out such that the sine wave is clean.

Agreed. I will have to do some sort of housekeeping with. the data coming in.
I was hoping that there'd be some mode in which the last sample is simply repeated until a new sample is supplied.
I see now, that that is not how the library works as it expects buffers to be filled, which are piped out at a fixed rate.

The main reason of using an Audio interface is that it allows me to easily use existing tools for recording, filtering, FFT, etc on the host.
 
One crude way of resample it would be to simply use a DC component and update it from whatever the sensor source is. DC outputs the same value until changed. Maybe add a lowpass @ 2kHz if the input f is around 4kHz.
 
I think it's pure coincidence that the timing of calculating the next sample happens to work out such that the sine wave is clean.
Presumably, but I'm quite surprised!

Agreed. I will have to do some sort of housekeeping with. the data coming in.
I was hoping that there'd be some mode in which the last sample is simply repeated until a new sample is supplied.
I see now, that that is not how the library works as it expects buffers to be filled, which are piped out at a fixed rate.

The main reason of using an Audio interface is that it allows me to easily use existing tools for recording, filtering, FFT, etc on the host.
At any moment you can expect someone who knows about these things to pop up and blind us with the science of properly-implemented sample rate conversion. "Simply repeating until a new sample is supplied" definitely isn't it, especially if you're planning on doing the processing you describe.
 
One crude way of resample it would be to simply use a DC component and update it from whatever the sensor source is. DC outputs the same value until changed. Maybe add a lowpass @ 2kHz if the input f is around 4kHz.
That’s exactly what my first post is about.
The problem with this is, that the output is only updated every 2.9ms; so you won’t get more than 344Hz this way.

Simply repeating until a new sample is supplied" definitely isn't it, especially if you're planning on doing the processing you describe
I’m fully aware that this is not the “correct“ way to resample, especially audio.
Having dug into the inner workings of the library I now understand why the library doesn’t work that way.

I’ll have to implement my own buffering and resampling if I want to use this library.
 
Back
Top