Tags

, , , , , ,

For a personal project, I needed a device that could play distorted audio and take cues, as described by a previous post. With hardware selected and some idea of what the function is to be, it is time to write some firmware.

I used the Teensy Audio library and a new synthesized signal source for the primary signal path. Recorded sounds came from uncompressed 16-bit PCM WAV files stored on a µSD card via the SD library. I also used the Bounce library to handle a couple of push buttons for cuing.

Background

The selected hardware is an ARM CORTEX M4 running at 96 MHz, providing a beefy platform for audio sample rate signal processing. It can handle both synthesis and playback of stereo CD quality audio with ease. With appropriate jumper configurations, playback of quadraphonic audio is practical with two Teensy http://adapterst3a. I only need monophonic audio, so even the single adapter I used provides more capability than I need.

In addition to the audio adapter and rechargeable battery power source described in the earlier post, I swiftly realized that I needed both a true power switch and mechanical switches for queuing.

A power switch was easy to insert in the battery positive lead as a simple low voltage and current toggle switch from the spare parts bin. In that position, it is able to turn the Teensy off even when the USB is connected while allowing the battery to charge. This is convenient for charging the battery without making noise.

For cuing, I simply connected micro-switches from Teensy pins 28 and 29 to GND. I used fine gauge solid wire routed through a slot cut in an inconspicuous place in the prop’s housing, with the switches glued to the outside. Black switches glued to a black part are well enough camouflaged. A future refinement would be to use the touch sensor library and conceal the sensors even more completely.

Make some noise

The Teensy has been well-integrated into the Arduino IDE, despite being a completely different CPU architecture. This makes it easy to get started, once the Arduino IDE and the Teensy enhancements in Teensyduino are installed.

I began with the PlaySynthMusic sample to prove that the hardware was all functioning. After getting the battery set up, I let that run (playing the “William Tell Overture” on continuous repeat, at least it wasn’t “Its a Small World”) for more than an hour on battery, demonstrating that the 500mAh battery I had available was going to run longer than I expected to need in performance.

I next moved to the WavFilePlayer sample to prove that the µSD card I had available would work, and that I could prepare WAV files it could play. The basic sample plays several files with fixed names from the root of the card, in stereo, with no effects. My final firmware plays one file in a loop mixed with various effects that change continuously, as well as other files with less modification from time to time.

The Audio library provides a remarkably flexible mechanism for describing a chain of process steps and effects applied to audio streams. There is even a GUI designer that allows the process to be visualized.

The library provides blocks for WAV file input, mixers, effects, filters, arbitrary waveform generation, white and pink noise, and, of course, various outputs.

No, really, noise

To get an old radio tuning effect, I started with pink noise, and filtered it with a band-pass filter, with its center frequency modulated by a slow random walk. All of these pieces, except the random walk, are stock components in the Audio library.

The pink noise source is full spectrum 1/f noise, which sounds more “authentic” than white noise, but is also to static and unchanging in its sound. Pulling a somewhat narrow band out of it with a soft band-pass filter makes it sound more like an empty seashell.

Modulating the center frequency of the pass band makes the shell change size. At the low end, the pink noise has a lovely rumbling quality. I mocked up the concept using a sine wave as the modulation, but that too was too predictable. So I bit the bullet and implemented a simple low update rate red noise based on a random walk of sample values.

The slow random walk is implemented by a very simple class inherited from AudioStream:

class AudioSynthSlowWalk : public AudioStream
{
  public:
    AudioSynthSlowWalk() : AudioStream(0,NULL),
        _stepsize(1.0), _value(0.0) {};
    virtual void update(void);
    void stepsize(double s) { _stepsize = s; };

  private:
    double _stepsize;
    double _value;
};
void AudioSynthSlowWalk::update(void) {
  if (_stepsize == 0.) return;
  audio_block_t *block = allocate();
  if (!block) return;
  _value += _stepsize * (2.*rand()-RAND_MAX)/RAND_MAX;
  if (_value > 32767.) _value = 32767.;
  if (_value < -32767.) _value = -32767.;
  short sv = (short)_value;
  for (int i=0; i<AUDIO_BLOCK_SAMPLES; i++) {
    block->data[i] = sv;
  }
  transmit(block,0);
  release(block);
}

