Play audio with midi clock

Status
Not open for further replies.
Hi everyone,
I have just started using Teensy after years of pics and Arduino, loving the the ability to work with audio!

I have just been trying to see if its possible to play an audiofile along side a synced midi clock. I figured it could be accurately done by adapting the "Blink While Playing" sketch and grabbing the time from positionMillis() and then using this to send a clock message through a midi breakout (which I have had working in another sketch just using a delay). But when I recreate it with a serial debug it seems the messages are not every 21ms . There is lots of skipping so maybe this is not a good way of doing it at all.

Start playing
Playing, now at 63 ms
Playing, now at 84 ms
Playing, now at 147 ms
Playing, now at 252 ms
Playing, now at 336 ms
Playing, now at 420 ms
Playing, now at 504 ms
Playing, now at 588 ms
Playing, now at 609 ms
Playing, now at 693 ms
Playing, now at 777 ms
Playing, now at 861 ms


If anyone has tried to do this or knows a good way to sync the audio with a midi clock I would love to know.

Code is below.

Many thanks

Simon




void loop() {
if (playSdWav1.isPlaying() == false) {
Serial.println("Start playing");
playSdWav1.play("HELLO2.WAV");
delay(10); // wait for library to parse WAV info
}



if (playSdWav1.positionMillis()%21 == 0 && clockSent == 0) {
// print the play time offset
Serial.print("Playing, now at ");
Serial.print(playSdWav1.positionMillis());
Serial.println(" ms");

clockSent = 1;
}


if (playSdWav1.positionMillis()%22 == 0) {
// clock
clockSent = 0;
}



// read the knob position (analog input A2)
/*
int knob = analogRead(A2);
float vol = (float)knob / 1280.0;
sgtl5000_1.volume(vol);
Serial.print("volume = ");
Serial.println(vol);
*/
}
 
My guess is the serial print stuff is still running during the 21st millisecond most of the time.

Normally with running counters you check inequalities because you can't be sure the condition will be checked while an equality holds.

Serial monitor stuff being IO is likely orders of magnitude slower than setting variables and doing math.

Switching to a test system where the results are reported afterwards (a counter increasing every time the modulus condition is met and then afterwards compared to the actual time lapsed) would give you a better indication on how often it might fail under more normal circumstances but you still should move to inequalities.



I'm not sure how you would do this with modulus division other than maybe a lag value and read 'clock' whenever the current value is less than the lag.



It's not entirely clear what you are actually trying to do. How does this get you towards a sync'd clock?
 
Last edited:
Thanks that is really helpful... I will look into a different way of timing it.

Basically I have another Teensy programmed up as a slave sequencer which is working really well.

I can play an audio track from logic and the teensy responds to the clock and the start stop messages.

I want this teensy to be able to play the audio file and send out a master clock as well. So basically replacing my logic setup.

Hope that is clearer.

Best wishes

Simon
 
Here is how I did a simple midi clock which works pretty well. I am basically trying to recreate this with an audio track playing at the same time. So they are both locked in together. But now I am a bit unsure how best to do it.

Thanks again
Simon

const int buttonPin2 = 2; // the number of the pushbutton pin
int buttonState2 = 1;
int timedelay = 21;
bool buttonFlag2 = LOW;

void setup() {
// Set MIDI baud rate:
Serial1.begin(31250);
pinMode(buttonPin2, INPUT_PULLUP);


}

void loop() {
Serial1.write(0xf8);
delay(timedelay);

buttonState2 = digitalRead(buttonPin2);
// check if the pushbutton is pressed.
// if it is, the buttonState is HIGH:
if (buttonState2 == LOW && buttonFlag2 == LOW) {
//timedelay = 10;
Serial1.write(0xfa);
// just do it once
buttonFlag2 = HIGH;
}
else {

buttonFlag2 = LOW;

}




}
 
The playback module has an elapsed playback-time method - positionMillis() - you can call to get the current mSec.

With each clock message you can increment a threshold by the duration of one midi-clock cycle in mS (21 in your example) and that becomes the test condition for the next position reading.

The test condition needs to be an inequality so:

Code:
if (positionMillis()>=threshold) {
   threshold = threshold + clockInterval; // bump threshold up by one clock duration
   usbMIDI.sendRealTime(usbMIDI.Clock);
}

I've use the usbMIDI call but I think the regular MIDI library has something similar.

Not sure what your hex-write to Serial1 does.
 
That looks better

Thanks so much for helping me out with this.

MIDI beat clock defines the following real time messages:

clock (decimal 248, hex 0xF8)
start (decimal 250, hex 0xFA)
continue (decimal 251, hex 0xFB)
stop (decimal 252, hex 0xFC)

So I have been happily using those. So I just wanted to send out 24 hex 0xF8 per quarter note as the midi spec.

I guess I need to refer to my playing track with the threshold which like you say is set at 21. Where do I set clockInterval. ?

if (playSdWav1.positionMillis()>=threshold) {
threshold = threshold + clockInterval; // bump threshold up by one clock duration
//usbMIDI.sendRealTime(usbMIDI.Clock);
}

Thanks again

Simon
 
Hi
I just tried this which I think is closer to the suggestion.

interval = 21

