Destructor for AudioStream?

chipaudette

Well-known member
Over the last bunch of years, there's been great work done to enable dynamic audio connections. Fantastic! But, as freely acknowledged, this work was limited to creating/destroying AudioConnection instances. Destroying Audio objects themselves (ie, instances of AudioStream) is still forbidden.

The primary issue is that AudioStream keeps a linked list of every AudioStream instance that's ever been created. If you need to destroy an AudioStream instance, you need to remove it from the list. There is no mechanism right now that does this.

This action should be triggered by the class's destructor. But, AudioStream has no destructor (other than the default one that implicit with C++).

Can we add a destructor to AudioStream? Or has that already been considered and then rejected?

If we were to implement a destructor, one might start with this:

Code:
virtual ~AudioStream() {
 
    //remove this instance from the AudioStream update list
    //__disable_irq(); //should we start with this?
    AudioStream *p = first_update;
    while (p != NULL) {
        if (p->next_update == this) {
            p->next_update = this->next_update;  //remove the pointer to this instance and replace it with the next instance
        }
        p = p->next_update; //go to next step in linked list
    }
    //__enable_irq();

    //release any audio blocks held in the input queue for this instance of AudioStream
    for (int i=0; i<num_inputs; i++) {
        audio_block_t *block = inputQueue[i];
        if  (block != NULL) release(block);
    }
}

Yes, this is a minimal destructor. For example, it doesn't do anything about stale pointers in AudioConnections. But, now that we dynamic connections implemented in AudioConnection, it is possible for the user to do the right thing and correctly destroy the relevant AudioConnections. That's not the case with AudioStream. Without this minimal destructor, there is no way to remove the instance from the linked list.

Has this kind of change been considered in the past and rejected? Please, let me know!

Chip
(As an aside, I do have an application that desires to dyanmically create / destroy AudioStream instances. So, this is not just idle chatter. I'd love to help make it a reality!)
 
Last edited:
Hi Chip

I've put a bunch of effort into this, with results at https://github.com/h4yn0nnym0u5e/Audio/tree/features/dynamic-updates and https://github.com/h4yn0nnym0u5e/cores/tree/feature/Audio/dynamic-updates, and a forum thread for support and discussion. In addition, I worked for a while with @manicksan using his Design Tool++ update which got to a state of being able to do live patch editing ... until it crashed, or exported nonsense, or something. (It's really very close to being truly excellent ... but not quite there.)

It's been a while since I touched my code, but I think it's reasonably stable, albeit somewhat behind Paul's updates to cores and Audio. If I thought there was a reasonable chance of it being adopted I'd put some effort into getting it more up to date, and submit some PRs, but since even simple fixes for fairly major bugs get ignored for ages, I feel I have better ways of spending my time.

If you fancy having a crack at it, please do, and report any shortcomings you may find (ideally on the specific forum thread). There are a probably few gremlins left to be ironed out, one of which is destroying hardware objects properly - at the moment they just get set to an inactive state. Having said that, those are probably the primary example of objects you never want to destroy, because you always have the same hardware, right? :giggle:
 
I had seen your work before, but I had not realized how deeply you had pushed your dynamism. In my mind, I was seeking something a bit more modest. As I'm hoping that Paul might choose to include a basic destructor in the next release, I think that a more modest set of changes is more likely to be accepted. A smaller set of changes is easier to get comfortable with.

I have offered this pull request: https://github.com/PaulStoffregen/cores/pull/755
 