There is only one state variable: _value. It is adjusted by a random amount for each block requested from the stream, and the block filled with that value. The size of the adjustment ranges from -_stepsize to _stepsize, the one control parameter. Call AudioSynthSlowWalk::stepsize(double) to adjust it from its default value of 1.0 (a very slow walk indeed).

A refinement that could be made here is to interpolate from the the previous value to the new value rather than filling the sample buffer with a constant. At 44100 Hz sample rate and 128 samples per buffer, the value is adjusted at just over 344 Hz, so the sharp edges per block could be audible as a 344 Hz overtone, making some refinement worthwhile.

Another refinement would be to use a random number source other than rand(). Its primary defect is that it is not thread-safe, and uses private storage for its state. A better choice would allow its state to be stored in each AudioSynthSlowWalks object so that they each had an independent stream of random numbers.

With the random walk noise source working, an eerie random tuning sound is readily formed by filtering a pink noise source with a state variable filter whose center frequency is modulated by the walk.

Waves of WAV files

The builtin AudioPlaySdWav class supplies a stream of samples from a named .WAV file on cue.

Its full capabilities are unclear, but it has been demonstrated to produce left and right sample streams from stereo 16-bit PCM at 44100 Hz. If storage space were a concern, the 10 MB/min this implies might become a problem. However, since the days when the CD was invented, storage has become remarkably cheap. The largest µSD card available that is compatible with the libraries is 32GB or something more than 52 hours of audio. A more pressing limit is the FAT file systems which has a hard limit of 4GB files. A single file then can hold more than six and a half hours, which is likely longer than the battery capacity.

I could just ignore the right channel, but the library makes a simple mid-side encoder/decoder object available which will trivially produce a balanced mix of left and right.

Playing a .WAV file is as simple as telling the player object its name, keeping in mind that the Arduino’s SD access library is restricted to using only 8.3 FAT file names. The player sample code included this handy function which starts the file playing, then waits for it to complete. While waiting, it polls the volume control knob and allows it to be adjusted. The rest of the audio pipeline is also running, so the file’s audio stream is being summed to mono, then mixed with the random walk tuned noise, and piped out to both channels of the audio adapter.

void playFile(const char *filename)
{
  // Start playing the file.  This sketch continues to
  // run while the file plays.
  playWav1.play(filename);

  // A brief delay for the library read WAV header info
  delay(5);

  // Simply wait for the file to finish playing, 
  // adjusting volume as it goes.
  while (playWav1.isPlaying()) {
    float vol = analogRead(15); // volume pot
    vol = vol / 1024;
    sgtl5000_1.volume(vol);
  }
}

If a blocking call were inconvenient, it would be simple enough to test playWav1.isPlaying() in the loop() function instead.

Because the Audio library is implementing all the interesting processing, the audio presented in the files needs only a minimal amount of pre-production effort. I recorded it in a quiet room using the mic built in to my monitor, figuring that I was going to mix it with noise anyway.

I used Audacity to make the recording, and then did some minimal edits to get rid of a couple of clicks and pops, trim some of the silences shorter, and auto-level the recording. If I’d used other voice talent, I might have preferred to use a better quality microphone, and take some additional care with the room and obvious sources of noise.

All plumbed up

