Audio library with external clock, or PLL modification

hatchjaw

Member
Hi all,

I'm working on a networked audio project, and naturally I'm faced with issues of clock drift and jitter. To date I've been using a delay-locked loop approach (something akin to that described by Fons Adriensen here), but for improved inter-client synchronicity I'm keen to explore other options.

I created a rudimentary JackTrip client for Teensy few months ago; now I'm working on a bespoke, JUCE-based, multicast audio server and Teensy client.

While working on the JackTrip client, I found QNEthernet to be unreliable (I may have been using it incorrectly) so I stuck with NativeEthernet. However, I recently encountered AsyncUDP_Teensy41, which is based on QNEthernert and appears to work very well, so I intend to incorporate it into my project. This may help mitigate jitter, but clock drift will still be an issue.

My enquiry is twofold:

- As per this thread I've experimented with adjusting PLL4 to set Teensy's sample rate to compensate for clock drift. To do so, I just copied the code from output_i2s.cpp:406-416, adjusting `AUDIO_SAMPLE_RATE_EXACT` with a figure derived from the ratio of reads and writes to a circular buffer storing audio data from the network:

Code:
        //PLL:
	int fs = AUDIO_SAMPLE_RATE_EXACT * (numWrites/numReads);
	// PLL between 27*24 = 648MHz und 54*24=1296MHz
	int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
	int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);

	double C = ((double)fs * 256 * n1 * n2) / 24000000;
	int c0 = C;
	int c2 = 10000;
	int c1 = C * c2 - (c0 * c2);
	set_audioClock(c0, c1, c2, true);

Hardcoding an unrealistically low or high value for `fs` instead, I find that my delay-locked loop drags or rushes as expected, and I don't hear any discontinuity or distortion when I call `set_audioClock`. I don't know well enough what I'm doing with that function though (or whether I should set `CCM_ANALOG_PLL_AUDIO &= CCM_ANALOG_PLL_AUDIO_POWERDOWN` first, or regarding the registers in imxrt_hw.cpp -- I can't link what's going on with the documentation I believe I should consult at https://www.nxp.com/docs/en/data-sheet/SGTL5000.pdf), so I would be very grateful if someone could help me identify how I can set appropriate values for c0, c1, and c2 to achieve an arbitrary sample rate, as so far I've only been able to use integer values for fs. Word is the audio clock can be adjusted in very fine incrementst, so I'd like to give it a try. I gather c0 should fall between 27 and 54, for example, but I don't know why.

- Ultimately, I think I should probably be using an external clock. Rather than considering word clock, or NTP/PTP, I'm wondering whether the followiing is possible:
  • Have one Teensy running as a USB audio device (i.e. use AudioOutputUSB and define USB_AUDIO);
  • Use that Teensy as the audio interface for the computer acting as the networked audio server, thus timing on the server is derived from that Teensy;
  • Share the audio clock from the audio interface Teensy with a collection of network client Teensies.

Does anyone know whether this is possible? If so, how would I go about sharing the clock of the audio interface Teensy with the network client Teensies? Something similar to using a RTC or GPS clock (as per here)?

Please forgive the rambling enquiry. Any assistance/advice would be very much appreciated.
 
Change only c1 value by add or subtract by 1.
Code:
// -- Freq 88200    for 44100kHz ADAT, MCLK1 22,579,125Mhz -- we need 22,579,200MHz
// c0 DIV   30      -- OSC_24000000Mhz *(30 + (1055/10000)) = 722,532,000MHz
// c1 NUM   1055    <--- issue
// c2 DENOM 10000
// n1 PRED/ 4       -- 722,532,000 / 4 = 180,633,000MHz
// n2 PODF/ 8       -- 180,633,000 / 8 = 22,579,125
// MCLK--22,579,125MHz

