Ethernet audio library ready to beta test

Status
Not open for further replies.

palmerr

Well-known member
Hi,

I've been working on an Ethernet transport layer for the Audio library which is now ready for beta testing.

https://github.com/palmerr23/EtherAudio - this repo only contains the added/changed files, not a full library. I'll create a pull request for Paul when the additions are fully tested.

It comprises a Control object which uses Paul's Ethernet library (v2) to control WizNet adapters (5500s preferred); an Input and Output object with a stereo channel each. More than one can be used for extra channels.

As well, it has a facility to send messages between hosts - the SendControl and RecvControl examples illustrate how this works.

Two other examples are included
SendSimple/RecvSimple - which just sends a pair of sine waves from one host to another
Send/Recv2way - bi-directional send and receive to/from both hosts.

I've done some initial testing on T3.2, T3.5 and T4.0 with two different varieties of Wiz5500 module. The only known issue is block loss due to different audio clock rates of T3.x and T4.0 (about 0.05% different).

There is some design and use information in the DOCX file in the root of the repo.

I'm open to code improvements - e.g. at the moment I'm transmitting audio buffers of zeros on no input, maybe I can simply not transmit for that cycle as I believe Paul has mentioned in other posts that "all objects (should) handle no data available on update() as silence"?

Have fun!
 
Note to self, don't accidentally drag a file onto the browser when you have a post 98% written. And again...

Have you tested this as a library? From what I'm reading, you can't use #defines in the ino and #ifndef in the library as they are not the same scope (that's the error I get).
The issue is the Teensy audio shield uses non standard SPI pins and the library is not the place to be defining them. I spent over an hour trying to fix this (and learning) and finally just added private member variables for MISO MOSI CLK and CS as well as a setter in control_ethernet so I could make a call and set them before calling enable(). The the IP and MAC need to be set too. I see where you were going with the MYID thing, but having a #define for MYID doesn't work for the scope issue as well as the fact that if you want to program 2 of these, as you would expect for a 2 way example, you have to have separate code bases for each, or change it every time you reprogram. I pull the ID, IP etc from EEPROM and pass it...but this brings me to my second question.

Why does enable() run at construction? Seems like this is known as you mention setting the #define for the SPI CS pin before the includes. I tried to move everything in enable to it's own function and that did not work.. Seems like you used to have the code in begin() but since moved it to enable(). Things go downhill if you remove that code from begin() and try to run it later. When it's in there it works, but it still makes an initial connection attempt (and fails) with the defaults in the library before my runtime sets the right values (SPI pins, IP/ mac, etc) and calls enable() again. I assume this has something to do with my next question.

Maybe 20 years ago I would have know the answer to this but it's been a while since I've written C++