if (playSdWav1.positionMillis() - previousMillis >= interval) {
previousMillis = playSdWav1.positionMillis();
Serial1.write(0xf8);
}

It definitely sounds more stable but it is running at about 110 bmp rather than where I want to be which is 120bmp. Now I am not sure if what I am doing is possible because I am never going to be able to match tempos.

Even if I got it perfect which its not, my choice of bmp would be really limited as the maths wouldn't add up between clocking ms and the bpm I am after.

Is there a way to do it so that if I want 120 bmp I can pretty much get it bang on. So 120 bmp would be
500/24 which would be 20.83333333 ?

Thanks
Simon
 
I think with that method you accumulate error -- even when the intervals are exact.

You're moving the goal posts for the previous step to where you landed rather than where it should have been. So the timing error is cumulative.

You can do the calculations as microseconds and scale the position readings to match but you need an absolute scale so that means calculating the position the clock should fire at and then fire as close to that as circumstances allow.
 
Thank you.

I just tried this as a test... I don't know why I didn't start with it.

Without the wav playing stuff its great... a solid 120 bmp. But when thats in there it drops down to about 100bpm. So not so good. I had a feeling the audio playing happened more in the background and did not impact the running of the loop. Unless there is a cleverer way around it. Thinking I may need two teensys now one as an audio play and one as a clock, but I bet there is a way around it.

Cheers
Simon



void loop() {

if (playSdWav1.isPlaying() == false) {
// Serial.println("Start playing");
playSdWav1.play("HELLO2.WAV");
delay(20); // wait for library to parse WAV info
// send a start message
// Serial1.write(0xfa);
}

// send a midi clock signal 2
Serial1.write(0xf8);
delayMicroseconds(20833);


}
 
...Now I am not sure if what I am doing is possible because I am never going to be able to match tempos.
Yeah there is still a begged question of '...and then what'. Even if you can generate a midi clock along with an audio playback to match any tempo... what tempo... what if it's played in loose tempo or changing tempo? Where's the downbeat?

Not really sure where you are headed.
 
You have delay in your loop... that can't work. You must let the processor do its thing every code pass as fast as possible and ask it on each one whether it's time for your event yet.
 
Last edited:
Yeah there is still a begged question of '...and then what'. Even if you can generate a midi clock along with an audio playback to match any tempo... what tempo... what if it's played in loose tempo or changing tempo? Where's the downbeat?

Not really sure where you are headed.

Hi Oddson,

Yes sorry should have been clearer about that. The application is an installation where are want to keep some things synced together. I found that a good way to do this was to have my sequencer as a slave and then control it from a master with the audio and the clock from one source. So I as I mentioned that worked out well coming from my mac in Logic.

So everything is quantized to the grid at 120bmp, the start of the track should be the first downbeat.


I thought that delay would be fine as for the most part the rest of the code is just going to confirm that the track is playing and be ignored.

Thanks
 
Hi thanks for the advice with this one.
I have come up with this solution. Its seems pretty accurate. I am pretty sure it could be improved as in logic I played about
a bit with delaying the clock to compensate for latency.

Any this should be ok for me to work with but if you think of any improvements they would be more than welcome.

Code:
#include <TimerOne.h>
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

AudioPlaySdWav           playSdWav1;
AudioOutputI2S           audioOutput;
AudioConnection          patchCord1(playSdWav1, 0, audioOutput, 0);
AudioConnection          patchCord2(playSdWav1, 1, audioOutput, 1);
AudioControlSGTL5000     sgtl5000_1;

// Use these with the Teensy Audio Shield
#define SDCARD_CS_PIN    10
#define SDCARD_MOSI_PIN  7
#define SDCARD_SCK_PIN   14

// Use these with the Teensy 3.5 & 3.6 SD card
//#define SDCARD_CS_PIN    BUILTIN_SDCARD
//#define SDCARD_MOSI_PIN  11  // not actually used
//#define SDCARD_SCK_PIN   13  // not actually used

// Use these for the SD+Wiz820 or other adaptors
//#define SDCARD_CS_PIN    4
//#define SDCARD_MOSI_PIN  11
//#define SDCARD_SCK_PIN   13

bool clockSent = 0;
int threshold = 21;
long interval = 21;
long previousMillis = 0;

void setup() {
  Serial1.begin(31250);
  AudioMemory(8);
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.8);
  sgtl5000_1.adcHighPassFilterDisable();
  sgtl5000_1.lineInLevel(0, 0);

  
  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    while (1) {
      //Serial.println("Unable to access the SD card");
      delay(500);
    }
  }
  Timer1.initialize(20833);
  Timer1.attachInterrupt(sendClock); // blinkLED to run every 0.15 seconds
  
  delay(1000);
}


void sendClock(void)
{
 Serial1.write(0xf8);
 
 if (playSdWav1.positionMillis() < 1) {

// when the track starts restart the clock to the beggining of the beat and bar.
Serial1.write(0xfa);
  
}

}




void loop() {

  if (playSdWav1.isPlaying() == false) {
  //  Serial.println("Start playing");
    playSdWav1.play("HELLO2.WAV");
    delay(20); // wait for library to parse WAV info
  }


 
}
 
Status
Not open for further replies.
Back
Top