// -- Freq 88200   for 44100kHz ADAT, MCLK1 22,579,200Mhz
// c0 DIV   30     -- OSC_24000000Mhz *(30 + (1056/10000)) = 722,534,400MHz
// c1 NUM   1056   <--- PLL frequency correction @88200
// c2 DENOM 10000
// n1 PRED/ 4      -- 722,534,400 / 4 = 180,633,600MHz
// n2 PODF/ 8      -- 180,633,600 / 8 = 22,579,200
// MCLK--22,579,200MHz -- good @88200
Also this is extremely useful: MCUXpresso Config Tools: Pins, Clocks and Peripherals
 
I feel I need to add: QNEthernet is very reliable (notwithstanding any undiscovered bugs). My understanding is that it was being called from an interrupt context, and the library is expressly not designed to be used asynchronously. This includes multiple threads, being called from within ISRs, and the like.

It’s possible to write very performant networking code using QNEthernet by thinking in single-threaded terms.

With regards to that aforementioned so-called “async” UDP library, it doesn’t actually use QNEthernet, nor is it asynchronous. (My view is that it shouldn’t even contain “QNEthernet” in its description.) It merely uses the library’s included lwIP distribution, configuration, and initialization code. QNEthernet can be made to behave the exact same way by modifying the “receive” function in QNEthernetUDP.cpp to call a callback. However, if queuing or buffering needs to be used, then the whole idea will end up looking like what I already put in the EthernetUDP class, and you won’t need the “async” (callback) approach at all.
 
I just wanted to say that JackTrip is an awesome project, never heard of it and I will be digging in for sure.

Exactly what connection is needed electrically to connect an external clock to the Teensy? I know I've seen BNC connectors used for this. Does MCLK1 become an input, and the Teensy is a slave?
 
Thank you very much for the replies!

@Chris O. this is interesting stuff. I downloaded MCUXpresso Config Tools, and that (plus the i.MX RT1060 reference manual) has thrown a little more light on how clocks are set up. So the manual says PLL4's output frequency = Fref * (DIV_SELECT + NUM/DENOM), where Fref is the 24MHz reference clock.

From output_i2s.cpp:

Code:
//PLL:
int fs = AUDIO_SAMPLE_RATE_EXACT;
// PLL between 27*24 = 648MHz und 54*24=1296MHz
int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);

double C = ((double)fs * 256 * n1 * n2) / 24000000;
int c0 = C;
int c2 = 10000;
int c1 = C * c2 - (c0 * c2);
set_audioClock(c0, c1, c2);

That'll give:

c0 (DIV_SELECT) = 28
c1 (NUM) = 224
c2 (DENOM) = 10000

PLL freq = 24000000 * (28 + 224/10000) = 672,537,600

which is (15250 + 2/7) * SAMPLE_RATE_EXACT. In your example we have 722,534,400 = 2^14 * SAMPLE_RATE_EXACT -- that power of two makes a lot more sense than what appears to be happening in i2s setup. Have you any idea why Teensy sets the audio PLL up with this frequency be?

Thanks to @shawn I now understand that using QNEthernet (or indeed the aforementioned "async" lwip library) directly from the audio ISR isn't viable. This works for NativeEthernet, but if I'm to use QNEthernet I'd have to take a different approach.

Does MCLK1 become an input, and the Teensy is a slave?

@JayShoe, this is the sort of thing I'm wondering!
 
I think you miss calculated something here: 24000000 * (28 + 224/10000) = 672,537,600
-- Freq 44100 -- OSC_24000000Mhz *(28 + (2240/10000)) = 677,376,000
c0 DIV 28
c1 NUM 2240
c2 DENOM 10000
n1 PRED/ 4 -- 677,376,000 / 4 = 169,344,000
n2 PODF/ 15 -- 169,344,000 / 15 = 11,289,600MHz
-- MCLK--11,289,600MHz
44100_MCLK11,289,600.jpg
 
Last edited:
I think you miss calculated something here:

I cannot exclude that possibility.

Thank you very much for the screenshot; that really clears things up. I wasn't taking account of SAI1_CLK_PRED and SAI1_CLK_PODF. So, as far as I understand, the output frequency of SAI1 should be (256 * MY_TARGET_FS).
 