Believe me, if I thought a more modest approach would work, I'd've adopted it! Well, to be fair, I did, to start with. But then the shortcomings became increasingly apparent, until I arrived at what exists today.
  • pushing the contract to disconnect or delete the AudioConnections onto the programmer adds a significant burden: IMHO, since the library has that information, it should bear the responsibility
  • with the static scheme, there is an implicit requirement to add objects in signal-flow order, because they're added to the end of the update list at construction time. It's alluded to in the documentation, and if it doesn't happen, you end up with nasty variable latency issues because blocks they emit don't get consumed until the next update. My dynamic version goes to immense trouble to link in at the right place in the list, including where several objects have been constructed and connected in isolation before being connected to the "main" update list
  • the inputQueue arrays are part of the derived classes, so you cannot use them in ~AudioStream(), they're already gone. It took me a while to figure out why I had lots of crashes when I thrashed the new/delete cycle mercilessly
 
Believe me, if I thought a more modest approach would work, I'd've adopted it! Well, to be fair, I did, to start with. But then the shortcomings became increasingly apparent, until I arrived at what exists today.
  • pushing the contract to disconnect or delete the AudioConnections onto the programmer adds a significant burden: IMHO, since the library has that information, it should bear the responsibility
  • with the static scheme, there is an implicit requirement to add objects in signal-flow order, because they're added to the end of the update list at construction time. It's alluded to in the documentation, and if it doesn't happen, you end up with nasty variable latency issues because blocks they emit don't get consumed until the next update. My dynamic version goes to immense trouble to link in at the right place in the list, including where several objects have been constructed and connected in isolation before being connected to the "main" update list
  • the inputQueue arrays are part of the derived classes, so you cannot use them in ~AudioStream(), they're already gone. It took me a while to figure out why I had lots of crashes when I thrashed the new/delete cycle mercilessly

Regarding your first two points, they are points that I agree with. However, to get dynamic destruction to work *at all*, I propose that we go with the minimum changes. This is the classic debate over whether it's better to offer a number of incremental small changes or to offer a single "big bang" change.

I feel that smaller incremental changes are more likely to be accepted. I could totally be wrong. It's up to Paul.

From the "minimal changes" perspective, I would offer that your first two points are best addressed through their own small incremental pull requests. My proposal to add a destructor does not create the issues that your first two bullets identify. Those unfortunate behaviors are already built into the Teensy Audio system. The addition of a destructor could help address these issues (especially the first one), but they're really separate issues.

The third bullet is a good one. I will remove the clearing of the `inputQueues` from the pull request. If it's the derived class that has ownership of the audio_block, the dervied class's destructor can release it. I agree that it would be more helpful if AudioStream could handle it for you, but again, that could be addressed with another incremental pull request.