The GUI designer for the Audio library nicely draws all the connections, and transforms them into code declaring all the objects and wires. Unfortunately, it doesn’t appear to support including custom objects (like my random walk noise source) in the diagram. I used an AudioSynthWaveformSine as a stand-in then edited it after export to my source file.

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioPlaySdWav           playWav1;       //xy=114,458
AudioSynthSlowWalk       walk1;          //xy=117,583
AudioSynthNoisePink      pink1;          //xy=122,514
AudioFilterStateVariable filter1;        //xy=277,517
AudioEffectMidSide       midside1;       //xy=285,459
AudioMixer4              mixer1;         //xy=486,480
AudioOutputI2S           audioOutput;    //xy=674,485
AudioConnection          patchCord1(playWav1, 0, midside1, 0);
AudioConnection          patchCord2(playWav1, 1, midside1, 1);
AudioConnection          patchCord3(walk1, 0, filter1, 1);
AudioConnection          patchCord4(pink1, 0, filter1, 0);
AudioConnection          patchCord5(filter1, 1, mixer1, 1);
AudioConnection          patchCord6(midside1, 0, mixer1, 0);
AudioConnection          patchCord7(mixer1, 0, audioOutput, 0);
AudioConnection          patchCord8(mixer1, 0, audioOutput, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=678,574
// GUItool: end automatically generated code

The Audio library internally uses the AudioConnection object to pump blocks of samples from outputs to inputs. The AudioOutputI2S object is the I2S port wired to the audio adapter and needs no further configuration. The AudioControlSGTL5000 object represents the controls of the codec chip on the adapter. We’ll primarily use it for the volume control, but it needs to be present so that it can initialize the chip for us.

void setup() {
  Serial.begin(9600); // for debug output

  // Audio connections require memory to work.  For more
  // detailed information, see the MemoryAndCpuUsage example
  AudioMemory(12);

  // enable the audio board
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.5);

  // set some initial levels
  noise1.amplitude(0.5);
  mixer1.gain(0, 0.5);
  mixer1.gain(1, 1.0);
  filter1.frequency(1000.);
  filter1.resonance(0.707);
  filter1.octaveControl(3.5); // sensitivity to modulation
  walk1.stepsize(2000.); // tweak for "tuny" enough sound

  // bring up the SD card, which must be present
  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here, but print a message repetitively
    while (1) {
      Serial.println("Unable to access the SD card");
      delay(500);
    }
  }

  SkipButton.update();
  GoButton.update();
}

Cues? What cues?

At the last minute, I remembered that I needed to be able to nudge the prop. The key feature I wanted was the ability to make it be silent until poked again. I added two buttons to the hardware setup, and glued them down to an inconspicuous location on the prop itself.

The buttons are wired to Teensy digital pins 28 and 29, such that the pin is grounded when the button is pushed. The internal pull-up is used to restore the level and minimize the external components required.

The Bounce library easily handles contact bounces as long as its .update() method is called often enough. I declared global objects for each button, and then initialized the pin mode to guarantee the pull-up is present in setup(). To allow for silence, I declared a global state flag Silent which will be TRUE when the prop is silent.

Bounce SkipButton = Bounce(28, 10);
Bounce GoButton = Bounce(29, 10);
int Silent = 0;
void setup() {
  //...
  pinMode(28,INPUT_PULLUP);
  pinMode(29,INPUT_PULLUP);
  SkipButton.update();
  GoButton.update();
  //...
}

In playFile() I added a check of each button inside the loop waiting for the playback to complete. Here’s how that looks for the Go button:

void playFile(const char *filename)
{
  //...
  while (playWav1.isPlaying()) {
    //...
    if (GoButton.update() && GoButton.fallingEdge()) {
      playWav1.stop();
      Silent = 1;
      break;
    }
  }
}

When the Go button is pressed during playback, the currently playing file stops playing immediately and the global Silent flag is set.

A similar pattern is used to notice the Skip button, with details left to the imagination.

The rest of the work is done in the loop() function, which had been rather boring before: all it really did was play the prop’s audio track by calling playFile() with its file name. Now, it handles the Silent state too.

void loop() {
  if (Silent) {
    AudioNoInterrupts();
    playWav1.stop();
    sgtl5000_1.volume(0);
    AudioInterrupts();
    if (GoButton.update() && GoButton.fallingEdge()) {
      Silent = 0;
    }
    return;
  }
  AudioNoInterrupts();
  mixer1.gain(0,0.50);
  mixer1.gain(1,1.0);
  AudioInterrupts();
  playFile("BACKGRND.WAV");  // always uppercase 8.3 names
  delay(250);
}

Summary

I needed to make some sounds from inside a prop without much room to spare. A commercially available ARM CORTEX M4 module provided all the audio DSP horsepower needed and fits inside the opening.

Obviously I’m not describing all the features that went in to this prop. My purpose here was to provide a nudge towards a way to achieve procedural sound effects running from battery in a small space, not to provide a blueprint for a specific prop.


If you have a project involving embedded systems, micro-controllers, electronics design, audio, video, or more we can help. Check out our main site and call or email us with your needs. No project is too small!

+1 626 303-1602
Cheshire Engineering Corp.
710 S Myrtle Ave, #315
Monrovia, CA 91016

(Written with StackEdit.)