Does MCLK1 become an input, and the Teensy is a slave?
@JayShoe, this is the sort of thing I'm wondering!

I noticed on an old thread some discussion about external clocks. The discussion suggests using the teensy SPDIF hardware input as a word-clock input. https://forum.pjrc.com/threads/60914-ADAT-white-noise-on-Teensy-4-0?p=239884&viewfull=1#post239884. It sounds like BNC is more "consumer" level and AES is considered "pro" level; because BNC is single ended and AES is balanced (for longer runs). afaik...

I found a discussion on diyaudio that referred to a schematic on the CS8416 datasheet.

Screenshot 2023-04-04 090616.png

There is another reference to the Twisted Pair Audio S/PDIF 4:1 MUX/RECEIVER MODULE.

Screenshot 2023-04-04 090828.png

So the conversion from AES to single ended is either via a transformer or a digital line receiver ("The two of the usual suspects for this job are the 26LS32 and the SN75176"). Or in the case of a single ended clock it can be a simple RC filter. Then the single ended input is fed into the Teensy SPDIF input. That makes it pretty simple stuff in hardware! Then there is the small issue of configuring the Teensy to use the SPDIF input as the word-clock. :p

See thread linked above for some ideas on how to make it work with the existing SPDIF Slave object (audio tool / github by Frank Bösing) code.
 
I reread the original post and realized that you weren't looking for an external word clock. Your issue is with sending the first clock to the slave teensies over a network... So my list is a bit off topic.

You seem to be ok with the quality of the primary teensy clock but you want to send the clock to the secondary teensies.
 
You seem to be ok with the quality of the primary teensy clock but you want to send the clock to the secondary teensies.

I'm very much open to suggestions and ideas. Your previous post has given me plenty of things to investigate (e.g. how AudioInputSPDIF3, AudioOutputI2S, etc. attach an ISR to a DMAChannel... I guess this is the basis for the system of interrupts on Teensy). Thank you for taking the time to share that information.
 
This may or may not be relevant to all the use cases mentioned above, but I have a PR in which recovers the incoming S/PDIF clock and uses it for the I2S objects. I seem to recall I couldn’t do TDM because it needs a clock twice as fast as provided by the recovered one.

If nothing else, the code changes may help people do what they need to, or indicate it’s not possible and thus save some wasted time!
 
I'm working on a networked audio project, and naturally I'm faced with issues of clock drift and jitter.

What I've been thinking about recently, is how to (or whether one should) broadcast a clock over a network. It sounds messy to me. Asynchronous/resampled data sounds like the correct path for the client teensies on a network.

It's also possible to have incoming data be received at a different clock speeds and sample rate on different SAI ports (IN2), then it can be resampled for playback via SAI1 at a different rate. This is being done at Alex6679's ESP32_I2S_Teensy4 code if I'm not mistaken. I wonder if that would be a valid way to receive the data from the network on a different SAI port and then resample it locally? I'm just spitballing.

Some standards to study would be Dante, AES67.

Probably one of the most helpful places to look is the EtherAudio library written by Palmerr23.
 
...I have a PR in which recovers the incoming S/PDIF clock and uses it for the I2S objects.

Thanks for giving this a try! Perusing your changes is an eye-opener in itself.

Asynchronous/resampled data sounds like the correct path for the client teensies on a network.

That's the approach I've taken so far. The aim is to support distributed spatial audio, so perfect synchronisation is the dream. For now, just something that works, while I build up an understanding of the workings and limitations. It's a time-limited project, and I'm reaching the stage where I have to settle for best-effort and call it a day.

Thank you for linking to those resources! Dante I'm aware of, and it's proprietary so I don't think that's a viable line of enquiry for me, but AES67 looks promising.

EtherAudio bears some similarities to what I've been working on, though my current system uses a server that's multicasting audio and OSC control data on separate addresses. A key difference is I've been redefining AUDIO_BLOCK_SAMPLES to 32 or 16, so it's possible to represent many more audio channels in each packet. Useful to know that the Wiznet devices can't handle jumbo frames.
 
Back
Top