I'm not trying to persuade anyone that my pull request is the best. It's just that I did not see ANY pull request addressing the destructor issue (you didn't offer yours for inclusion?). So, in the absence of any option, I wanted to put at least one option on the table for consideration. I really want a destructor!
 
No, I've not put in a PR for my all-singing all-dancing updates, partly because they are so far-reaching, and partly because even minor changes weren't being pulled. I know Paul is aware of my changes, and keeps a close eye on the forum, so I'm hopeful that if they become desirable he'll flag that and I can bring them up to date and ready to merge. I'll probably get in sync anyway once Teensyduino 1.60 is released - I do try to keep up with every official release. For a PR set I'll likely have to do a cleaner branch - sometimes "great ideas" sneak in that probably shouldn't be there :giggle:

The third bullet means you have to edit every object derived from AudioStream that has inputs, and is the reason for the weird SAFE_RELEASE_INPUTS() macros and the apparent complete re-write of the Audio library. Mostly the changes are very simple, though a few object types are a pain. Obviously if you're happy just to maintain edits of the few objects you want to destroy then life is (perhaps) a bit easier.

OK, so you want a destructor ... but then what happens when you've laid waste to your design by destroying most of it? Maybe you carefully re-build it in order, so bullet 2 doesn't matter so much. It still matters slightly, because the object with update responsibility probably hasn't been destroyed, and is now close to the start of the update list, not at the end. But you could just wave your hands and say "so long as you take care, it won't be any worse than another 2.9ms of latency". Not ideal, but it fits in with the incremental changes philosophy.

Whether there's merit in a small changeset, like just your PR + tell everyone to edit their Audio library + be careful on construction order + accept a probable 2.9ms latency increase + arbitrarily more if they aren't careful, I'm not sure, and I'm not the arbiter - as you rightly say, Paul is The Man!
 
I'm very familiar with the latency issue. Been working with it for years and years. It just isn't relevant in most situations that I deal with. Occasionally, latency is relevant...like in my hearing aid experiments. But, in those cases, I won't use dynamic connections. Again, including a destructor doesn't create the ordering problem. The ordering problem is much bigger / deeper.

If we want to talk about solutions to the ordering problem, I'm sure you've got good solutions. Your solutions probably solve the problem for-real. Please offer them for inclusion!

From my "minimal changes" viewpoint, I'd simply add one method to AudioStream: my one method would allow me to increment or decrement my position in the update list (while returning its location within the update list). Being so minimal in its goals, it would be a just a couple of lines of code. Being so short, it'd be easy to read and (hopefully) easy to approve. No, it wouldn't be easy to know that you've gotten the order that you want, but it would at least be possible. Currently, it's impossible.
 
Or, to solve so many of these problems even more simply, we could move the update list (and its other associated data members) out from being `private` and, instead, make them `protected`. This would give derived classes access to the update list. Therefore, we each could brew our own update management system that suits each of our own application's needs.

Right now, with all that stuff being `private`, it's out of reach from our derived classes. The only recourse that we have is to edit `cores`, which (IMO) is a super-messy place to work due to `cores` being entangled with so many other things.

Personally, I really like the idea of moving the update list out of `private` and up to `protected`. I would have proposed this in my pull request, but I figured the perceived risk would be too high for Paul to approve.
 
Last edited:
I've just glanced over my code to refresh my memory - it's been a long time!
If we want to talk about solutions to the ordering problem, I'm sure you've got good solutions. Your solutions probably solve the problem for-real. Please offer them for inclusion!
So ... the cores changes are confined to AudioStream.cpp and .h, and almost entirely about:
  • dealing with the ordering problem
  • keeping track of stale pointers, e.g. AudioConnection objects that refer to an AudioStream object that's in the process of being destroyed (i.e. what you want the programmer to take care of)
  • trying to make sure that an audio update occurring while destruction is in progress doesn't cause a crash
  • helper functions for the AudioStream-derived objects to implement in their destructors
Changes to the Audio library itself are fairly extensive, but boil down to:
  • giving every object a safe destructor: most are trivial, except...
  • ...input and output objects, which I couldn't figure out, so are put into a "dormant" state, and ...
  • ...objects that use extra audio blocks or allocated memory
There is a lot of debug-related cruft in AudioStream.cpp and .h, plus I don't like having to do things like deal with first_update as a special case, so there's a lot of nasty pointer-to-pointer code in there. Sorry!

I discovered I stated at one point that dynamic cores are fine even if you don't have the dynamic audio library, so just the cores changes could be done early and speculatively.

Paul said quite some time ago that he was planning to merge my changes into Teensyduino. Obviously that got deferred, but I live in hope :giggle:. If the reasoning is that the code's too messy, I can certainly tidy it up. Just need a bit of encouragement...

If you can bear to, I'd be very grateful if you'd give my changes a spin to see if they work for you. I know a few people have done so, and given very valuable feedback.
 
I've just glanced over my code to refresh my memory - it's been a long time!

So ... the cores changes are confined to AudioStream.cpp and .h, and almost entirely about:
  • dealing with the ordering problem
  • keeping track of stale pointers, e.g. AudioConnection objects that refer to an AudioStream object that's in the process of being destroyed (i.e. what you want the programmer to take care of)
  • trying to make sure that an audio update occurring while destruction is in progress doesn't cause a crash
  • helper functions for the AudioStream-derived objects to implement in their destructors
Changes to the Audio library itself are fairly extensive, but boil down to:
  • giving every object a safe destructor: most are trivial, except...
  • ...input and output objects, which I couldn't figure out, so are put into a "dormant" state, and ...
  • ...objects that use extra audio blocks or allocated memory
There is a lot of debug-related cruft in AudioStream.cpp and .h, plus I don't like having to do things like deal with first_update as a special case, so there's a lot of nasty pointer-to-pointer code in there. Sorry!

I discovered I stated at one point that dynamic cores are fine even if you don't have the dynamic audio library, so just the cores changes could be done early and speculatively.

Paul said quite some time ago that he was planning to merge my changes into Teensyduino. Obviously that got deferred, but I live in hope :giggle:. If the reasoning is that the code's too messy, I can certainly tidy it up. Just need a bit of encouragement...

If you can bear to, I'd be very grateful if you'd give my changes a spin to see if they work for you. I know a few people have done so, and given very valuable feedback.
What would be the easiest way to try out these changes?
 
  • ...input and output objects, which I couldn't figure out, so are put into a "dormant" state, and ...

Yeah, this is a tough one. In my typical minimalist approach, I was simply going to assert that destroying inputs and outputs was not allowed. In effect, I'd kick the can down the road, if you will. For my own uses, I have no need to dynamically create/destroy those objects, so not supporting I/O destruction was not a problem for me. It's cool that glad that you found a way to make them dormant.

  • dealing with the ordering problem

What was your approach for dealing with the ordering problem? Did you choose to provide methods to let the user re-arrange the order? Or, did you try to do something automatic by having code that analyzes the AudioConnection tree?

Letting the user do it (which is where I'd start, of course) seems simple enough to implement. But, if you did something more automatic, I'd love to hear your thinking!

If you can bear to, I'd be very grateful if you'd give my changes a spin to see if they work for you.

I'm headed out of the country for a bit over a week. So, sadly, no fun Teensy hacking for me for the next little bit. :(
 
For my own uses, I have no need to dynamically create/destroy those objects, so not supporting I/O destruction was not a problem for me. It's cool that glad that you found a way to make them dormant
I know a bit more now, so maybe I can figure out a way to destroy I/O objects completely, though it probably has very little practical use...

What was your approach for dealing with the ordering problem? Did you choose to provide methods to let the user re-arrange the order? Or, did you try to do something automatic by having code that analyzes the AudioConnection tree?

Letting the user do it (which is where I'd start, of course) seems simple enough to implement. But, if you did something more automatic, I'd love to hear your thinking!
For ordering, I took the fairly simplistic approach that it becomes defined between any two objects when the first connection is made, provided an ordering doesn't already exist. For [groups of] objects not yet connected to the "real" update list, an internal ordering can be established, and as soon as one object in a group is connected to the real list, the whole group is inserted. I'm not a good enough mathematician to prove that's guaranteed to work, but it seems to be OK. In the code I called the unconnected groups "clans" - couldn't think of a better name.
I'm headed out of the country for a bit over a week. So, sadly, no fun Teensy hacking for me for the next little bit. :(
Shame ... but at least you have something fun to play with when you get back!
 
What would be the easiest way to try out these changes?
Most information can be found on this thread, and that's the ideal place to go for support as any answers I give will be fairly easily found by future Seekers of Knowledge. The thread started out with me checking I wasn't about to re-invent the wheel, so the first details of how to try stuff out are actually to be found in post #17.

Important point - make sure you back up anything you're about to overwrite!

In cores, you just need AudioStream.cpp and AudioStream.h from my cores branch. Copy the files over the top of the ones in your Teensyduino installation; there's a pair for teensy3 and another for teensy4.

For the updated Audio library, you need my features/dynamic-updates branch. This is essentially a copy of the Audio library, and is used by putting it in your local libraries folder within your default sketch folder, exactly like any other library. It's worth double-checking the verbose compiler output in the Arduino IDE to check that it's found multiple Audio libraries, and picked the new one.
 
Back
Top