EV Synthesiser arrangement in project box.
EV Synthesiser arrangement in project box.

Introduction

A potential issue for electric vehicles and robotics in general is that they move relatively silently. This can pose safety issues when the vehicle/robot is in close proximity with people.

As an aside to my main UAV research project, the synthesiser explores how sounds can be created that relate to the movement of an electric vehicle or robot.

The prototype is extremely flexible and can incorporate sound samples, waveform tables and many effects to create a rich variety of sounds associated with traditional and granular synthesisers.  It uses a mix of low frequency, higher frequency and pink noise to assist in localisation and responds to both vehicle speed and acceleration.

The addition of extra sensors could permit the sound to reflect the more complex movement of robots.

Build

EV Synthesiser key components.
EV Synthesiser key components.

The audio is generated using a Teensy 3.1 and Teensy Audio Board, both available from pjrc.com.  They are stacked (audio board on top) on the left.

On the right is a 20W Class D Amplifier from Adafruit.

The Teensy is powered from the amplifier (which will be connected to the 12V), via the 5V regulator. During development, a 9V battery is supplying the amp.

The volume of the amp is software controlled using the I2C port, but it is also capable of being analogue controlled with a potentiometer.

Connected to the Teensy via UART is a standard gps unit, available from many radio control/UAV stores, such as HobbyKing.  It supplies the vehicle speed, which is then used to control aspects of the sound (volume, waveform frequencies etc.) .  For development, the blue potentiometer is connected to an analogue pin on the Teensy (A0) as a proxy for the speed.

EV Synthesiser with Audio Board dismounted.
EV Synthesiser with Audio Board dismounted.
EV Synthesiser topside connections.
EV Synthesiser topside connections.
EV Synthesiser stripboard connections.
EV Synthesiser stripboard connections.
EV Synthesiser project box.
EV Synthesiser project box.

It’s a bit of a squeeze, but everything fits into a box of size 110x60x35mm (excluding mounting tabs at each end).  The GPS sits in the top of the cover, as can be seen.

All worked well first time (which was a nice surprise), except no GPS signal.  Discovered I had managed to reverse the TX/RX lines from the breadboard version. So you can spot the grn/yel wires on the GPS connector reversed.

Power plugs in with a regular power jack so the whole thing can be easily removed, and the USB port is externally accessible as well for easy reprogramming in situ.

With the GPS working, I have switched over from dialling speed using the pot to detecting actual speed and acceleration using the GPS.

I took it out for a test spin in the car, with the system connected to the Aux input on the car stereo – just like the SoundRacer system.  All went well, so with the hardware largely finished, there’s now more time to think about the sound generation itself.  This will be the subject of future blogs, but one initial focus will be capturing samples and wavetables to use as grains for the granular synthesiser capabilities.

Sound Synthesis

The prototype was designed to produce a synthesised sound which:

  • responds to speed with frequency and pulsed wave forms;
  • contains a spectral spread to aid localisation (so pink noise so people can hear where it’s coming from);
  • has ‘onset noise’, again to aid localisation (so elements of  ‘chuff chuff’ instead of only continuous noise).

There is much flexibility in how to synthesise sounds using the Teensy Audio Board.  There is an excellent GUI tool at http://www.pjrc.com/teensy/gui/ which can be used to produce and connect the coding objects quickly.

For the code below, the GUI tool was used as follows:

http://www.pjrc.com/teensy/gui
http://www.pjrc.com/teensy/gui

The first two waveforms are the bass.  These are combined to form a slowly modulating rumble which is constant with speed, but which does vary with acceleration.

The second two waveforms are the midrange, again combined to form a modulated output.  However, these increase in frequency with speed.  Furthermore, there are combined with a little pink noise and then replayed repeatedly and with increasing frequency as speed increases.  So we get higher pitch and faster ‘chuffs’.  A significant part of the coding is dedicated to this aspect, ensuring that the envelope effect is controlled to prevent clipping as the frequency (of delivery) increases.

Coding

Credits go to Adafruit, PJRC (Paul Stoffregen) and Mikal Hart for libraries and code snippets.

———————————————————————————————————————————————–
/*
Software by Mike Isted.  April 2015.
Acknowledgments to PJRC, Adafruit, Paul Stoffregen, Mikal Hart
This software is shared ‘as is’ for educational purposes with no guarantees.
*/
#include
#include
#include
#include
#include