Code:
class AudioControlEtherNet : public AudioControl, public AudioStream
{ 
public:
	AudioControlEtherNet(void) : AudioStream(0, NULL) { enable(); begin(); }

I don't think I've ever seen function calls inside a class declaration. I assume the { enable(); begin(); } is what triggers the call prior to runtime? What is it about enable() that it must be called at construction rather than at runtime? Or am I completely lost?

Sorry if this is basic stuff. Pointers to what to search for to get the answer are welcome.
 
Maybe 20 years ago I would have know the answer to this but it's been a while since I've written C++

Code:
class AudioControlEtherNet : public AudioControl, public AudioStream
{ 
public:
	AudioControlEtherNet(void) : AudioStream(0, NULL) { enable(); begin(); }

I don't think I've ever seen function calls inside a class declaration. I assume the { enable(); begin(); } is what triggers the call prior to runtime? What is it about enable() that it must be called at construction rather than at runtime? Or am I completely lost?

Sorry if this is basic stuff. Pointers to what to search for to get the answer are welcome.

Is that not the complete (implicit?) implementation of the constructor (using {,} )?
 
I'm at the limit of my syntax (and OOP) knowledge.. I found this: https://docs.microsoft.com/en-us/cpp/cpp/delegating-constructors?view=vs-2019
This code seems to match up with a delegated constructor.

So I'm thinking the constructor for a new AudioControlEtherNet delegates the construction so the AudioStream() constructor gets called, though I have yet to find any example that explains putting function calls in the {}. What exactly happens there? I can only guess he AudioStream constructor runs, then enable() and begin() get called?

The AudioStream constructor is equally as baffling to my simpleton brain.
Code:
class AudioStream
{
public:
	AudioStream(unsigned char ninput, audio_block_t **iqueue) :
		num_inputs(ninput), inputQueue(iqueue) {
			
			// snipped a bunch of code.
			
		}

Guess it's time I take an updated C++ course. Apparently this notation was introduced within the past 10 years (C++11)?
 
To answer some of your earlier questions:

Ethernet inherits from AudioStream and AudioControl, rather than delegating - but in essence, yes to your question.

Yes it has been tested, but I'm still working out a few issues with Audio Board pin conflicts.

With the SPI library, you can dynamically change the pins before you use them. SPI.setMOSI(x); etc. I hope to have this all sorted out this week. (I took a break on this project to work on the CS32448 8x8 board update.)

Code:
void setup() // T4 + Audio Board pins
{
  AudioMemory(40);
  pinMode(7, OUTPUT);
  pinMode(14, OUTPUT);
  pinMode(10, OUTPUT);
  SPI.setMOSI(7);
  SPI.setSCK(14);
  SPI.setCS(10);
  SPI.begin();
...

However, with the audio library + ethernet, the ethernet library starts SPI internally, so using the pin changing routines in setup() can be hit and miss, depending on when the constructor runs.

I agree about #defines in .ino / .h - if I've made that mistake, I'll fix it. I doubt this, as I've tested the basic routines extensively on T3.2, T3.6 and T4 with a several different WIZ5500 modules.

Please keep providing feedback and finding bugs or ways that I can make the library easier for people to use.
 
From my understanding this Ethernet audio library only works from Teensy to Teensy, would it be too far out of the question to ask about supporting a standard like Dante?
 
I don't have anything useful. I've spent probably about 8-9 hours on trying to get EtherSend2Way to work. No Joy.

After revamping the setup code to better handle setting the mac, IP and SPI pins, so far...
I can ping both units at the address I assign them in the code.
I can see them show up in my network gear.
I can see them put out a single ARP request for the gateway.
Nothing else. I would expect them to start broadcasting their streams.

What's really weird is if I take one line out of control_ethernet.cpp that seemingly does nothing, the whole thing goes into an ARP loop.

Current code snip

Code:
	if(_myIP[0] != 10)
		_myIP = defaultIP_CE;  // default it if not set to a 10. network, need something better.
	
	Ethernet.begin(_myMAC, _myIP); 
	
	Ethernet.setSubnetMask(defaultNetMask_CE);
	
	_myIP = defaultIP_CE;     // if I comment this line, it all stops.. why I don't know since it's already started with the correct IP. Not even sure why it was there in the first place.
	
			
	wiztype = Ethernet.hardwareStatus();
	udpval = Udp.begin(_controlPort);

This produces the following single broadcast message:

Code:
No.	Time		Source			Destination	Protocol	Length	Info
3810	11.820090	de:ad:be:ef:fe:02	Broadcast	ARP		60	Who has 10.0.0.1? Tell 10.0.0.177



If I comment that one line, then I get this:

Code:
No.	Time		Source			Destination	Protocol	Length	Info
487	4.689967	de:ad:be:ef:fe:02	Broadcast	ARP		60	ARP Announcement for 10.0.0.177
488	4.892181	de:ad:be:ef:fe:02	Broadcast	ARP		60	ARP Announcement for 10.0.0.177
489	5.094425	de:ad:be:ef:fe:02	Broadcast	ARP		60	ARP Announcement for 10.0.0.177
491	5.296766	de:ad:be:ef:fe:02	Broadcast	ARP		60	ARP Announcement for 10.0.0.177
492	5.498935	de:ad:be:ef:fe:02	Broadcast	ARP		60	ARP Announcement for 10.0.0.177
493	5.701186	de:ad:be:ef:fe:02	Broadcast	ARP		60	ARP Announcement for 10.0.0.177
498	5.903424	de:ad:be:ef:fe:02	Broadcast	ARP		60	ARP Announcement for 10.0.0.177

If I leave the line in, it seems to load and be waiting for streams, but it never puts out anything. It just puts out the occasional status update through the serial port.

Here's what my modified output looks like..

Code:
Start Enable
Starting Ethernet: DE:AD:BE:EF:FE:1:  -->  0.0.0.0.

**** Ethernet shield was not found. Sorry, can't run without hardware. :( ****
UDP Bad
Ether Audio stream: Active
AQ Free 32
CQ Free 32
Audio memory 0
control_ethernet.enable() complete
 inputNet.begin() start
Audio Mem Usage: 0
 inputNet.begin() complete
Mike's Audio Hello world!
ID Read: 2
 IP Address set to 10.0.0.177
Start Enable
Starting Ethernet: DE:AD:BE:EF:FE:2:  -->  10.0.0.177.
WIZ chip: 3
UDP OK
Ether Audio stream: Active
AQ Free 32
CQ Free 32
Audio memory 2
control_ethernet.enable() complete
Found WIZ type 3
My IP is 192 168 1 100 
Ethernet cable connected = Yes
Audio Memory 3
Exiting setup() - ethernet link may take some time to connect. 
_____________________

Ethernet cable connected = Yes
Processor (Curr/Max) = 17.51/129.92. Audio Memory (Curr/Max) = 3/5
Streams

Ethernet cable connected = Yes
Processor (Curr/Max) = 17.35/129.92. Audio Memory (Curr/Max) = 3/5
Streams

...repeats...

I know there are some random comments about not having ISRs and that they are needed for it to work. What exactly are we supposed to add?

Also it does not seem like the audio subsystem is running because I patched the sine gen to the output and nothing is coming out.

I'm sure you have a lot of time and energy into this, I hope others can help make it more mature. Unfortunately I think it's a little too beta for my knowledge level.
 
Looks like you're 90% of the way to success. You've got the hardware up and running and the Ethernet cable is registering as connected.

What I suspect is happening is that nothing is triggering the update() cycle for the Ethernet control. This is what the "no ISRs" comment is about. The Ethernet code is entirely program driven, and needs something else with an ISR to trigger the chain of update() calls in the Audio Library. Whatever you choose needs to have a patchCord to something else, or it will remain dormant.

That's what this code in the example is for
Code:
#ifdef T4     // T4 use PWM for output and to provide interrupts
  AudioOutputPWM           pwm1;          
  AudioConnection           patchCord1(net_in1, 1, pwm1, 0);
#endif
#ifdef T35_6         // other Teensys have DACS for better output quality
  AudioOutputAnalogStereo  dacs1;          
  AudioConnection           patchCord1(net_in1, 0, dacs1, 0);
  AudioConnection           patchCord2(net_in1, 1, dacs1, 1);
#endif
#ifdef T32         // other Teensys have DACS for better output quality
  AudioOutputAnalog        dacs1;          
  AudioConnection           patchCord1(net_in1, 1, dacs1, 0);
#endif

On a T3.x I use the DACs, as connecting an output to them is an easy way to check things are working.

On a T4.0 things are messier as some of the things that could provide interrupts are not fully implemented yet. PWM was working on T4 but I haven't re-tested for the since 1.49

The "gold test" for whether something has an ISR is that somewhere in the begin() or init() code for the item there will be a line something like: update_responsibility = update_setup();

As for your line where everything stops when you comment it out...

Changing _xxx variables in libraries is fraught with danger! I thought I had made _myIP private to avoid this. Maybe you have created your own version of this variable.

Can you post complete code to the end of setup(), as you seem to have modified my example code, so that I can see what, and how, you've initialized things?
 
From my understanding this Ethernet audio library only works from Teensy to Teensy, would it be too far out of the question to ask about supporting a standard like Dante?

Is this something the USBHost Ethernet code could conform to for testing against another Teensy to see it work?
 
What I suspect is happening is that nothing is triggering the update() cycle for the Ethernet control. This is what the "no ISRs" comment is about. The Ethernet code is entirely program driven, and needs something else with an ISR to trigger the chain of update() calls in the Audio Library. Whatever you choose needs to have a patchCord to something else, or it will remain dormant.
Corrrect. Audio library data-driven updates need at least 1 'real' data input or output object. It has not to be connected to other objects, but it must be running. Typical case where this is required is USB_Audio. ADC input or output is typically used. Alternatively one can generate own periodic trigger (e.g. cloned from ADC_input) to triiger update_all().
 
Is this something the USBHost Ethernet code could conform to for testing against another Teensy to see it work?

There’s no reason it couldn’t, it’s just a matter of porting it over since I haven’t had time to make an easy wrapper library yet.
 
From my understanding this Ethernet audio library only works from Teensy to Teensy, would it be too far out of the question to ask about supporting a standard like Dante?

I looked into this a couple of years ago. AES67 is the standard to aim for (with which Dante is compatible). It turns out you'll need a native Ethernet implementation to get the hardware timestamping necessary for the PTP part of the stack. There is a little more detail in this old thread.
 
So I have a "real" output (I think).

The original code implemented spdif1 as a real output. I changed that to a AudioOutputI2S output. And patched the sine out to it. I have the Audio Shield working very well in another sketch so I know the hardware works.

Code:
AudioOutputI2S            spdif1;  
AudioConnection          patchCord1(sine1,   0, net_out1, 0);
AudioConnection          patchCord3(net_in1, 0, net_out1, 1);   // net in to second net out output
AudioConnection          patchCord2(sine1,   0, spdif1, 0);     // local monitor?  Sin to I2S output

One thing that came to my attention yesterday was the following line was not in this sketch to tell it I was using the Audio shield.
Code:
AudioControlSGTL5000 audioShield;
In my other sketch all audio control is done through this object. But if I understand it correctly, AudioControlEtherNet extends AudioControl library, so I assume then that in my case, AudioControlEtherNet should extend AudioControlSGTL5000 instead(?) I don't see how else to indicate I'm using the Audio Shield.

I've attached the hot mess, but I don't expect anything in return.. My natural curiosity will keep me going. But here are some notes if you choose to dive in. The code is a mess due to inserting serial prints in order to understand what the program is doing.

98% of my changes are in control_ethernet.cpp and .h but there may be other changes. Audio, input_net and output_net may also have a changes. May want to do a full compare. I have debugging turned up a bit too.

Some concepts I've changed.
The myID is read from the EEPROM so I can use the same code for both units
The IP address is set in the sketch based on (but not derived from) the ID read from the EEPROM and passed to the control_ethernet
I have removed the manipulation of the IP address in control_ethernet where it used myID to derive the actual IP as I set them explicitly in the sketch
The MAC is still derived from the default + myID
I added hacky code to default the IP address if it is not set to a 10. IP, as well as the MAC. Original code seemed to always default it.

As far as the issue where things stop if I comment out the IP address defaulting... I've gone through each instance of _myIP in the code and I just don't see why it would break. It's set correctly right before it's sent to the ethernet library, why would I want to default it right after I use it. Unless that's the only time it works and there is another issue breaking it.
 

Attachments

  • Eth2wayMike.zip
    450.4 KB · Views: 91
To answer the "how does it all work together?" question first...

ControlEthernet and its two objects inputNet and outputNet work as a set. Control looks after all the plumbing and manages input and output queues of audio and control messages.

InputNet and OutputNet take/put audio messages from/into the queues and interact with other audio objects via patchCords. Control messages are directly managed by user code, other than when they're being used to manage network audio streams (hidden from users).

AudioControlSGTL5000 manages the audio board hardware and the I2S objects.

Other than the connection via patchCords the two sets of objects are quite independent.

I can't see why you are setting _myIP to defaultIP_CE after running Ethernet.begin(); (other than your comment that it works!) This will throw off a number of things in the library (unless they have identical values). _MyHostID also needs to be set, or audio and control message processing will not occur properly (blocks discarded). Try using setMyID() instead.

BTW EtherNet1.volume(0.7); won't do anything useful. It's merely a stub for compliance.

I like your setSPI() routine, and will probably put a version of it into the library. However, you have to watch out that the library code starts working as soon as the objects are created, often before setup() runs. You will see a number of blockers in the update() routines, to make sure uninitialized objects aren't trying to process messages. I've been thinking about putting an SPI.end() before the setMOSI() etc commands to see if that helps, as I can't remember if the SPI (and WIZ5100) routines like having their pins changed after initializing. If your latest code is recognizing the WIZ, then what you've got obviously works OK!
 
Ok, I went back to basics. I stripped everything but the audio code out of the 2Way sketch because I know I should be hearing a tone and I'm not.

I found that changing the SPI pins after the include doesn't seem to work. (I see you just posted a warning about this) I set the default pins to what I needed and I got a tone out.
I then put everything back in the example and still got a tone. Ok we're making progress!

I'm still not seeing any broadcast traffic in wireshark and neither unit is showing that there are any streams available. The only thing I see in wireshark from these units is the ARP for the gateway.

But the odd thing is, the light on the RJ45 goes solid like it's sending constant traffic. Not sure how else to troubleshoot the network side of things as I don't have a "known good" half.

Does this mean anything to you:

Code:
<E A Mem 1 E-up, ASent 3999, CSent 3, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 5998, CSent 5, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 7997, CSent 6, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 9996, CSent 8, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 11995, CSent 9, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 13994, CSent 11, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 15993, CSent 12, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 17992, CSent 14, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

<E A Mem 1 E-up, ASent 19991, CSent 15, CRecd 0, ARecd 0, B 0 AQF 32 O 0 I 0 CQF 32 O 0 I 0>

Seems like ASent is Audio packets sent. and CSent is control packets. So now to figure out if it's not sending the right thing, or not receiving the right thing..
 
OK, good news!

I'm having all kinds of pain with SPI pin setting as well - it's a bit hit-and-miss, and is high on my list of TODOs - as it's contributing to the issue with the SGTL5000 board working with the WIZ.

The diagnostic output:

A Mem 1 ... one audio block in use
E-up ... ethernet is up
ASent = xxx ... lots of blocks have been queued for output.
CSent = x ... these are control messages that have been queued, none are left in the queue (CQF = 32 below) - they will be the ones identifying your output stream.
ARecd, CRecd ... Nothing coming in! Here's where your problem probably lies.
AQF, CQF = 32 ... Both the audio and control queues have nothing in them (i.e. all 32 blocks are in their Free queues). Thus the control engine is sending them to the WIZ.
The related O and I values are counts of what are in the In and Out queues (shared). Free + O + I <should> = 32. (These were meaningful when I was testing the queue management routines.)

So, all looks OK other than no blocks being received.

Are you broadcasting output packets or sending them to a specific address? Broadcasting works on most switches.

On my switches (little TP-link 10/100 cheapies) I get a fixed light for connect and rapid blinking for transmission.

On the WIZ8510 I get a solid green light when things are working OK.

Make sure you have everything set properly in the WIZ - Ethernet.linkStatus() is a good test, but don't run it in live code as it immediately creates some SPI calls, which might be gazumped by the SPI calls in update() which is called by interrupt!

BTW, I sometimes get caught forgetting to have both SerialMonitors running, and debug > 0 ... and wondering why nothing is happening!

I've only tried 192.168.x.y ethernet addresses, but I can't see why 10.x.y.z shouldn't work if the netmask is appropriate.
 
SUCCESSSSSSS!

So the weirdness with that _myIP commented line just made no sense, which means it will make sense as soon as I figure it out.. You dropped some clues in your replies (THANK YOU) and I found I had commented one line that kept the 255 from being written to the last octet in the target IP and instead just left it set to itself. I thought I understood what the HostIDtoHostIP was doing but I was wrong. I thought it only dealt with our own IP.

I'm still battling some wierdness where the light on the Teesy 3.6 stops light up after an upload, but everything runs, except the audio output. I have to load a different audio sketch and then load this one back to get it lit again, and when it lights, the audio output works.

In any case, I patched the audio a little differently so I could tell what was going on.

Sine 1 -> net_out, so I should hear this on the other end.
Added a sine 2 of a different tone that goes to a mixer
net_in also goes to that mixer
Output of mixer goes to I2S output.

So if I get dual tones out both speakers, i have 2 way audio.

My one last (haha) question is, what does this line do?
Code:
AudioConnection          patchCord3(net_in1, 0, net_out1, 1);

It seems it patches the network input to the network output (on the other channel) which I think would create a complete round trip... But if I don't want the network input be retransmitted back out and I remove that line, the whole thing stops working.. It won't even boot past the ethernet init.
 
SUCCESSSSSSS!

... But if I don't want the network input be retransmitted back out and I remove that line, the whole thing stops working.. It won't even boot past the ethernet init.

Note sure if this from above relates - maybe without that patchcord active there is no 'real' object driving the data engine?

Corrrect. Audio library data-driven updates need at least 1 'real' data input or output object. It has not to be connected to other objects, but it must be running. Typical case where this is required is USB_Audio. ADC input or output is typically used. Alternatively one can generate own periodic trigger (e.g. cloned from ADC_input) to triiger update_all().
 
Hoping someone can give insight on this odd issue:

Normally I load my sketch, press the program button in the IDE, Teensy light goes out, the little loader app is in auto mode. I hear a couple of "windows USB disconnect" sounds, it reprograms, and reboots. Teensy LED turns back on, sketch runs as expected. I can do this 100 times without issue.

If I unplug the USB and plug it back in after having this 2Way sketch loaded, it boots without turning on the LED. The network side works fine, It starts streaming audio to the other unit, serial works fine, just no audio out. I've tried unplugging power, unplugging everything, letting it sit.. The sketch will never work again after you pull power the first time.

I've tried plugging it into just power (I removed the serial wait), no difference... Boots, have network, have streaming, but no audio out.

No amount of reloading the sketch will fix this issue. The only way I can fix it is if I load up a different sketch. A fairly simple one that uses the same audio library, but the sketch uses no ethernet functions and does not use AudioControlEtherNet, it uses AudioControlSGTL5000. If I load that one in, then when it comes back from reboot after programming, the LED is back on. This sketch survives a power cycle and boots fine with just power. LED turns on and the audio works just fine.

I tried fixing it with a sketch that does not use the audio library, just the standard Ethernet library and that did not seem to fix it. Nor does it work (it's the server example). Serial works, but notwork does not. No LED on the Teensy. If I load the other "fixing" sketch and then load this one back, it works fine.

This happens on both copies of the Teensys (Teensies?) I have. Reset button doesn't do anything either other than reboot back into this partial failure mode.

One possible explanation I could come up with is the AudioShield being left in some weird mode. I ran my "fixing" sketch one one, powered it down. Ran the 2Way on the other, and power cycled it to make it fail. Then swapped the AudioShields and booted the bad one. No difference.
I did the same with the WIZ module. No change.

Doesn't make any sense to me. Not a behavior I'd expect from an embedded platform. If you power cycle it, it should boot. The fact that it doesn't recover after reprogramming boggles my mind.

Not sure where to go from here.
 
Ok, so discovered something:

I took my "fixing" sketch, and did this:

Code:
//AudioControlSGTL5000 audioShield;                   // comment this (works with this line enabled)

AudioControlEtherNet audioShield;                  // added this line instead of the above.

Everything still worked after the switch..
But if I power cycled it, audio stopped working and the LED stopped lighting. This sketch has I2S audio in and out with some tone detectors and nothing is working audio wise, but I still get serial out.
Before I go any further, I need to revert the EtherAudio library back to release to make sure I'm not chasing my own gremlins.
 
Yes, I'm having trouble with the SGTL5000 control as well when used with ethernet.

It's my top priority after getting a reliable way to change SPI pins that works for both T3 & T4.
 
Status
Not open for further replies.
Back
Top