// GUItool: begin automatically generated code
AudioSynthWaveform       waveform2;      //xy=64,125.19998168945312
AudioSynthWaveform       waveform1;      //xy=65.39999389648437,56.40000915527344
AudioSynthNoisePink      pink1;          //xy=66.19999694824219,182.00006103515625
AudioSynthWaveform       waveform3;      //xy=71.79998779296875,242
AudioSynthWaveform       waveform4;      //xy=75.79998779296875,294.0000305175781
AudioEffectMultiply      multiply2;      //xy=207.80001831054687,260.3999938964844
AudioMixer4              mixer1;         //xy=355.7999267578125,206.00003051757812
AudioEffectMultiply      multiply1;      //xy=415,122.80000305175781
AudioEffectFlange        flange1;        //xy=510.20001220703125,208.39999389648437
AudioEffectEnvelope      envelope1;      //xy=603.7999877929687,260.3999328613281
AudioMixer4              mixer2;         //xy=739.7999877929687,179.60000610351562
AudioOutputI2S           i2s1;           //xy=1003.0001220703125,214.79998779296875
AudioConnection          patchCord1(waveform2, 0, multiply1, 1);
AudioConnection          patchCord2(waveform1, 0, multiply1, 0);
AudioConnection          patchCord3(pink1, 0, mixer1, 0);
AudioConnection          patchCord4(waveform3, 0, multiply2, 0);
AudioConnection          patchCord5(waveform4, 0, multiply2, 1);
AudioConnection          patchCord6(multiply2, 0, mixer1, 1);
AudioConnection          patchCord7(mixer1, flange1);
AudioConnection          patchCord8(multiply1, 0, mixer2, 0);
AudioConnection          patchCord9(flange1, envelope1);
AudioConnection          patchCord10(envelope1, 0, mixer2, 1);
AudioConnection          patchCord11(mixer2, 0, i2s1, 0);
AudioConnection          patchCord12(mixer2, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=82.99996948242187,475.60003662109375
// GUItool: end automatically generated code

//Part of setup for the Adafruit audio amp volume on the I2C port.
// 0x4B is the default i2c address
#define MAX9744_I2CADDR 0x4B

// Set the iniial volume level (must be between 0 and 63).
int8_t thevol = 40;

// Start instance of GPS
TinyGPS gps;

/* On Teensy, the UART (real serial port) is always best to use. */
/* Unlike Arduino, there’s no need to use NewSoftSerial because */
/* the “Serial” object uses the USB port, leaving the UART free. */
//HardwareSerial Uart = HardwareSerial();
#define HWSERIAL Serial1

// Prototype functions
// Function to print float variables to serial monitor
void printFloat(double f, int digits = 2);
void gpsdump(TinyGPS &gps);

// Declare global variables

float pot = 0;
float voltage = 0;
int analogin = 0;

int waveform1Freq = 90;
int waveform2Freq = 92;
int waveform3Freq = 180;
int waveform4Freq = 182;

// Variable for the vehicle speed.
// Ultimately this could be scaled according to the vehicle maximum, but for now
// we will run it from 0 to 100 arbritrary units.
float vspeed = 0;

// Variable for mid and pink frequency note repetitions – the frequency will range from 1 to 50Hz
// and is proportional to the speed of the vehicle.
float vfrequency = 1;
float lastvfrequency = 1;
float acceleration = vfrequency-lastvfrequency;
// Number of samples in each delay line
#define FLANGE_DELAY_LENGTH (6*AUDIO_BLOCK_SAMPLES)
// Allocate the delay lines for left and right channels
short lf_delayline[FLANGE_DELAY_LENGTH];
short rf_delayline[FLANGE_DELAY_LENGTH];

int s_idx = FLANGE_DELAY_LENGTH/4;
int s_depth = FLANGE_DELAY_LENGTH/4;
double s_freq = .5;
void setup() {

Serial.begin(9600);
HWSERIAL.begin(9600);
// Activate I2C port.
Wire.begin();
delay(2000);

Serial.println(“MAX9744 demo”);
if (! setvolume(thevol)) {
Serial.println(“Failed to set volume, MAX9744 not found!”);
while (1);
}

delay(2000);
Serial.print(“Testing TinyGPS library v. “);
Serial.println(TinyGPS::library_version());
Serial.println(“by Mike Isted”);
Serial.println();
Serial.print(“Sizeof(gpsobject) = “);
Serial.println(sizeof(TinyGPS));
Serial.println();
delay(2000);

// Give the audio library some memory.
AudioMemory(20);

// enable the audio shield
sgtl5000_1.enable();
//This affects the headphone out only.
sgtl5000_1.volume(0.7);

// Set up the pink noise generator.
pink1.amplitude(0.1);

// Set up the bass synth waveforms.
// The two bass are constant.
// The two mid-range increase frequency with speed.
waveform1.begin(0.3,waveform1Freq,WAVEFORM_SAWTOOTH);
waveform2.begin(0.3,waveform2Freq,WAVEFORM_SAWTOOTH);
waveform3.begin(0.5,waveform3Freq,WAVEFORM_SAWTOOTH);
waveform4.begin(0.5,waveform4Freq,WAVEFORM_SAWTOOTH);

// Set up the mixer gains.
// mixer2 is bass
//mixer1.gain(0,1);
// mixer2 is pink noise
//mixer2.gain(1,1);
// mixer3 is mid-range
//mixer1.gain(2,1);

// Set up the flange effect:
// address of delayline
// total number of samples in the delay line
// Index (in samples) into the delay line for the added voice
// Depth of the flange effect
// frequency of the flange effect
flange1.begin(lf_delayline,FLANGE_DELAY_LENGTH,s_idx,s_depth,s_freq);
//flange2.begin(rf_delayline,FLANGE_DELAY_LENGTH,s_idx,s_depth,s_freq);

Serial.println(“Setup complete”);
AudioProcessorUsageMaxReset();
AudioMemoryUsageMaxReset();
}
// Setting the volume is very simple! Just write the 6-bit
// volume to the i2c bus.
boolean setvolume(int8_t v) {
// cant be higher than 63 or lower than 0
if (v > 60) v = 60;
if (v < 0) v = 0;

Serial.print(“Setting volume to “);
Serial.println(v);
Wire.beginTransmission(MAX9744_I2CADDR);
Wire.write(v);
if (Wire.endTransmission() == 0)
return true;
else
return false;
}
void loop() {
Serial.print(“Entering main loop…\n”);
//  Declare variables
// Boolean to indicate new GPS data has arrived.
bool newData = false;

// The envelope variables (ms)
unsigned long totalTime = (1/vfrequency)*1000;
unsigned long delayTime = 1;
unsigned long attackTime = 1.5;
unsigned long holdTime = 0.5;
unsigned long decayTime = 10;
unsigned long releaseTime = (totalTime-decayTime-attackTime-holdTime-delayTime)/2;
unsigned long timeToNoteOff = (totalTime-releaseTime);

bool interrupt = false;

while (interrupt == false)  {  // So this would normally be an endless loop

//pot = analogRead(0);
//voltage = (pot/1024);
//Serial.print(” Voltage (speed/2): “); printFloat(voltage);
Serial.print(” vspeed: “); printFloat(vspeed);

lastvfrequency = vfrequency;
//vfrequency = ((voltage*49)+0.5);
vfrequency = (vspeed);
if (vfrequency < 0.5 ) { vfrequency = 0.5; } else if (vfrequency > 30) {
vfrequency = 30;
}

acceleration = vfrequency-lastvfrequency;

Serial.print(” vfrequency: “); printFloat(vfrequency);
Serial.println();

// Set up the envelope.
// The envelope takes the filtered signal and segments it into a periodic sound.
// Later we will initiate using noteOn and noteOff.
// noteOn initiates attack phase.  noteOff initiates release phase.
totalTime = (1000/vfrequency);
envelope1.delay(delayTime);
envelope1.attack(attackTime);
envelope1.hold(holdTime);
envelope1.decay(decayTime);
releaseTime = (totalTime-decayTime-attackTime-holdTime-delayTime)/2;
envelope1.release(releaseTime);
timeToNoteOff = (totalTime-releaseTime);

Serial.print(” totalTime: “); printFloat(totalTime);
Serial.print(” delayTime: “); printFloat(delayTime);
Serial.print(” attackTime: “); printFloat(attackTime);
Serial.print(” holdTime: “); printFloat(holdTime);
Serial.print(” decayTime: “); printFloat(decayTime);
Serial.print(” timeToNoteOff: “); printFloat(timeToNoteOff);
Serial.print(” releaseTime: “); printFloat(releaseTime);
Serial.println();

// So now we play the note and while this is going on, we check for new GPS data.

waveform1Freq = 90 – acceleration;
waveform1.frequency(waveform1Freq);

waveform3Freq = 180+(vfrequency*3);
waveform4Freq = 182+(vfrequency*3);
waveform3.frequency(waveform3Freq);
waveform4.frequency(waveform4Freq);

// Initiate note.
envelope1.noteOn();
unsigned long startTime = millis();
newData = false;

// Now loop to check for new gps data.
while ((millis() – startTime) < timeToNoteOff) { if (HWSERIAL.available()) { char c = HWSERIAL.read(); //Serial.print(c); if (gps.encode(c)) { newData = true; vspeed = gps.f_speed_mph(); Serial.println(); Serial.print(” (mph): “); printFloat(vspeed); Serial.println(); //Serial.println(“Acquired Data”); //Serial.println(“————-“); //gpsdump(gps); //Serial.println(“————-“); //Serial.println(); } } } // Initiate the release phase and allow a gap between the next note. envelope1.noteOff(); delay (releaseTime); if (Serial.available()) { // read a character from serial console char cvol = Serial.read(); // increase if (cvol == ‘+’) { thevol++; } // decrease else if (cvol == ‘-‘) { thevol–; } if (thevol > 60) thevol = 60;
if (thevol < 0) thevol = 0;

setvolume(thevol);
}

}
}
void gpsdump(TinyGPS &gps)
{
long lat, lon;
float flat, flon;
unsigned long age, date, time, chars;
int year;
byte month, day, hour, minute, second, hundredths;
unsigned short sentences, failed;

gps.get_position(&lat, &lon, &age);
Serial.print(“Lat/Long(10^-5 deg): “); Serial.print(lat); Serial.print(“, “); Serial.print(lon);
Serial.print(” Fix age: “); Serial.print(age); Serial.println(“ms.”);

// On Arduino, GPS characters may be lost during lengthy Serial.print()
// On Teensy, Serial prints to USB, which has large output buffering and
//   runs very fast, so it’s not necessary to worry about missing 4800
//   baud GPS characters.

gps.f_get_position(&flat, &flon, &age);
Serial.print(“Lat/Long(float): “); printFloat(flat, 5); Serial.print(“, “); printFloat(flon, 5);
Serial.print(” Fix age: “); Serial.print(age); Serial.println(“ms.”);

gps.get_datetime(&date, &time, &age);
Serial.print(“Date(ddmmyy): “); Serial.print(date); Serial.print(” Time(hhmmsscc): “);
Serial.print(time);
Serial.print(” Fix age: “); Serial.print(age); Serial.println(“ms.”);

gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &age);
Serial.print(“Date: “); Serial.print(static_cast(month)); Serial.print(“/”);
Serial.print(static_cast(day)); Serial.print(“/”); Serial.print(year);
Serial.print(”  Time: “); Serial.print(static_cast(hour)); Serial.print(“:”);
Serial.print(static_cast(minute)); Serial.print(“:”); Serial.print(static_cast(second));
Serial.print(“.”); Serial.print(static_cast(hundredths));
Serial.print(”  Fix age: “);  Serial.print(age); Serial.println(“ms.”);

Serial.print(“Alt(cm): “); Serial.print(gps.altitude()); Serial.print(” Course(10^-2 deg): “);
Serial.print(gps.course()); Serial.print(” Speed(10^-2 knots): “); Serial.println(gps.speed());
Serial.print(“Alt(float): “); printFloat(gps.f_altitude()); Serial.print(” Course(float): “);
printFloat(gps.f_course()); Serial.println();
Serial.print(“Speed(knots): “); printFloat(gps.f_speed_knots()); Serial.print(” (mph): “);
printFloat(gps.f_speed_mph());
Serial.print(” (mps): “); printFloat(gps.f_speed_mps()); Serial.print(” (kmph): “);
printFloat(gps.f_speed_kmph()); Serial.println();

gps.stats(&chars, &sentences, &failed);
Serial.print(“Stats: characters: “); Serial.print(chars); Serial.print(” sentences: “);
Serial.print(sentences); Serial.print(” failed checksum: “); Serial.println(failed);
}

void printFloat(double number, int digits)
{
// Handle negative numbers
if (number < 0.0) {
Serial.print(‘-‘);
number = -number;
}

// Round correctly so that print(1.999, 2) prints as “2.00”
double rounding = 0.5;
for (uint8_t i=0; i<digits; ++i) rounding /= 10.0; number += rounding; // Extract the integer part of the number and print it unsigned long int_part = (unsigned long)number; double remainder = number – (double)int_part; Serial.print(int_part); // Print the decimal point, but only if there are digits beyond if (digits > 0)
Serial.print(“.”);

// Extract digits from the remainder one at a time
while (digits– > 0) {
remainder *= 10.0;
int toPrint = int(remainder);
Serial.print(toPrint);
remainder -= toPrint;
}
}

Advertisements