#bevy_seedling
1 messages · Page 2 of 1
Well actually, there is a surprising amount of "generic" UI stuff in DAWs like toolbars, menubars, dropdown menus, tileable panels, a settings window, tooltips, scrollbars, and (in my case) a properties panel and a sample browser.
Indeed
Things like knobs, the timeline, and the piano roll need to be custom of course, but all of the other generic stuff is really hard to get right if the UI library doesn't have them built in already.
No for sure, I don’t think it’s a bad choice at all to go with something that does all that stuff for you, me saying that I would just use Bevy and reimplement everything is more a reflection of my (very possibly outdated) trust in the state of Rust's UI ecosystem than it is a statement on using UI libs in general
(And don't get me started on accessibility)
Yeah that’s very true, maybe even with the state of Rust UI rebuilding that stuff is still an awful choice 😅
Don’t worry. I just mentioned bevy_egui and got it
Like one thing that's interesting to me about the KDE framework is there is a toolbar widget that automatically puts actions that don't fit on the screen into a dropdown menu. Definitely useful for small screens.
They also have a tooltip widget that can show a much longer explanation if the user hits the "Shift" key.
Oh that’s fantastic
They also have pretty solid Human Interface Guidelines. https://develop.kde.org/hig/
(Though if I do got with it, I will probably make my own tweaks to reduce some of the padding.)
You might be convincing me to dig into KDE instead of using flutter, although I’m going to try not to nerd-snipe myself with it too hard until I actually start work on the project that needs it
And plus I like the style of their icon theme.
Yeah, lol. Though do keep in mind that the downside is that it is based on Qt which is a huge complicated C++ library with decades of technical debt.
Ah, lol. Autocorrect.
I like it.
Though I am actually just aiming for 32 bit processing now. I may add 64 bit processing later, but it's not a priority.
Decades of technical debt = decades of support and decades of stackoverflow questions though so it’s not all bad. I love shiny new tech for personal projects but for production-grade stuff that I want to turn into a business I’d rather use something imperfect that’s battle-tested
That is true.
I liked FL Studio 9 32 bit great bass
They changed languages going to 64 bit
And I stopped
That’s part of why flutter is appealing to me, it’s old enough (and it’s been pushed enough by google) to be battle-tested but new enough to learn from the mistakes of frameworks like Qt
In particular, it was made after react/redux became big, so it learned a lot from that paradigm
I think the main pain with Flutter is the Rust/Dart bindings.
Absolutely, it’s a nightmare
I wrote a system to automatically generate protobuf files from rust types because it’s the only communication method supported by both apart from stuff like json but protobuf is such a garbage format that generating the rust types from the protobuf schema results in an unwieldy nightmare
And a dealbreaker for me was the lack of support for "pointer locking", which is important if you don't want a knob/slider to stop registering drags if the cursor hits the edge of the screen.
In fact, hardly any GUI framework supports that. Even Qt doesn't support it directly, but it does seem possible to do it in a "hacky" way.
Yeah, that’s fair. That’s to be expected from a framework that was designed for mobile first. In my case I specifically want touch devices to be first-class though (even though desktop is the intended primary platform), I don’t necessarily need to support mobile devices specifically but if I can then that’s a nice bonus
Yeah that’s kinda wild to me because it’s an incredibly common thing in creative applications
Ah yeah, that definitely would be important for mobile.
Of course the JUCE library does (a very common C++ framework for audio plugins). But JUCE is slow and is very hard to work with.
JUCE is an absolute nightmare and if I never have to use it again it’ll be too soon 😅 It’s necessary and I’m glad it exists in the abstract but it’s probably my least favourite thing about working in audio
Just yesterday we had an issue with our product that meant we had to set a major new feature to disabled-by-default because of an issue that ultimately appears to be caused by a bug in JUCE
The Visage library made by the guy who made the Vital synthesizer looks interesting (you can do some crazy custom shader effects with it), but it's still a fully retained-mode object-oriented C++ library.
Ooh I’ve never heard of that before, it looks really interesting
Love the idea of a library that combines processing-style rendering with standard UI elements
And I've seen more developers using the clap_wrapper library for cross-plugin-API development.
Really? I don’t what all the major manufacturers are using but all the ones that I know of are using JUCE
Yeah it's really neat. Although I imagine the lack of a declarative API would get pretty hard to maintain for a large application.
Yeah I feel like it’s more useful for something like a plugin UI than for a DAW
Well, I meant more in the circles I'm in, which is admittedly more CLAP focused.
Ah fair, yeah I don’t know much about what’s going on with any smaller plugin devs, we make a plugin host at work so we mostly care about the most-popular plugins and for my own personal stuff I have a small stable of major plugins that I use for everything. I don’t know what the cutting edge of plugin dev looks like really
A lot are emulations
I'm actually a contributor to the nih-plug plugin framework (I created the glue layer that ties the baseview windowing backend to various Rust GUI frameworks.) https://github.com/robbert-vdh/nih-plug I've even created my own plugin with it, which is a Rust port of the reverb module from the Vital Synthesizer. https://github.com/BillyDM/vitalium-verb
And by the way the Spectral Compressor plugin in the nih-plug repository is a legitimately awesome plugin. https://www.youtube.com/watch?v=jo_ayanaKo4
It is.
Oh sick
My only complaint is the lack of a dark theme. But I think the developer is considering making a paid version of the plugin with a nicer UI and more features.
I used it as a reference for building my compressor
So it’s effectively like a mb-compressor or vocoder with a very high number of bins? That’s really useful
Yep!
I like the tagline "everything can become pink noise if you try hard enough".
(Though that might be for the similar plugin in that repo called "Loudness War Winner")
Lmao
Ah
Do you know the video that’s referencing? It’s one of my fav videos about audio of all time
I guess it could be a coincidence since they’re both referencing the loudness war but any excuse to post this vid https://youtu.be/s_ANEQu5Lto?si=j0MN1C1ahPE1oy9q
I did it. I won. Good luck getting any louder than this. You can all stop this sillyness now. Loudness doesn't matter, just make it sound good.
Lots of you requested I release this mix as it apparently "slaps". That means it's good, right? So you should now or soon be able to find it on all the usual platforms, or here on YouTube:
https://youtu...
Seeing a + before LUFS is so cursed
Yeah, I've seen that. Great video.
Yeah, the TDR plugins are great.
I wish there was a way to bundle them with my DAW project. But alas, they are not open source.
It's easy enough to download them though.
Although actually, a cut down version of TDR's Molot compressor actually is open source. https://github.com/magnetophon/molot-lite
Write a list of free plugins suggestions in readme
Yeah, that's a good idea.
The Molot is more of an "aggressive" compressor than a mixing compressor though.
And for a lookahead limiter, the lamb plugin is actually really good (it's written in Rust too!) https://github.com/magnetophon/lamb-rs
But it is a high latency, heavy CPU limiter.
The ZLCompressor is also quite good. I might make it the main "bread and butter" compressor in my DAW. https://github.com/ZL-Audio/ZLCompressor
As well as ZLEqualizer.
Though I learned through porting the Vital reverb that porting C++ to Rust is quite time consuming. I'm probably just going to set up a build system to compile the C++ code into a static library.
But I should focus on actually finishing the core DAW first. 😅
Probably
A lot of the backend is done. It's mostly the frontend that's been the blocker.
I'm in a lucky situation where my parents can support me financially for years to let more work on it full time. I of course want to find a way to make a living out of it someday, but that would be much easier to figure out once I have an actual product.
It also helps that I'm single and can live comfortably at my parent's place. I'm aromantic, so I'm not interested in "traditional" things like getting married and raising a family.
More the norm
Also the cost of living in the country in Kansas is quite low compared to the rest of the country.
Let me know if you want advice on this in the future 🙂
(Of course medical insurance is still much higher than it should be.)
Will do! In addition to a Patreon, I've had some ideas like making an integrated store for samples and presets. Or maybe do what Blender does and sell tutorials.
We need an Alice podcast like pronto about Bevy. She’s so hip.
She has same insights as you.
And also maybe try a yearly fundraiser like what Krita does.
Maybe just wait for Bevy GUI
And personally I could get by with just $20-30k a year.
More would be nice of course, but I'm in open source for the passion, not the money.
right
I feel like because I don't want to get married or start a family, my life would just feel unfulfilled if I just sold my soul to a corporation.
Though that being said, if I can't raise money myself then I might be able to get a part time job or something.
What's also lucky is that my parent's property actually has two houses on it. So once my sister finishes her degree and moves somewhere else, I'll probably move there. Granted it's a tiny house, but I don't need anything more than that.
Though it would be kind of nice to live closer to a city. But that's not absolutely necessary.
Well …
It’s
Kansas
Kansas has some cities 😎
It's a 30 minute drive to the nearest town with a grocery store, and a 50 minute drive to the nearest city.
Though the countryside is very pretty. That's where I got the inspiration to name my projects after native flora and fauna in Kansas (Meadowlark, Firewheel, etc).
That’s like suburb without suburbs
Oh yeah and I'm calling the audio engine in Meadowlark "Dropseed", which is a type of wild grass, and it's the seed to make SICK DROPS 🤘
Yeah, GUI sucks
dang
But it's necessary, and I'm really passionate about getting both the UI and the UX right.
And plus there are some fun parts of GUI, like designing the themes, and doing custom shader stuff to render waveforms and visualizers.
I also want to have a "command palette" like in VSCode. That's something I've never seen a DAW do before.
But that's a later goal, if I even get around to it.
So wha is MAX channels?
What do you mean?
Well initially I'm supporting mono/stereo channels, but the audio graph engine can support any arbitrary number of channels.
As for bit depth, I'm planning on 32 bit initially, and then work on 64 bit later.
And in reality, there's actually very little quality difference between 32 bit and and 64 bit audio processing, so little that it is hardly audible if at all. The much bigger factor to audio quality is the quality of the antialiasing algorithms.
*and the quality of the filtering algorithm. For example SVF filters produce a higher quality output on 32 bit than biquad filters.
Plus 32 bit has a performance advantage. You can fit twice as many 32 bit floats into a SIMD vector than 64 bit floats, and you can fit twice as many 32 bit floats into the CPU cache.
64 what? Are you talking about f32 vs f64?
Well, a SIMD vector on SSE2/ARM neon can fit 4 f32s or 2 f64s. On AVX you can fit 8 f32s or 4 f64s.
Any rust SIMD and multi threading learn tips?
Well I'm not sure about SIMD specifically, but I have been compiling a list of audio development learning resources over the years. https://github.com/BillyDM/Awesome-Audio-DSP
I’ll check them out. Thanks.
A quick search also brought up this which looks good. https://medium.com/@Razican/learning-simd-with-rust-by-finding-planets-b85ccfb724c3
And rust also has an experimental cross-platform SIMD library in the works. https://doc.rust-lang.org/std/simd/struct.Simd.html
I’m pretty sure we can largely hide the scheduling from the user when they don’t care about it, or if they apply animations with some animation API we write.
That is, they’d still just mutate a simple set of parameters. Everything else would happen in the background. Anyone using bevy_seedling now could probably migrate with zero code changes.
However, they’d now have a big new set of capabilities due to the arbitrary scheduling, if they wanted it. And we would build the animation API around it, which would allow very easy performance tuning at runtime (if you’re applying a slow animation, maybe you only need events every 16ms).
I’ll want to double check my assumptions here, and I think we’d need to make those changes I suggested to Diff maybe, but it I’m pretty sure it would just be a straight win for users on the game side.
idk if it would change how easy it is to write nodes, but hopefully not by too much since it seems like a really powerful approach
Oh I love this idea
Yeah that style of UI element is very helpful for new users. Blender has something similar.
I think it’d be possible to write a trait that basically does the process_range method from the volume processor and automatically implement AudioProcessor in terms of that, which makes most processors easy to write but still allows you to implement AudioProcessor directly if you need more control
Which also lets us have some helpers around receiving patch events, like there could be a type Node: Diff + Patch; assoc type instead of manually doing events.for_each_patch::<SomeNode>
That might be a little too much coupling. Separating them out like we have may add a single line of boilerplate per processor, but it makes things very flexible. I'm a little wary of abstractions that are constraining at this point (I've written myself into a corner more than once!).
Oh but also I don't think you could just apply patches in bulk like that if they're time sensitive. But maybe I need to check your PR.
Oh I’m talking about a secondary SimpleAudioProcessor trait and then an impl<T: SimpleAudioProcessor> for T
As a helper in firewheel-nodes not in core
So the way that param updates are done in the PR is to process up until the next event and then apply all the param updates that occur at the same time in one batch. Since events for different fields aren’t necessarily correlated, the solution I had in mind to prevent accidentally processing small ranges is some kind of opt-in quantisation for param update times, I don't think it should be enabled by default but either Firewheel or seedling (probably Firewheel, as then asserts can be added as close as possible to the process call in order to allow LLVM to optimise better) should be configurable to quantise events in order to minimise how often we process a very short range
I don't know if this would be performant, but maybe the event quantization time could be on the configuration struct for a lot of nodes.
Yeah it’s something that would very rarely change and it’s an optimisation anyway. Seedling can ensure that it always produces events quantised to time, it doesn’t need to be a feature of firewheel except for the sake of paranoia and giving extra info to LLVM
I’m not even sure that LLVM would use the info unless it’s a constant
Long story short, I don’t think it’s worth putting in Firewheel unless we find out later that it’s an issue
If the performance is potentially better, and it's not too difficult, it seems a lot easier to me to manage quantization in Firewheel itself. That would guarantee that even if someone was doing manual scheduling (which we should allow) and they didn't quantize correctly, it would still work great.
In other words, managing that on the processor side guarantees correctness not just for Bevy, but all Firewheel users.
For sure, I get that. It’s possible to add in a backwards-compatible way though, at first it’d probably be a global setting hidden away in the FirewheelProcessor and not directly interacted with by nodes (although it could be added to AudioNodeInfo down the line)
Btw, is there any plan to introduce latency compensation? If you ever want that then it might be useful to put it on AudioNodeInfo now, even if it goes unused, so that you can add it later without needing to communicate that developers need to retroactively add it to their nodes
I don't think BillyDM was interesting in doing that. Too complicated I think.
If you happen to need that in the meantime, it could be done with a LatencyCompensation node or something.
I figure it'll only show up in either niche scenarios where you're doing lots of varied processing, or via something like a limiter on the output chain, where that latency doesn't matter.
And in the former case, it's relatively easy to look at your graph and figure out where you'd need compensation, right?
Side note -- now that we have a limiter, would it make sense to automatically insert it before the output in the default configuration?
I have some changes in progress that provide a more opinionated default configuration, with a few additional options of increasingly minimal configurations. You can see that work here. Would a limiter with a small (1-3ms) lookahead be a good default for most games?
I think 5ms is fine for games, you wouldn’t want it as the default lookahead for music software but it’s less than a third of a frame at 60fps so it’s unlikely to ever be noticeable. I’d even be fine putting it at 10ms tbh, studies show that even trained musicians don’t start to misplay until you hit around 11-13ms latency
but yeah I’d be very happy putting it on the master by default or at least giving a method to add it when instantiating the plugin, I think almost any non-pro software that produces audio should have a limiter on the master by default
Keep in mind that it's added to all other sources of latency, which right now is generally no better than 50ms.
(round-trip anyway, idk how much of that is input latency)
Just want to interject real quick that for skilled rhythm game players 10ms is definitely noticeable, but I don't know enough about what you're talking about to judge if it can be fixed with lag compensation which rhythm games normally also have
It might even become more standard for non-DAW audio programs. If you haven't seen it, plugdata (a nice, modern GUI on top of Puredata) has a limiter on by default.
Interesting, is that including all other sources of latency (e.g input polling, OS audio)?
I think this is very valid! Although it would be so easy to change the default configuration (and probably wouldn't be used by bigger projects anyway) that I wouldn't worry too much about it. In other words, a rhythm game really should not use the default configuration, and it's so easy to change and customize that this (hopefully) shouldn't cause anyone problems where that latency (among other things) is critical.
Yeah, pretty much from when you press a button until the game registers it. However the way lag compensation usually works is that it allows you to desync visual and audio, usually in 1ms steps. So if I put the lag compensation to +5 it'll delay the audio by 5ms I believe
I don't know how rhythm games handle it technically and there's probably different ways of handling lag, but the end goal is always the same. To allow users to delay either the visual or audio aspect because their monitor is slow or something else is causing lag
This user's rough calculations put osu at like 50ms round-trip I think. Now maybe they don't have an optimized setup (and I don't know if anyone's replicated it), but it seems like it can be quite high even for a timing-critical rhythm game?
Yeah, if just delaying the audio so that it aligns with the visuals is good enough then the limit is how high (or low) the lag compensation slider will let you go I guess
Oh but you're saying games will also facilitate the opposite? i.e. delay the visuals to line up with the audio?
Well that's the same as offsetting the audio to play earlier isn't it? But yeah, rhythm games will usually let you go from some negative value to some positive value range
Could be that some games just delay the visuals, I have no idea how different rhythm games handle it on a technical level
Well, we can't trigger audio to happen before the user presses buttons! So if there's negative compensation, then it seems like the only thing that could be done is delaying the visuals.
But it might be more complicated than that.
I guess what's more important to me when making a rhythm game is getting an accurate timestamp for use in scoring calculation
Yeah that’s really common in rhythm games afaik, they delay the backing track and visuals
but sounds that occur in response to user input aren’t latency-compensated
I feel like it needs to be in Firewheel because it'll interact with scheduling samples in the past
You would have the total latency, though -- you could calculate that manually.
Yeah with some recent changes to Firewheel, it should actually be fairly easy to calculate that quite precisely -- well within any error introduced by input latency / processing. In fact, we might actually need proper input timestamps to get any kind of real accuracy at all.
That’s fair actually
Ah, absolutely that is true and I didn't explain myself well enough. When configuring the lag compensation it's often the music that is offset, but it would not surprise me if some games also have it affect the hit sounds. It's been a while since I last played one
Actually, something that I thought might be useful in Firewheel would be multiple coexisting custom_states, using something like type_set::TypeSet. So that nodes can have some shared metadata types that can be read generically. That would be useful for latency compensation because a third-party library like seedling could provide a LatencyInfo type that nodes can set as a custom state without affecting any other custom state they have
Although for Seedling specifically it can just define that as a component and nodes can configure it using the required component system
That doesn’t work for non-Seedling users but if it's necessary later then afaik it'd be a backwards-compatible change to go from a single custom state type to many custom state types so we can safely kick that can down the road
Btw corvus, I made a dumb one-line change in the last commit to my limiter PR that broke it and I’ve submitted a PR to fix it
Delay compensation is a number z = x + y that has its own x delay and group delay y to keep everything in sync in audio where heavy CPU algorithms will add delay y to everything.
Just talking audio
Shared node state was discussed here maybe a week or two ago, but I don't think anything actionable came out of it.
I figured that could just be handled manually (i.e. find a way to get that shared state into the audio processor on construction). That approach wouldn't couple it to any frontend, even if it can be a little cumbersome.
I don’t think using a TypeSet couples it to any frontend, part of the appeal to me is that, as far as I understand it, it’s frontend-agnostic
Not going to annoy BillyDM with an @, but if you see this I think the scheduled events PR is ready
No that part would be fine, I was talking about your followup.
(The component-based appraoch.)
Right, that’s kinda exactly what I was saying though: seedling has a solution and it’s the main "power user" of firewheel right now so it’s not super pressing, but having state represented as a set of types is nice for other users so it’d be nice to have eventually
Latency compensation is less about calculating latency and more about traversing the node graph to figure out where to apply compensation
Once you’ve figured out those points, calculating how much latency to add to them is pretty easy
Well actually it is possible, I just didn't think it was necessary for games (you generally already know what your final audio graph will look like, so you can just insert a delay node manually if you need it). That being said, I do need to create a delay compensation node. That's very simple to do.
Well again if you already know the final structure of your audio graph, you could account for the delay.
Yeah, this gets into pretty heavy graph theory stuff.
Yeah, I'd have it on by default. It's simple enough to add a setting to the config to disable it if the user wants.
On the first iteration of the audio graph engine for my DAW, we actually messed up on the algorithm and ended up doubling the amount of latency instead of correcting it. 😅
@obsidian tusk The PR looks good. My one nitpick is I'm not sure SimpleAudioProcessor is the best name for that trait. Maybe something like AutomatableAudioProcessor or AutomatedAudioProcessor?
Also, I guess it wouldn't hurt to add a latency field to NodeInfo in case we do decide down the line to add that feature.
Hm, would this work with nodes whose latency changes dynamically?
Ah, no it wouldn't.
Aside from that, having access to the latency like this would also give frontends a standard way to access and account for it.
Even if Firewheel itself isn't using it.
(So it would be super useful to have anyway.)
In the CLAP spec (and in my DAW engine), plugins are required to deactivate first in order to change their latency dynamically. That would definitely be complicated to do in the Firewheel engine without adding a lot of complexity.
Much of the complexity in a DAW audio engine is just doing bookkeeping on the state of CLAP/VST3/LV2 plugins. While I am planning on adding CLAP support to Firewheel, it will just be basic support.
And even then, CLAP plugin hosting might be best handled by a separate (or even a 3rd party) crate.
State machines galore!
Ah, I guess that's why limiter plugins tend to have a fixed selection of lookahead values instead of a little knob or slider.
But if by convention, node authors write their nodes such that the latency won't change after construction, then having access to a latency value in the node info would be perfect.
I think that’s misleading because A) it’s not full automation and B) you can support scheduled events without it
Simple is kinda not that great though. Simple compared to what? Without deeper knowledge of the crate, Simple doesn't really mean anything. (I'm also not the biggest fan of SpatialBasicNode. I think it should just be SpatialNode, and then if we have a more advanced one, we can give it a fancy name like HrtfSpatialNode.)
ChunkedAudioProcessor?
No for sure I’m not tied to that name, I was just saying automatable isn’t perfect either
Yeah, I'm not sure what the best name is either.
Maybe even something like EventSchedulerWrapper?
Batched?
Oh yeah, batched might be the best word to describe it.
Batched is good
I kinda like Simple even if it’s not as meaningful because it shows the intent as a convenience trait but Batched def explains what it does better
What about SimpleBatchedProcessor?
You’re ok with the overall design of BufferRange though BillyDM? I wasn’t sure about having impl Trait fields but it does make it nice to use
Oh actually I didn't look at that part that closely. I'll take a closer look here in a bit.
Tbh if we’re going to say batched I’d just call it BatchedAudioProcessor
I kinda like that ngl
Yeah, that could work too.
Hm, is itertools used in the PR btw?
oh the message i replied to is unrelated
It looks like itertools 0.14 is used in a number of Bevy crates, so it won't generally constitute an additional dependency for Bevy users, but it might for non-Bevy Firewheel users. But it seems like it's actually not being used at all.
I'm fine with itertools. Handy crate.
Ah ok, that is an interesting way to do it. I suppose it would be pretty difficult to do it with just lifetimes. I'm fine with it.
I can also check the real-world performance delta compared to Firewheel main if you want. I'd like to do a quick integration to see how the scheduling API might look in the ECS.
And actually, there is one more thing I thought of. It would be nice if there was a way to pause and discard future events.
Because currently there is no way to do that once an event is scheduled.
Actually yeah that would be kinda important if we buffer events on the game engine side.
i.e. if you're scheduling events 2-3 frames ahead of time for a pitch animation, but you suddenly want to pause or cancel the animation
you'd be kinda toast
The pausing can be solved by counting how many frames have passed since the event was paused for each event in the buffer, and then adding that number of frames to the event's instant when resumed.
And discarding of course would be easier. Though I think there should be an option to either discard all events, or just future events.
That would be awesome!
That being said, we should probably also add a method to the Diff trait that forces every parameter to send an event. That way we can ensure that the ECS state and the node processor's state stay in sync even if future events have been discarded.
Hmm, and I think it would also require a special event type for pausing/discarding to ensure that events are always in the correct order. If we just used atomics then the order of pause/discarding commands and parameter events could get out of wack.
oh like a refresh
Yes
You might just leave this to the integrators like bevy_seedling.
If we can inspect what events are removed, we can handle re-syncing any state that is outdated.
Potentially mutating the representation in the game loop.
Well you would have to keep track of what events have been sent.
Still, it would be nice if there was an easy way to refresh.
Hm, so we couldn't "recover" the discarded events once they've been sent?
Well, the events are sent to the audio thread.
While I guess in theory we could send the events back to the main thread, there would be a delay.
Well either way, I think a refresh method would be easy? We can derive it in essentially the same way we derive the normal diff behavior.
Yeah, that's what I was thinking.
And yeah it would be a simpler design if we don't have to send back cancelled events.
I also don't think the "pause/resume/remove" events need to have an optional timestamp. I think it makes sense to just pause/resume/remove as soon as the node receives the event. Correct me if I'm wrong though.
Keep in mind that all events are batched and flushed to the audio thread at once, which ensures that all nodes receive those events in the same process block.
Hm, the exact details there do seem tricky
Yeah, it is a bit tricky.
What's the simplest approach? Just a Cancel event? Is it impossible to managing pausing from the game-logic side in the general case?
I suppose the problem is you wouldn't necessarily know which events have been consumed by the audio processor.
Exactly.
But I suppose we could get away with just a Cancel event. The game engine could re-schedule the events if needed.
Oh yeah, and by the way there is no need to pause InstantMusical events because the transport itself can be paused.
But having the ability to pause/resume events would be nice. But if it ends up being too complicated to implement, we could just use the cancel approach.
I think we might just get away with it. In practice, we'd typically only evict maybe 4-8 events with a Cancel, and since the whole animation would be paused, the slight jump you'd observe when unpausing probably wouldn't be noticeable.
Yeah, and the only events that really require exact timing are the musical ones, but those can be paused anyway.
Yeah, I should’ve explicitly mentioned it in the PR description but it might as well be part of std at this point. It might actually not be used in the PR any more though, let me check because I think I might have gone with a different implementation of the thing that originally required itertools
That would be great!
There are a lot of things that should be in the standard library (especially rand and log).
And smallvec and arrayvec.
Unless I’m misunderstanding how events work in firewheel, I think you need to reschedule the events again every frame bc they’ll be cleared after processing is done
Which might be a reasonable implementation but I don’t really know
My understanding is that events are drained out of the buffer by the audio processor. If the audio processor doesn't read them, they build up. But I could be wrong.
Also thiserror, bitflags, anyhow, and derive_more.
Ah ok that makes sense
Oh wait, actually you're right. Glad you caught that. The node needs to put any future events into its own event buffer in order to use them in the future.
By that I mean the node's event buffer gets cleared at the end of a processing block.
I think for seedling's usecase that’s ok, right? Seedling can schedule more events than are absolutely necessary for the current buffer period but it doesn’t need to persist then between frames
Hmm, it might be possible to create an API where a node can request the Firewheel processor to hold on to an event.
What I'm saying is that the Firewheel processor itself clears the event buffer at the end of the process block. So the event won't exist in the next processing block.
IMO it’s a reasonable restriction that scheduled events are only valid for the next update call and you need to reschedule them
But again, that is not a robust way to schedule animations.
That will result in gaps with unstable framerates. (If we can't schedule a little in the future.)
I'm not talking about rescheduling. I'm talking about that the way your PR is set up, you won't even get any future events. They will be discarded before you can use them.
Keeping track of which events have been consumed and which haven’t seems like a huge source of gotchas
True. The only other way though is to require the node to create its own event buffer.
Uh, I’m not sure I understand why? The scheduled events are intended to be for sub-buffer-period precision not to persist events between frames, which can already be handled by the code that’s sending the events
Yeah, it’s supposed to be a good-enough solution for the general case and a way to let the user configure buffer size without affecting param update rate
Oh, I see what you're saying. Still, that seems like it would be a confusing limitation.
Hm, you’re not wrong there. I wonder if just documentation would be enough to solve the confusion
Especially since on some platforms (like Windows) the size of the process buffer can actually vary from block to block.
(Annoying I know, but Windows is Windows)
But it's a fundamental API limitation that, again, cannot be solved by the user code. Maybe I should create a diagram to illustrate the problem.
If the external code owned the event buffer and passed it into FirewheelCtx::update that would make the usage clearer
Then firewheel doesn’t need to decide the event buffer clearing strategy
I started Windows compiling only last week
However, say if you had a video frame that lasted 15ms, but the audio processing block only lasted 5ms. You would lose 10ms of scheduled events.
Yeah, I still maintain that it’s fine to just not handle that. It’s better than the status quo where you lose 15ms of scheduled events because you can’t schedule them to begin with 😅
Also, because there's no synchronization between the update rate of the audio and game, you'd actually lose events all the time, even with stable frame rates. (Unless I'm misunderstanding the mechansim.)
Though I suppose the typical frame time for a video game is 16ms and a typical audio block time is 23ms (1024/44100).
Yeah I was originally thinking it’s fine to just reschedule each frame but I’m now realising that doesn’t make sense
Hmm, yeah. This is turning out to be quite complicated.
Though like I said, I think it would be possible to create an API where nodes can ask the Firewheel processor to hold on to events for the next processing block.
IMO the ideal is that the frontend only ever schedules events for exactly the buffer period and the processor always processes all the events. Like, that would just be an invariant of the API. If that’s a reasonable invariant is another question
But I don't think it's possible for the frontend to know exactly what the buffer period is.
Well exactly 😅
Firewheel has the timing information, it can handle that itself right?
And even if you knew the buffer period was constant, you don't know exactly where you are in that buffer period (you could be in the middle of one, or you could be right before a new one is about to start).
Yep I def don’t think the buffer period should ever be assumed to be constant
Hmm, yeah I suppose it could.
Either that or, if we only clear events when the node pulls them off the event list, the node could just "peek" until it gets to the end of the queue or the next event is beyond the current processing period.
I'm ignorant of the implementation, so I don't know how easy that is.
Well, the node will still pull events if it gets a "process immediately" one.
Would it be terrible to have two event queues? One with timestamps, and one without?
I’m not sure that’s necessary tho
I think even just returning a list of events indices you wish to be kept the next block is good enough.
Could we fit a "keepalive" flag next to the events in the list, and clear any where it's false?
stopped = true
I’m still having trouble understanding why firewheel can’t just remove any events that are within the just-processed period
del
Like, why the nodes need to think about that
last sample played then delete
Like this:
pub struct ProcInfo<'a> {
// ...
// This is a pre-allocated Vec that will be empty. The user pushes
// the indices of events it wishes to be retained in the next
// process block.
pub keep_event_indices: &mut Vec<u32>,
}
Oh, that might work too.
That feels like a lot of bookkeeping and I don’t see why a node would ever keep an event alive for reasons other than it being after the end of the buffer period
That's exactly the reason for having it.
You want a scheduled event to be scheduled even if it hasn't elapsed this processing block.
Like, it’s one thing if it’s always hidden inside the helper trait but any processor that directly implements AudioNodeProcessor and wants to handle scheduled events now has extra boilerplate
Right I’m not saying you don’t schedule it
I think @obsidian tusk's suggesting that the processor's caller can just see which events happened during the block it just processed, and only remove those.
No, I don't think it needs extra boilerplate. The flag will be set to false by default.
But if you wanted to clear the events for that node, you wouldn't be able to do that if the Firewheel processor handled that.
Though I suppose we could add a DiscardEventsForThisNode event that gets sent to the Firewheel processor.
Hm, I see. Do you have some objects or scenarios in mind where the node would want to discard a bunch of events?
Node deletes sample
Well, if you have an animation scheduled, but you want to pause or stop that animation.
Oh yeah I guess I just assumed that would also be handled by the Firewheel processor. Is that unreasonable?
Currently no, that's not handled by the Firewheel processor.
But you're right, I think it might be possible to have the Firewheel processor automatically do this. In fact, we might not even need the fancy BatchedAudioProcessor trait, the Firewheel processor could just call the node's process method multiple times as necessary.
Oh one thing I was thinking -- Cancel should only remove events with timestamps, right? Or maybe just the ParamData variant? Like, I don't think we'd want to drop a sampler sequence event.
Correct.
We could even have a CancelAllEvents event for convenience.
If you did it this way... would we even need to update the processor code at all, actually?
It would kinda just look the same, woudln't it?
Though one thing the user would need to make sure of is that they call FirewheelCtx::update before they schedule any new events.
You could apply patches once at the beginning of the method, then process the audio and return whatever silence flags.
i.e. exactly how it looks now, but the audio buffers may just be smaller
Correct. As long as we can make it work with the ECS, then we wouldn't need any extra work or custom Timeline types for node authors!
yes
Oh my goodness!
hmmmmmm
that would be kinda cool
Of course the question is if it would work with the ECS. Obviously the Timeline type is handy in a data-driven environment.
(This is actually what I was talking about when I said that Firewheel could automatically schedule events for nodes if it used an event-driven API).
ah i see
My intuition is that it should be no problem (Bevy's ECS is very flexible), but I'll prove it out here soon.
And for the standalone APIs, the Memo type should also keep it simple.
In theory we could require node authors to create a Bevy wrapper struct for their node type that uses the Timeline type. (Unless you have a better solution in mind.)
Oh yeah I don't think we need that at all
Oh right
Is it a reasonable implementation to, when pushing a new event, delete any events that happen later than the just-queued event? Or is scheduling events out-of-order something we want to support?
Awesome, I'm getting excited about this! This would make it so much nicer to create custom nodes.
If it's not complicated to allow, out-of-order would actually be nice imo.
Ok fair enough
Yeah, the Firewheel processor can automatically sort events.
Imagine you're animating one param while applying simple changes to another.
Depending on the order, one might be scheduled "back in time" relative to the other.
Even though, once sorted, they'd both be reasonable.
Hmm, although sort_unstable might not work here, as you probably want to retain the order of events that happen at the same time.
Yeah, you want to handle it param-by-param so you don’t want to care about order
I think that’s fine, it’s a very short list so a stable sort won’t be slow
I only used sort_unstable because that’s what I default to
Though then again that is only a problem if you have a multiple events for the same parameter happening at the same time.
Does stable_sort allocate?
No I think it's just slower sometimes
Oh great question, I’m pretty sure it doesn’t
Oh ok, cool.
Yeah exactly
Worth investigating but I don’t think it does
I think it just uses a different algorithm
Oh wait, no it does allocate. When applicable, unstable sorting is preferred because it is generally faster than stable sorting and it doesn’t allocate auxiliary memory. See sort_unstable. The exception are partially sorted slices, which may be better served with slice::sort.
Ugh ☠️ Ok
Well you can use an incrementing ID to work around that but it’s annoying
Although I think it’d be possible to make an API that allows you to not guarantee maintaining insertion order for now, but add it as a guarantee later if that’s desired
If a frontend is only scheduling events using the patch mechanism then the order that patches are generated is already not guaranteed
Yeah, I think that's what I'll do.
So you could just say that you don’t guarantee maintaining insertion order for simultaneous events but if that becomes an issue you can add that guarantee later
Hmm, the stable sorting wouldn't be a problem if there was a way to give the algorithm a pre-allocated buffer. I wonder if there are any crates that let you do that?
Ooh, interesting idea
If anything we might be able to implement our own stable sort algorithm that takes a pre-allocated buffer.
Yeah I don’t see why that would be too hard. It can be done later though
Apparently there are two algorithms that can stably sort a list without allocations (at the cost of being slower) called GrailSort and WikiSort, but there doesn't appear to be a Rust crate for those.
The algorithm’s speed probably isn’t too much of an issue considering the number of elements is small
but I think this is premature optimisation, I think until it becomes a problem then there just shouldn’t be a guarantee of preserving insertion order
To elaborate on this -- we already store a mini event queue per node in the ECS. It's on the same entity, essentially like:
(VolumeNode, Basline<VolumeNode>, NodeID, Vec<NodeEventType>)
We use the baseline for diffing, pushing the patches to the adjacent Vec<NodeEventType>. This does involve allocation for every node, but the flipside is that all nodes can be Diffed in parallel at once, without accessing the !Send audio context.
Typically, you'll modify a parameter in a system like this:
fn mute_volume(mut nodes: Query<&mut VolumeNode>) {
for mut node in &mut nodes {
node.volume = Volume::SILENT;
}
}
Notice how we don't touch the baseline, node ID, or vector of events. For users who don't care about scheduling, this would be the same regardless of whether events have timestamps.
For users who do care, we could change the type of the local event buffer. Instead of a Vec<NodeEventType>, it could be some EventBuffer with a method like diff_scheduled. Then, if you want to manually schedule events at a particular time, you'd do something like:
fn mute_volume_scheduled(
mut nodes: Query<(&mut VolumeNode, &mut Baseline<VolumeNode>, &mut EventBuffer)>,
time: Res<Time<Audio>>
) {
// Let's schedule in the future by one frame
let instant = time + Duration::from_millis(16);
for (mut node, mut baseline, mut events) in &mut nodes {
node.volume = Volume::SILENT;
events.diff_scheduled(&node, &mut baseline, instant);
}
}
(If you're not used to systems, the query might look scary, but it's really not that bad.)
Now, that said, users probably wouldn't have to interact with the scheduling 99% of the time anyway. We'd have a higher-level animations API that handles breaking up Diffs and scheduling them.
(I'm sure the example could also be way better, this isn't a refined idea yet.)
So the approach in the PR is to have a TimedEventQueue (can't remember if that’s the exact name) instead of modifying the diff trait
Where TimedEventQueue wraps an &mut T where T: EventQueue, so you tag the queue with a time just before passing it to diff
It’s only implemented for ContextEventQueue (again, can’t remember exact name) for now though, I think to make it fully generic you’d need to modify the EventQueue trait
Oh yes that's actually what I was imagining for this, more or less.
I guess the details aren’t super important
If seedling already has a buffer of events, could the event queue be taken out of the firewheel context entirely, with the frontend being expected to handle all that stuff and pass a reference in?
or is that too onerous of a burden on non-seedling frontends?
I think it's probably still good to have the event queue on the context.
Fair enough
Yeah it actually has really good support for spatialisation
awesome
Check out SpatialBasicNode and SpatialListener3D https://docs.rs/bevy_seedling/latest/bevy_seedling/spatial/index.html
Spatial audio components.
Oh that reminds me, I want to add some delay for the speed of sound and simulate doppler shift.
Those can both be handled purely by the ECS
Sick, that’d be useful for racing games
and I’d love to integrate that into my quake engine because it’d make getting up to crazy bhopping speeds more satisfying 😅
One of my favorite gaming experiences was the impact of the speed of sound on combat in PUBG back when it came out.
Though to be fair, it is a pretty basic spatialization algorithm (just volume, panning, and filtering). I don't have the DSP chops to do any fancy HRTF or anything. (Though that being said, the Fyrox engine has an HRTF implementation we could borrow).
If someone is interested, we could also integrate steam audio into some nodes. It's a C dependency, but it's very high quality, and someone made nice Rust bindings.
I wanted to integrate steam-audio for that bc it’s got a v efficient hrtf implementation but it’s a lot of work. I think I might have mentioned that before
oh
Ahaha
XD
Great minds
Yeah valve understandably put a hell of a lot of work into it for HL: Alyx
The occlusion stuff is particularly interesting to me with my quake engine, it’s the kind of audio effect that sounds great but doesn’t feel out of place in an old game even if it’s anachronistic, which fits the goals of the project very well
kinda like raytracing on old games
Basically I want it to be "how you remember quake being"
By "occlusion" do you mean dampening a sound to make it sound like it's being played from behind a wall and such?
I hate it 😅 I think darkplaces looks awful too
Yeah, steam audio has a really fantastic system for it
It's not even just dampening, you can bake reflection information and everything.
Oh, neat.
In my simple spatialization node, there is a parameter to dampen the sound (literally just a lowpass filter).
Yeah true, it’s pretty amazing. The audio tech in HL:A is such an underrated part of it, I didn’t even get to experience it with head tracking etc I just put on headphones and watched gameplay and I was blown away
And I of course would like to see an ecosystem of Reverb nodes for better realism.
A convolutional reverb would be especially useful, since you can replicate almost any space with it (at the cost of some CPU usage).
Yep a general convolver node would be great
Aw that's too bad. I played it when it came out! I don't think it's too controversial to say it's the best VR game ever made (though some die-hard VR users will say it's basic).
Yeah I hope one day I’ll be able to justify getting a VR setup
rfd, ring buffer, and the math for convolution reverb, I think
but it’s not worth it for like 3 games that I’m interested in, all of which are linear games that I’ll play once
VR Chat is kinda cool if you know people who use it. They have like... art exhibits, cute worlds, literal raves where you need to know someone to get in. Kinda wild, actually.
I live in berlin, we have all those things irl 😅
That’s true.
I still think the culture around VR chat is fascinating though
yeah but it could be ✨ virtual ✨
I barely know anyone who plays games, let alone VR chat
Anyway, hopefully VR picks up again. Seems to be in another big trough right now.
Yeah I want it to become worth investing in because it's amazing when it works. Every time I get a chance to use it I love it and on a technical level I think it's fascinating
Okay so.... the baseline performance with the new changes is essentially identical. Is that expected?
Here's the new plot -- you can see the old one here. Looks like any differences are just within the noise threshold of my system.
If my interpretation is correct, this means that having the ability to schedule finer animations does not impact the baseline performance in any meaningful way. I also ran a quick test with volume fades on a few samples, scheduling them for just the current time. I saw no difference in performance between the scheduled events and Firewheel main (where these events can't be scheduled).
In summary, it looks like this general approach will not distrupt the overall performance of Firewheel. We may observe lower performance on a per-node basis when we schedule animations, but that's essentially eating our cake and having it too; the user has the ability to finely tune how performant their animations are versus how smooth they are, all at runtime.
(Oh and to be clear, I doubt sending a dozen or so events per frame to achieve a fairly smooth animation would really harm the overall performance that much.)
Wow! Thanks so much for checking this!
Would it be possible for you to the performance if you schedule, say, 16 updates per frame? My intuition would be that it'll still be fairly efficient
let me just say I am not savvy with audio programming, but I enjoy learning from your discussions 😄
I think we’re all learning as we go too 😅
I’ve been doing audio professionally for years but there’s still far more stuff I don’t know than stuff I do
Okay so here's the setup:
- 48 total samplers in the graph.
- 4 samples playing, all fading in over three seconds.
Results:
- 1 audio event per frame: 108us per block (512 frames)
- 16 audio events per frame: 112us per block (512 frames)
This is recorded over just about three seconds. The time represents the total audio evaluation time, so it's only a coarse indicator of performance for individual nodes. Note that the 16 events are typically truncated to 8 or so per buffer, given the lack of synchronization and current inability to schedule into the future.
Oh I should probably mention my game frame time is 8.4 ms.
Once we're able to schedule a little into the future, I realized it actually gives us some really nice properties.
For example, we can reliably ensure that audio events are processed at a fixed rate, regardless of frame rate or frame variability. For example, if we settle on a default rate of 16ms or so, then animations driven by the ECS can effectively guarantee that the events will arrive every 16ms whether you're running at 120fps or 30fps. Note that a rate of 16ms could actually provide better performance than Firewheel main on my computer, since my display typically pushes 120fps.
This would ensure the audio experience remains exactly the same regardless of these external factors, excluding some extreme cases like freezes that last several frames.
Oh wow that’s even better than I expected
Yeah again it's not exactly the clearest side-by-side comparison, but it seems... totally fine? I think even if you animated every sampler with fairly small time steps, you wouldn't see anything crazy like an order of magnitude worse performance.
In fact, I think you'd still see significantly better performance than rodio 😅
Oh wow that’s even better than I expected
It makes sense though, the worst thing about decreasing buffer size isn’t the higher param update rate, it’s that you have to pay the cost of traversing the graph every period. So long as each chunk is larger than the vector width then you're only paying for a few more branches every period to handle the param updates, and with volume then you might not even be paying for that bc iirc there’s only one param. Tbh after seeing these results I wouldn’t be surprised if the cost to schedule an update for every sample isn’t so terrible, Timeline is almost certainly better for that ofc but at least you’re only paying for the graph traversal once. Should be discouraged but maybe quantising event times isn’t necessary
We have a strategy at work to massively reduce graph travel cost bc we need to have good support for low buffer sizes but I don't know if it’s proprietary so I’ll hold my tongue
It’ll still likely be slower than this tho, albeit with the benefit of lower latency
(To be clear, the scheduled events PR is a novel design and not inspired by anything with potential to cause legal issues)
Oh btw there were some errors in the implementation that I had to fix to get scheduled events going. Basically, the proc info needs to be updated to reflect the smaller number of frames and the time range subset, but the PR currently just passes the outer proc info.
I might make a note on the PR about that. It's currently kind of annoying to re-create the proc info struct, even though the entire thing probably should be Clone (and also Copy). We might add a method on it to nudge the number of frames and time ranges to the correct values.
Yeah I put frames in BufferRange for that reason but it’s def not the best way to do it, it should be more transparent
I noticed it with the spatial node in particular because it uses the proc_info frames to index directly into the buffers instead of iterating over them.
I’m wondering if there should be a way to specify that an input/output should be interlaced/deinterlaced, with some helpers to when the processor doesn’t care. Like, a volume node would prefer interlaced input/output for vectorisation/cache coherency reasons (and the existing implementation by BillyDM does interlacing for mono and stereo). It’d just be a hint though and all processors should handle input in any format
Sorry that’s not really relevant here except that it could potentially improve cases like spatial audio where you need to index directly into multiple channels
Since directly indexing into every channel with the same index will be faster with interlaced input
handling buffers of multiple formats might be slower overall tho 🤷♀️
ya maybe not worth it
Okay so I have some diagrams to illustrate why I've made the assertions I have.
This first one is a simple illustration of frame times vs audio block times at normal rates. At the start of each audio processing block, the audio processor advances its clock to the next block for external reference:
let mut clock_samples = self.clock_samples;
// The sample clock is ultimately used as the "source of truth".
let mut clock_seconds = clock_samples.to_seconds(self.sample_rate, self.sample_rate_recip);
self.clock_samples += DurationSamples(frames as i64);
self.sync_shared_clock(Some(process_timestamp));
It then runs the audio processing with the clock_samples variable (not self.clock_samples) so that by the end of the block, everything is caught up.
In this scenario, if we only scheduled animations for one game frame's worth of time, notice how we already fail to achieve any kind of smoothness.
Typically, the audio events are flushed from the game to the audio thread once near the end of the frame. So in frame 0, we'd successfully send events for most of audio block 1. The problems really start by frame 1.
By the time we actually flush frame 1's events (again, at the end of the frame), the audio processor will already be done processing block 1! So anything that was supposed to happen near the end of block 1 will be quantized to the start of the next block.
The same thing happens to frame 2, but it's even worse this time! Over half of the block will be missing.
Similar things happen when the block rate is a bit faster, although we'd start getting bitten hard if we can't schedule into future audio blocks.
Finally, look at what would happen if a frame takes twice as long to render as normal. If frame 1 doesn't schedule a few frames into the future, then two audio blocks (4 and 5) will be processed before frame 2 flushes any of its events.
btw @static quest I think this: https://github.com/BillyDM/Firewheel/blob/ee791718e51c35e48e0a037a88b35337ad9c1908/crates/firewheel-graph/src/processor.rs#L225 should be using clock_samples, not self.clock_samples, if I'm understanding the intent correctly.
I don’t think that’s worse than the status quo though, right?
The status quo being one event per frame (no scheduling)? I'd argue that's also not great in the event of a long frame.
But a long frame isn't really the core issue. If we have the tools to solve the other issues, which are frankly critical, then this gets resolved "for free."
No totally, I’m more saying if for some reason scheduling far into the future is a huge problem then I’d be ok with choppier params when there’s a slow frame. I think scheduling into the future should be ok though, I’m still of the opinion that firewheel should use the timing info to only clear the events that were inside the just-processed buffer period (and, during processing, to only pass events to processors that are within the current period)
Mm, I see. Although do note that we'd also have choppy params even without long frames. Any time the block rate is significantly faster than the framerate, you'd skip the delta between them on average. That is, if audio processing is twice as fast, you'd lose half of your sceduled events (assuming those event are evenly distributed over time).
Very true
Alright, so at this point, how are we looking to move forward? It seems like there are a few things we have a rough consensus on. I think the things we want to achieve are:
- Introduce optional scheduling for events.
- Introduce an event along the lines of
ClearScheduledEvents. - Adjust the Firewheel processor so that it breaks up processing for each node into sub-blocks, where events with a particular time are grouped.
- This requires event sorting, ideally without allocation.
- Also, the Firewheel processor should only clear the events that it allows the nodes to observe.
Please correct me if any of this is wrong.
Unless I'm missing something, it seems that achieving all this is surprisingly straightforward. As we discussed, this would allow crates like bevy_seedling to start scheduling fine animations without any changes to the audio node code.
These points are partially, but not fully achieved by your PR @obsidian tusk. Do you think we can build the rest of these features on top of it?
@static quest You mentioned you'll likely want to take a break from Firewheel. Do you mind if we iterate on this in the meantime, or would you prefer to take the lead at a later date?
1 and 3.1 are implemented but not 2 or 3.2, I could implement those two pretty easily I think
For 3.0, I think we can just remove the SimpleAudioProcessor and basically use that code for all processors, right?
Oh! I didn’t realise that’s what you were saying
Hm, I kinda feel like the flexibility of the regular AudioNodeProcessor is desirable
and I’m not sure what the downside is of having both
mm, API fragmentation
but yeah, it’s definitely possible
My understanding was that BillyDM was ready to forge ahead and break up the blocks like this.
but i might be wrong
I mean, after seeing the benchmark results I’m not totally against that
Yeah, again the baseline performance (when no subblocks were created for the SimpleAudioNodes, i.e. just one event per frame) was exactly the same as far as I could tell.
I don’t think it’s fragmentation really though, or if it is it’s not that terrible. It’s a fairly common pattern to have a "simple" and "advanced" API with the former being implemented in terms of the latter
but I guess AudioNodeProcessor could be kept but made private and we can consider our options later
I've been feeling sick the past couple days, so I'll get back to this later
Here's a couple node ideas I had. I'll write them down here so I don't forget 🤣
- LUFs node
There's a crate that looks really nice for perceptual loudness analysis: https://docs.rs/ebur128/0.1.10/ebur128/index.html. It would be super easy to wrap it up in a node. It might actually be craaaazy for mixing productivity, especially when we can give it a GUI.
- ITD (interaural time difference) node
One of the ways we localize sound is through the time gap between a sound arriving at each ear. The gap is pretty small (a maximum of ~650μs on average, or ~30 samples at 44.1k), so we could easily create a pretty performant node whose sole purpose is to produce this gap. It's still not full-blown HRTF or anything, but you could compose it with Firewheel's existing spatial node to get a slightly more convincing effect.
(It's actually kind of awesome that we could so easily compose more sophisticated spatialization with combinations of nodes. You could mix and match which effects you want depending on performance requirements or artistic goals.)
Oh yeah almost forgot (reminded by some fireworks) -- I'd also love a component that properly delays a sample's start time based on its distance. That would be super easy.
Oh yeah, fantastic idea, that’d be super useful
🌩️
Oh you're right. Good catch.
I'm feeling better now, so I can work on this again.
So have you gotten the built-in Firewheel node types like VolumeNode to work with Bevy's ECS? If so, I can move forward with making the Firewheel engine automatically schedule events for nodes.
There might be some breaking changes, so I'll let you know when I'm done.
Also yeah, I love the idea of a LUFs node.
If it can wait a couple more days I can make the final few changes, my mother in law’s here right now so I don’t have much time after work and what I have I’ve been spending on my quake engine
We could also add an "RMS" node. It's not as accurate as LUFs for measuring loudness, but it is a lot faster to compute.
Yes, at least with some foreknowledge of the parameters. That's where I was gathering these performance metrics from that I'm replying to.
In other words, I knew the shape of the volume object, so I just manually interpolated its volume like this:
for i in 0..steps {
let elapsed = (previous_elapsed + time_step * i as f32) / total_duration;
// we'll break up lerp into a bunch of steps
let baseline = chain.fx_chain.spatial_basic;
chain.fx_chain.spatial_basic.volume =
Volume::Linear(fade.event.start.lerp(fade.event.end, elapsed));
let instant = now + DurationSeconds(time_step as f64 * i as f64);
chain.fx_chain.spatial_basic.diff(
&baseline,
Default::default(),
&mut queue.with_time(firewheel::clock::EventInstant::Seconds(instant)),
);
}
You can see that this isn't the most efficient way to generate these events -- we're diffing the whole struct every time even though we know we're only changing one parameter. Obviously for volume, that doesn't matter (only one parameter), but we could make it nicer for larger sets of parameters.
I think that would look like some small addition to the Diff trait that allows us to break up diffs into chunks. However, I'm fairly certain that can be almost entirely separate from whatever changes need to happen elsewhere in firewheel-core.
(Oh I actually already implemented the LUFs node -- it's on an in-progress branch in bevy_seedling.)
Hm, actually maybe we don't even need to change the trait?
The events are a big enum. And it's pretty obvious which events can be interpolated and which can't (excluding type-erased ones, which I think it's probably fine to leave out).
Anyway, point is -- there are many ways we can manage the animation aspect, and I don't see any blockers with regards to how it fits in the ECS. For users who don't care about scheduled events, literally nothing would change!
If you don't mind, I'd like to implement this myself. This will require an intermediate "future events" buffer to do properly. I have an idea how to make that work efficiently.
I can fork your PR so that you'll still get credit for the changes you contributed.
(that seems like a good idea tbh!)
Actually working on this, I'm thinking that double-boxing ParamData::Any might not be as necessary anymore. The addition of timing information already increases the size of NodeEvent by 16 bytes, so reducing the final size of 60 to 52 by double-boxing doesn't seem as drastic.
Although actually, if it's not too much trouble, it would be nice if we could instead use ArcGc<Box<T>> or ArcGc<T>. Now that the event management is more complicated on the Firewheel processor's side, it would make it a lot easier if these types could be automatically garbage collected.
Another thing we could do is
pub enum ParamPath {
Single(u32),
Multi(ArcGc<SmallVec<[u32; 4]>>),
}
->
pub enum ParamPath {
Single(u32),
Multi(ArcGc<[u32]>),
}
idk if we could do both simultaneously
Very happy for you to take that on if it’s inspiring you!
The explanation for why it's a little weird currently is here in the doc comment:
/// When paths are more than one element, this variant keeps
/// the stack size to two pointers while avoiding double-indirection
/// in the range 2..=4.
Actually I think this doesn't change the total size
Oh yeah, that's better. I forgot you can avoid double-indirection by doing that.
with the timestamp
Yeah it doesn't appear to change the size.
so that's convenient
Hmm, it turns out that ParamData::Any(ArcGc<dyn Any + Send + Sync>) makes the generics in the diffing and patching quite complicated. I might just make it ParamData::Any(ArcGc<Box<dyn Any + Send + Sync>>).
Oh does it?
Yeah, I can't figure out how to go from, say, an Option<T> to an ArcGc<dyn Any + Send + Sync>.
Hmm, maybe if there was a way to convert a Box to and Arc.
Oh where does this happen?
This seems to work, but it's a bit complicated:
ArcGc::new_unsized(|| {
let a: Arc<dyn Any + Send + Sync + 'static> = Arc::new(value.clone());
a
})
In leaf.rs, I'm getting the error the trait bound ArcGc<(dyn Any + Send + Sync + 'static)>: From<Option<T>> is not satisfied
Adding this seems to fix the error:
impl<T: Clone + Send + Sync + 'static> From<Option<T>> for ArcGc<dyn Any + Send + Sync + 'static> {
fn from(value: Option<T>) -> Self {
ArcGc::new_unsized(|| {
let a: Arc<dyn Any + Send + Sync + 'static> = Arc::new(value.clone());
a
})
}
}
Hm, that's interesting. I didn't even know we has that impl
I don't think you need to clone though, do you?
since it takes it by value
It's because I think From<Option<T>> is automatically implemented from Box<dyn Any>, so it worked before.
(But yeah you shouldn't need the Clone bound unless I'm mistaken.)
Although we only use it in one place, so I could probably just construct the ArcGc there instead of implementing From<Option<T>>.
Actually, I can add this method for ArcGc:
impl ArcGc<dyn Any + Send + Sync + 'static> {
pub fn new_any<T: Sized + Any + Send + Sync + Clone + 'static>(value: T) -> Self {
ArcGc::new_unsized(|| {
let a: Arc<dyn Any + Send + Sync + 'static> = Arc::new(value.clone());
a
})
}
}
(Yes, although no need for the clone XD)
((also Sized is one of them auto trait bounds so you don't need to specify it))
Oh you're right, I can remove the clone.
I also realized we probably need to add a way to do realtime-safe logging. Though we can do that in a future version.
ya that would be nice at some point
Okay so without filtering, turns out ITD is pretty subtle 😅
I can definitely localize sounds with merely ITD (which is pretty cool -- I've never heard it demonstrated in isolation before), but the effect is dwarfed by volume adjustment.
but idk maybe it works better in context with more sounds 
Oh also, as I was working through this, I realized it's a little unconventional that SpatialBasicNode doesn't downmix the input to mono, then spatialized the mono signal.
Conceptually, it doesn't really make sense to merely attenuate the left and right channels of a stereo signal to spatialize it. That's essentially mixing two very different paradigms. It becomes particularly strange when using ITD -- you'd be introducing phase relationships that don't necessarily respect how things actually sound.
Now, there are more ways to manage this disconnect than simply downmixing. They tend to be a fair bit more complicated though, so I wouldn't want to mess with them right now.
rodio, for references, downmixes inputs to its spatial node before spatializing.
Of course, Firewheel is very flexible, so we could leave it as-is and allow users to decide how to manage inputs to the SpatialBasicNode. The new ItdNode in bevy_seedling does this downmixing, so if it precedes the SpatialBasicNode in a processing chain, then it all works out.
Another solution would be treating the left and right channels as individual emitters, and allowing them to have some amount of separation.
(Where a lot of sounds could just have a separation of zero.)
Ah yeah, you're right. I didn't think about that when making the SpatialBasicNode.
It would be pretty simple to add an option to the config of SpatialBasicNode.
ya that probably makes sense
It could even be a parameter, if for example you wanted to downmix one track but keep the stereo separation for a different one.
Oh yeah, I also need to add a bitfield similar to SilenceMask to let the plugin know which channels are actually connected.
Not that Firewheel itself would do this for the built-in nodes, but this information can allow users to build a node that either takes patches from the game logic (for slower, non-sample-rate animations) or audio-rate values from the graph itself.
which for certain domains is very useful/flexible
I also think I found a way to avoid having to allocate an event buffer for each node. Taking advantage of the fact that events for a node are often clumped together, I can just store the starting index for each clump in an ArrayVec (and then just do a linear search if that fills up).
Hey BillyDM, I just read your two DAW GUI articles, really interesting stuff and I’m happy I dug into it! Funny that when I got to the end of your second one, you mentioned a third-party dev using Flutter for the UI, did you decide against that in the end? I’m interested to know if you had specific problems since (in case it wasn’t obvious lmao) that’s my current preference for my own project and I don’t want to go too far down that rabbit hole if it’s a bad idea
hey, any1 want to give me a hand porting something like this fading io/out from bevy_audio to seedling? I'm having hard time figuring that out...
I could just add fraction of the value * time.delta() but maybe there is a better way
yeah eventually we'll have animations to make this easy
but replicating that functionality should be no problem -- is it the fade_towards that you're missing?
I guess so. Maybe I can just copy it to a local trait or something for the time being 😄
Or even add a PR to seedling?
(Assuming a fade_towards method, it would look something like this:)
fn fade_in(
mut commands: Commands,
players: Query<(Entity, &SampleEffects), With<FadeIn>>,
mut volume: Query<&mut VolumeNode>,
time: Res<Time>,
) -> Result {
for (entity, effects) in players.iter_mut() {
let mut volume = volume.get_effect_mut(effects)?;
volume.volume = volume
.volume
.fade_towards(Volume::Linear(1.0), time.delta_secs() / FADE_TIME);
if volume.volume.linear() >= 1.0 {
volume.volume = Volume::Linear(1.0);
commands.entity(entity).remove::<FadeIn>();
}
}
Ok(())
}
Volume is actually from Firewheel, so I think you'd want a PR there. @static quest might have some thoughts about it
Ok, that's enough to get me going, thanks!
Hey Corvus, are you planning to release a new version of seedling soon? I’d like to use the limiter from my PR and I’m wondering if I should wait until a new release or just switch to using a git dependency
yeah i was planning on a release relatively soon, although I think at this point I'll wait on @static quest's changes
so it might be a week or two before that actually happens
Makes sense to me! If I get back to working on my quake engine before then I’ll just switch to the git release then, I’m doing a big cleanup
To be honest, I'm still not completely decided on the frontend framework.
At the time I discarded Flutter as an option because it didn't support "pointer locking", but since then there has been a 3rd party plugin made that does this https://github.com/helgoboss/pointer_lock. But the main thing is that creating a bridge between Rust and Dart is quite a pain. That and I've heard some mixed feedback that Flutter's performance might not scale well to large applications. I can't say for sure, but it is a concern of mine.
Since writing that article, Vizia (https://github.com/vizia/vizia) has gotten quite a bit better, and I did make some progress on it. But the main problems are the bus factor of one, less features than the big frameworks, and it still has some quirks that haven't been ironed out yet.
I have also been looking into Qt, and more specifically Kirigami (https://develop.kde.org/frameworks/kirigami/). Apparently they actually have pretty decent Rust bindings, and I'm also a big fan of their human interface guidelines https://develop.kde.org/hig/. Though one of the drawbacks is that even though QtQuick (what Kirigami is based on) is declarative, it is not data-driven.
Plus I am a KDE user myself.
hear me out...
what if you used Bevy? 
...in reality it's not ready yet, but it could become viable in a year or so
Well, I need a lot of features that are specific to desktop applications:
- multi-window support
- well-integrated accessibility and localization features
- good support for dockable panels
- robust drop-down menus (this is a lot harder than you might think if you want proper drop menu nesting and scrolling for large drop downs)
- efficient UI updates (the UI engine can't just rebuild and redraw everything every frame)
- declarative data-driven API that gives clear separation between frontend and backend logic
Unless I'm mistaken, bevy's UI is "immediate mode", meaning it rebuilds the whole widget tree and redraws the whole UI every frame where there is an update.
nah it's already retained actually
Oh interesting.
Still, I don't want to have to implement all of the complex widgets myself.
yeah that's a work in progress right now, with some core widgets being landed for 0.17
but we'll still be waiting on a few until proper reactivity happens
Technically Bevy hits a lot of those bullet points already. It's just that the sum total isn't quite there yet, as well as some critical features like reactivity and stuff. But that's why I think it might just be great for this sort of thing in a year or so.
I was saying this before, I think BillyDM was dead on when we talked about this before saying that there are too many UI-specific features that Bevy's missing but I totally agree that the core of Bevy fits a high-performance custom UI really well. I think I could see a future UI library for Bevy being best-in-class among all game engines, the core (esp the way the ECS and asset system work) feels like it fits it well
Yeah I 100% agree with this, super excited to see what the future of Bevy's UI looks like
Yeah, I'll keep it in mind. And because I'm separating the frontend and the backend logic, it shouldn't bee too hard in theory to switch frameworks if I find the need to.
Oh, right, people are in the process of making an editor for Bevy using Bevy's UI. That does make it more promising in terms of making desktop applications.
That’ll be amazing dogfooding
Fair enough, I remember those two being an issue before. I wrote a library that used protobuf under the hood but ultimately looked very seamless in practice to solve that but Flutter's been putting a few more things in for the automotive industry related to directly embedding it in other programs. I think that was always just the rendering and stuff though, I don’t think I ever saw something about directly manipulating values in the runtime like you’d do when embedding e.g lua or whatever
Tbh for all the things to like about Rust the thing that keeps me coming back is that IMO no other language makes it easier to do big refactoring projects. I think keeping the UI in Rust could be worth it just for that
Anyway thanks for the update, even with what I’m saying about Rust making porting between frameworks easier you still haven’t scared me off using Flutter for mine just yet (whenever I get around to it)
@obsidian tusk Looking into Flutter more, one dealbreaker I found for me is the lack of good multi-window support. There are ways around it, but they are unofficial and kind of hacky.
I think there is something wrong on linux, using: bevy_seedling = { version ="0.4", features = ["mp3", "flac"] } OS: Linux (latest) Bevy: 0.16.1 Using dyn lib for Bevy, development/debug build by the dependencies are optimized.
Issue: application ~freezes (very very slow until it gets killed by the OS)
Output
INFO firewheel_cpal: Attempting to start CPAL audio stream...
INFO firewheel_cpal: Starting output audio stream with device "default" with configuration StreamConfig { channels: 2, sample_rate: SampleRate(44100), buffer_size: Fixed(1024) }
Reproduce: start the application, load bevy_seedling plugin, do not load/play any sound, leave the application open/idle (e.g. on a second monitor). After ~13 minutes output gets spammed after which the application (and whole pc) becomes very slow (<1 fps) rather quickly. Only solution is to close/kill it. This line is printed over and over again: ALSA lib pcm.c:8772:(snd_pcm_recover) underrun occurred (in the Visual Studio Code terminal)
Known issue or configuration issue on my end? Some more information i can provide/test?
Oh that really sucks, I didn’t even know
What audio driver do you use? e.g some combination of pulse, jack, pipewire and/or alsa
Hm, that's pretty strange! You shouldn't be getting underruns with optimized dependencies.
Seems to be pipewire 1.4.6
In case it helps: this is the audio device (headphones connected)
0e:00.1 Audio device: Advanced Micro Devices, Inc. [AMD/ATI] Radeon High Definition Audio Controller [Rembrandt/Strix] Subsystem: ASUSTeK Computer Inc. Device 8877 Flags: bus master, fast devsel, latency 0, IRQ 105, IOMMU group 27 Memory at f6a80000 (32-bit, non-prefetchable) [size=16K] Capabilities: [48] Vendor Specific Information: Len=08 <?> Capabilities: [50] Power Management version 3 Capabilities: [64] Express Legacy Endpoint, IntMsgNum 0 Capabilities: [a0] MSI: Enable+ Count=1/1 Maskable- 64bit+ Capabilities: [100] Vendor Specific Information: ID=0001 Rev=1 Len=010 <?> Capabilities: [2a0] Access Control Services Kernel driver in use: snd_hda_intel
(i do not have any audio/performance problems on other applications: browser, music player, ...)
Is there an easy way to verify? I simply use [profile.dev.package."*"] opt-level = 3 and run with cargo run --features dev
hm, yeah that should do it
but it sounds like there may be an unexpected leak or something
So I was running a program for around forty minutes earlier, and I never ran into underruns or slowdowns. I'm on a macbook. Although I was actually doing stuff -- playing a single sample with some moderate processing.
In other words, this may be an issue related to your platform specifically. It may be coming from Firewhee, but it might even be cpal itself.
I suppose for now, feel free to make an issue on the bevy_seedling repo until we figure out a little more about it.
Maybe try running some of the examples from the Rodio repo? They use cpal too
I tried to reproduce it with the examples of bevy_seedling (on master), and could not. Tried with one_shot example and removed the system. Tried also with adding the bevy default plugins, in both cases it went +15 min without any issues. So most like it is my bevy application itself, will be fun to debug. Will report back if i find something interesting (and time to debug).
Huh, that’s fascinating. Are you maybe still using the default Bevy audio system in addition to seedling?
No, disabled all bevy default features and bevy_audio is commented out (except if some other dependency enables it).
Disabling the features isn’t the most-reliable way to do it because other dependencies can re-enable the features (actually, I think Corvus should probably change that recommendation in the readme). Here’s what I do in my project https://github.com/eira-fransham/seismon/blob/0ba6b2cbb02931bc65d9fd2dcd03062f35f8ec26/src/bin/quake-client/main.rs#L238
Oh, did not know that; TIL (bit ironic i have to enable bevy_audio to be able to access the plugin type to disable it + it got me prelude ambiguity for PlaybackSettings).
Well i tried it, went afk, and came back to a frozen pc. I have confirmed it is a memory leak, ~50mb/s. So i ran again with bevy_audio and bevy_seedling disabled (not including the package and disabling the bevy audio feature) and it still leaks, so yea nothing to do with bevy_seedling not sure why my initial test (before i posted the initial message) ran fine (+15 min) when i disabled bevy_seedling in my app. This also explains why i could not reproduce it with the bevy_seedling example.
Summary: not related to bevy_seedling, apologies for the noise, and thanks for the suggestions!
Maybe try valgrind? It’s good for diagnosing memory leaks
(If you’re on linux/macOS)
A quick search finds this for windows but I can’t vouch for it https://www.deleaker.com
Look at the simplest way to find all memory leaks in your code. Download this tool for free! GDI, handles leaks can be found as well.
This will 1. compile rodio, which isn’t super ideal and 2. break glob imports, since both bevy_audio and bevy_seedling export types like Volume.
Ah, yeah that’s a good point. Might be worth mentioning both options with their upsides and downsides though just in case
@ionic sedge Is the new event timing system working well for bevy_seedling? If so, should I go ahead and push another beta release?
Actually, I've thought of one more thing for the sampler node. I think it would make more sense and also be more deterministic to have the playhead be part of PlaybackState::Play instead of its own parameter.
Well I was hoping to get something quick and dirty going for animations, but the more I got into it, I just couldn't resist making something a bit more substantial. So that's taken some of my time away.
My primary reservation with the new approach is that patches are taken by value now rather than by reference. This means that we can't efficiently update the baseline structs after diffing them on the main thread -- we instead have to rely on cloning the latest value into the baseline. (Just for clarity on what I mean, this is how it's down now in bevy_seedling.)
Is that a problem? I dunno, maybe. For small objects, cloning will probably be faster, but it wouldn't scale as well as patching for large objects.
I've had no issues with actually scheduling things -- I haven't written a fancy API for that just yet, but it'll be no problem.
Oh, you're using the patching stuff directly in bevy_seedling? I thought we were just using it to patch parameters on the audio thread.
I could change it back to patching by reference if that will help.
Yeah so to clarify on this usage:
In the ECS, we store the paramters and their baseline on an entity. That might look like (pseudocode):
(VolumeNode, Baseline<VolumeNode>)
Users will query for the VolumeNode and make all sorts of changes to it. Then, once per frame, we run through all the VolumeNodes that have changed and diff them against the value in Baseline<VolumeNode> (Baseline is just a newtype wrapper).
After diffing, we need to update the baseline so it matches the normal value. We can just clone the normal one into it, but we did just make fancy fine-grained per-field patches. Why not apply those to the baseline instead?
So that's one way they're currently used in the ECS.
I actually do use them elsewhere -- we sometimes want to keep multiple instances of parameters in sync. This allows us to co-locate the parameters so it's easier to query for them. However, for this use case, we just consume the patches anyway, so I didn't have to change it.
Ok, I can change it back to patching by reference. The user can still retrieve owned data from events, they will just have to use core::mem::swap to do it in a realtime-safe way.
Personally, I do like the by-reference approach! Since we have to align the baselines every time we diff, I'd love if we can do it as efficiently as possible.
@ionic sedge Alright, I switched it back to patching by reference!
oh sick lemme give it a whirl real quick
Yeah, definitely checks out from my perspective!
Let me also take a quick look at throwing lots of events at the processor.
Okay so I think it scales fine. I played almost a thousands samples at a time over the course of 30 secons or so, all with animations generating one scheduled event per frame (120hz I think) and... idk, it didn't underflow or anything. Max was ~10ms with a 23ms budget, average of 4.5ms.
The animations would be all bunched up in time (i.e. for each frame, all 1k would be scheduled at the same time), so maybe not the best test for the sorting.
But it's probably a somewhat realistic animation scenario -- I don't think most animations will schedule at a resolution finer than 8ms.
Hm, wouldn't this make it a lot more annoying to move the playhead around?
I think another thing I'd like to get in Firewheel this round is bevy_reflect. I don't know if you're familiar, but despite the name, it's written as an independent reflection library. We'd probably want *another* feature flag for it, if you can bear it! But it'll be increasingly important for a first-class Bevy experience as we start integrating inspectors and editors into Bevy.
Hopefully I can get to that today. The only thing that hindered my progress last time was implementing it for the sample resource trait object.
(And to be clear, a few folks have requested the feature already, so there is existing demand.)
To expand on this -- the default position for the playhead in bevy_seedling will always be the 0 when you queue up a sample. This is because, if you don't explicitly provide a playhead position, it will insert the default one. And due to how the sampler pools work, queuing up a new sample for an existing sampler will automatically and immediately align the sampler's parameters with the incoming values.
So my thinking is that putting the playhead parameter inside the playback state enum will be kinda cumbersome. But I could definitely be wrong!
No, you would just send another PlaybackState::Play event to change the playhead.
The problem I've been running in to is that in the sampler engine itself, it doesn't make sense to have a concept of a "playhead" parameter when the sample is paused/stopped. It makes state management more complicated, and it can lead to less deterministic timing (essentially if an "update playhead" event is sent before a "play" event or vice versa, correctly accounting for the delay between those events is very complicated and hard to make deterministic. It would be way easier if the state just had the playhead as part of the play event.
Yeah I suppose that's fine. It's not as if reading it is particularly valuable anyway -- the actual playhead value is stored in the shared state.
Either it's "you're playing a sample from a given position, or nothing is happening". There is no inbetween.
@ionic sedge Alright, I made the change to the sampler node. Now the way you "play a sample in the past" is either by setting the playhead in PlaybackState::Play or by scheduling the PlaybackState::Play event in the past (and if you do both, the delay will correctly be accounted for).
Ah, so you removed the instant field? Hm, I suppose if you schedule the PlaybackState event, you'd also have to make sure any associated sample event happens at the same time, right?
I think that should be easy to enforce with bevy_seedling
Yeah. Really the instant was a playhead in disguise. And plus this should make it more robust to schedule a sequence of events, especially musical ones.
yeah it correlates things nicely
btw do you think there's anything wrong with just... scheduling all events by default?
What do you mean?
Well, this would be in Firewheel's userspace. As in, in bevy_seedling, we could attach a timestamp to all changes by default.
I think you'd end up with slightly more regular event timings, but... it probably doesn't really matter much.
Hmm, well unscheduled events have less overhead.
Oh yeah, and in fact I was planning on adding a feature gate to disable scheduling and/or musical transports if you don't need it.
But I'll do that in a later PR.
yeah i wonder if it's enough to matter for most games
maybe I'll profile that at some point
I meant more like if you were using the engine for other, non-game applications.
I’m not sure that the difference would be measurable without checking, the difference in performance seems like it would be maybe a branch or two at most since they’re still all going through the same sorting and timestamp-checking mechanisms. I do think unscheduled events should still be an option for simplicity though, most of the time if you’re interacting with firewheel directly you just want an event to be handled asap and don’t really care about scheduling events. That’s more of an advanced feature
Okay I'm most of the way through reflecting types in Firewheel and bevy_seedling. I'll integrate the latest playhead changes later today just to make sure it all feels good.
Okay yeah I really didn't have to change much for the playhead stuff. Constructing the variant with a value is definitely a touch verbose
commands.spawn((
SamplePlayer::new(server.load("selfless_courage.ogg")).looping(),
PlaybackSettings {
playback: Notify::new(PlaybackState::Play {
playhead: Some(Playhead::Seconds(6.0)),
}),
..Default::default()
},
));
but I could easily add some shorthand for PlaybackSettings.
I think I'm pretty satisfied with where I'm at adding reflect derives too. I had to manually implement everything for Notify to preserve its invariants, which is a little unfortunate, but it definitely works!
@static quest feel free to merge your scheduling PR -- I feel like it's in a good place, personally
I can either PR the branch or wait until you've merged and PR main.
Ok, I'll merge it!
I'll add a feature flag to disable scheduling and musical transports, and then I can publish a new beta release!
On a slightly unrelated note, I had a nice idea for automatically re-initializing nodes when you want to change the configuration.
Basically, we can very easily detect when you've mutated a configuration struct in the ECS. On its own, that doesn't do anything since the configuration is used only when the audio processor is constructed. However, we can just re-create the processor and reinsert it into the graph whenever you make changes to the configuration.
This would be easier for me if we had something like a insert_at method where you could replace an object of a given ID. If it also allows you to avoid re-compiling the graph, it could be decently performant maybe??
I don't know the details of the graph enough to know whether the above makes sense. If not, then I can just remove, insert, and re-route connections, which would be fine.
Hmm, well considering that changing the configuration can change the number of input/output channels, it can cause the graph to re-compile.
Hmm, should I have those features enabled by default?
As in the scheduling and transport would be disabled by default?
Yeah, do you think they should be disabled by default? If for example, people mainly want to use this library to just play audio in their application, it wouldn't make sense to include that stuff.
Hm, hard to say exactly. It's pretty easy to add features, and people often don't disable a crate's default features.
(And then wonder why they have so many dependencies (ask me how I know).)
Oh but maybe this isn't clear. I'm not sure if these feature flags are adding or removing the functionality.
If they're adding it, then leaving them disabled by default makes sense to me. If they remove it, then it might be a bit more contentious.
[features]
default = []
# I'd have no problem excluding these by default
transport = []
scheduled-events = []
[features]
# whereas this might be more annoying
default = ["disable-transport", "disable-scheduled-events"]
disable-transport = []
disable-scheduled-events = []
I target default features at beginners
@ionic sedge Merged your PR!
Okay I think it should be ready for a beta release from my end. It'll take me a few more days to polish the features I've added to bevy_seedling, and at that point I'll be able to confidently determine whether there are any last minute features I need.
Cool, I'm working on adding those features, and then I'll publish a beta release!
Been super cool following this, nice work on the latest version both of you!
Thanks!
@ionic sedge Version 0.6.0-beta.0 is published!
Awesome! I'll try to get things wrapped up here as soon as possible 
Oh yeah, I also need to add an option to use a mono signal in the spatial basic node.
ya it would be easiest if there was an option to basically just downmix all the inputs to one channel
i mean we could insert a node before it to do that
but that's a little more annoying
I figure people would only want to do that if they have specific mixing needs
Well the problem is that currently it only works on stereo signals. It should be easy to fix though, I'll do that after lunch.
@ionic sedge Alright, I've added a "downmix" parameter to the SpatialBasicNode and published a new beta release!
I also ended up adding in_connected_mask and out_connected_mask fields to ProcInfo that tells nodes which channels are connected and which ones are disconnected. This way I can have the SpatialBasicNode detect if it should treat the input signal as mono or stereo.
oh nice!
@ionic sedge Hmm, I have thought of one more thing. I'm not sure how I feel about having glam as a dependency. I might make it so that glam is an optional dependency.
I do like that the glam types can just be in the events as-is.
You already have bevy as an optional dependency, right? Are there the same types/features in bevy_math that could be used instead so it can all be rolled into the same feature?
well the types in bevy_math are glam types
Yeah, and I can add a feature to optionally include those types.
Ok cool, I suspected as much
mm as in conditionally compile the variants?
that's probably fine
On second thoughts maybe it does make sense to have two features, so you don’t need to import all of the bevy stuff
And currently the only node that uses it is the SpatialBasicNode, which I can also add a feature flag to switch between using glam types or just [f32; 3] as the parameter type.
I suppose, but is glam isn't heavy I think. Without default features, it has no other dependencies.
Switching between two options with a feature flag is usually a bad idea, if a crate is written with the array in mind and another crate in the tree enables glam then the crate written to handle the array will now fail to compile
in other words, it would be better to simply add a new variant with the feature flag, or otherwise force people to use the custom or bytes one
Hmm, fair point.
Is it so bad to just enforce using an array, casting to/from it? The spatial node can use the glam types internally if it’s more efficient and you can use into() to convert to an array
on the user side
and since it's just an implementation detail at that point it’s fine to put the usage of glam behind a feature flag
it is nice to be able to provide Diff and Patch implementations for glam types, but the implementations could be optional and they could write to a [f32; 3]
Yeah, that makes sense to me
If we don't plan to extensively use glam, then not exposing glam types publically would make long term maintenance potentially easier
Well, the problem is you have already defined sequence_diff!(T, [T]); for diff and patch.
oh yeah that was the original contention
We could just use a newtype or something
that would be more convenient anyway i think
What do you mean by this?
struct Position3d([f32; 3]);
Ah, ok.
You could maybe implement it for any field types that implement Into<[f32;3]> + From<[f32;3]> since that covers p much all Vec3-like types across the ecosystem
but I don’t know if the current derive mechanisms support that kind of generic code
But we can't have both that and sequence_diff until negative traits are stabilized.
right
Fair, ok in that case just special-casing glam types for the purposes of bevy makes sense
that's a big reason we initially did it, but a newtype or even a custom Vec3 would also serve a similar role without any of the downsides of depending on glam publicly
Would users still be allowed to define Diff structs with bevy vec3 fields?
So long as the right feature flags are enabled
ya imo we should still have glam be optional and provide implementations for Vec2 and Vec3
Makes sense to me
and then bevy_seedling would just enable those
Yep, seems like a good plan
@ionic sedge Alright, what do you think of this implementation? https://github.com/BillyDM/Firewheel/pull/57
ya looks good to me! I think crates.io might complain about the glam wildcard, but 0.* should work.
Personally I'd skip the
impl<T> From<T> for Vec2
where
T: Into<[f32; 2]>,
impls, since (I think?) that might make it annoying to write such conversions for types that don't impl Into<[f32; 2]>, but 
might not matter in practice
but yeah since we can toggle on the glam Diff/Patch implementations, I don't think there will be any regressions for Bevy projects 👍
hey do y'all know what synths typically use if not soundfonts?
is it just like raw samples
depends on the synth!
okay I guess, let's say you move a project from one DAW to the next. I imagine...MIDI and a generated soundfont would work
I don't know what the standard is in that situation, but I'm not positive DAWs can generate sf2s on the fly
I'm not building a DAW, but I am building a networking system to transfer someone's voice with MIDI, so i just didn't know if there's a better system for this, or even a better networking protocol
these files are typically tiny though so like the latter is a spitball
Well, sound fonts are just collections of samples, more or less. Quite primitive ones at that, usually.
Modern sample-based synthesizers typically use more sophisticated techniques to sound more life-like.
Often, for each note in an instrument's range, samplers will have multiple samples for each velocity bin.
MPE synths may even do a lot of fancy processing and blending to get good sounds in-between notes.
oooo good to know! gotta look into that
Nowadays there are three "multisample" formats that are typically used. Kontakt is used the most, but it's proprietary. Then the biggest open source one is SFZ. There is also a smaller open source format that I quite like called DecentSampler.
i can only imagine kontakt is a bear to work with 😅 it just gives me those vibes
I heard that their scripting language is actually quite powerful. It's just a shame that it's proprietary.
Dunno if it needs to be said but most synths use FM, subtractive and/or wavetable synthesis (or one of the other wackier forms of synthesis). Using something kontakt- or soundfont-like is relatively rare
Ah yeah, there is a distinction between "synthesizers" and "samplers".
well it does depend on your definition of "synth"
but yeah I think most people use it to refer to like
non-sampled stuff these days
I had some older professors who called the big old keyboards that have a million samples in them synths though
so 
I think it’s ok to collectively refer to them as synths even if there’s a difference, I just wanted to mention in case they thought samples were the only way to make a digital instrument because I’ve talked to people before who thought that
but to transfer the instructions to make a particular sound...
there must be some medium of exchange there right? even if it's not guaranteed to be deterministic
It's not open source, but someone in the Rust Audio Discord recently released a really nice soundfont player plugin. https://estrobiologist.gumroad.com/l/sflt
It could be worth reaching out to them to see if they would be open to releasing a headless version, that way it can be loaded into Firewheel as a CLAP node (once we add that feature).
midi :3
I mean I have a soundfont player
It’s usually specific to each synth
Oh wait, it is open source under GPLv3!
I think they mean more like presets
midi :3
I think the difference we're talking about is I'm assuming that a synth is separable from the actual instructions that can be used to make a voice, and you're saying they are one and the same
VST and CLAP have an endpoint to get the current param state as a byte array and then you can load it back into the synth later, people use it for presets. It’s specific to each synth though, you can’t transfer those presets between synths
I understand
CLAP/VST plugins are just a list of f32 parameters with audio and MIDI input/output buffers.
but to clarify, depending on the complexity of the data you need to share, you can just control the synth parameters via midi too
That’s not really true because not everything that affects the sound is necessarily mapped to the host
Oh wait, plugins can have their own custom state stored as raw bytes. That's how you can, say, store a location to a sound file for a preset.
I thought that a synthesizer could take some waveform, or some common language to produce a waveform, and to provide modifiers onto those, like additive work with other voices, etc. kinda like how firewheel works
but I didn't realize I'm a step ahead
Ah yeah, I typed that and then remembered immediately after that there is more to it. 😅
my understanding is that a big reason for midi 2.0 is to allow synthesizers / processors to enumerate their parameters in a reflection-type approach
so at least it would be slightly more accessible
https://github.com/dsgallups/midix/tree/main/src/synthesizer/src/soundfont so I realize now that I did not build a synthesizer. I built something that can combine midi and sf2 to produce sound
I stole this code
Oh actually, you would probably interested in the OSS (open sound system) spec instead of MIDI since it was designed with networks in mind.
That’s fine too, the good thing about a soundfont player is that you can express a really wide range of sounds with a single implementation code-wise
I've went ahead and added a PiecewiseTransport type since that was fairly simple to do.
And so now, I think I'm pretty happy with the API of Firewheel! There shouldn't be much if any breaking changes now going forward (unless we notice something major).
(Although once negative implementations have been stabilized for a while, we might make use of that for the patching and diffing stuff).
Also just released 0.6.2-beta.0
Ah shoot, I just realized it would be better to make the musical transport a dynamic trait. I'll just yank it to avoid publishing yet another release.
oh ya that seems like a good idea
Alright, the musical transport is now a dynamic trait!
@ionic sedge Though actually, how do you feel about having a type-erased field in TransportState?
I suppose I could have an enum with a Static(StaticTransport) variant and a Custom<ArcGc<dyn MusicalTransport>> variants.
Yeah, thinking about this some more, I'm not sure how necessary it would actually be to let the user define custom MusicalTransport types. Once we have the AutomatedTransport type, that would cover all possible use cases.
Would that potentially integrate well with arbitrary animation sources?
It may still be nice. If we expect the cost of the dispatching to be low (is it especially hot?), then unless the trait object makes it harder to use, I don't mind it personally.
Well, the way I have it set up, it's only possible to have linear interpolation for tempo. The math is already tricky for linear, I can't imagine what it would be for non-linear.
Even the CLAP spec itself only supports linearly automated tempo.
I could imagine users wanting to dynamically change the bpm of a static tempo in response to gameplay. Although allocating what is essentially an Arc<f64> probably isn't that expensive.
What are you thinking for the AutomatedTransport?
Or would this make sense as something controlled from the ECS side? Like would we animate the StaticTransport in the ECS and update the TransportState with the new tempo values?
Well I guess the thing is that it pretty much already is an animation system for the tempo parameter. If you wanted to change the tempo dynamically, you would just automate the StaticTransport. Hmm, though I suppose it would be handy to be able to schedule exactly when to change the tempo, but that's starting to get pretty complex.
I'd imagine people would want finer animation than the ECS can provide anyway, to be fair.
Almost certainly better to handle it more precisely in the MusicalTransport implementation.
Ok, then I guess there is still a bit more work to do. 😅
At the very least, if you keep it as a trait object, it won't be breaking or anything to add another implementor.
But I'm sure that can be tackled later either way
as folks ask for it
Oh, I just got a great idea. Instead of having the user automate the bpm directly, they can automate a "speed" multiplier that would work for all MusicalTranports.
Hmm, and maybe we don't even need a MusicalTransport type. The user could just schedule a sequence of transport events.
Hm, but would that be able to capture non-linear tempo changes?
Well the thing is, it's hard to define how to even present non-linear tempo automation to plugins. With linear you can have a delta_bpm value in ProcInfo, but nonlinear would be much more difficult.
Hm, right, you're planning ahead for presenting this to plugins?
I already have. There's a delta_bpm field in ProcInfo.
The reason why nonlinear tempo automation is difficult is that tempo is the derivative of the playhead position in seconds, and thus automated tempo is a second derivative.
For linear automation you can use the kinematic equation pos = speed(t) + 1/2acceleration(t)^2. I suppose you could in theory derive equations for more complicated curves, but that seems like a pain.
Add on top of that the fact that keyframes make it a piece-wise function.
Essentially it's like you're trying to animate the speed of an animation.
Though I suppose we could omit the bpm information in ProcInfo.
@ionic sedge Ok, here's what I've come up with. Instead of making things complicated with interpolated tempo, I decided it would probably be good enough to just support "piecewise" automation for tempo (I doubt anyone could even tell the difference between true linearly interpolated tempo and a piecewise animated tempo updated every beat or so.) I also added a TransportSpeed field to TransportState that lets you set when to update the speed multiplier, or even have a bunch of speed multiplier keyframes. I feel this will give plenty control and precision for games. https://github.com/BillyDM/Firewheel/pull/58
Hm, yeah that seems good!
I haven't made a fancy ECS API or anything for the transport, although maybe I should
but either way this seems all good to me from the bevy_seedling perspective.
I just realized it would probably be better to have "instants" for each of the keyframes instead of "durations". I'll fix that after I eat.
I also need to figure out how to do a binary search on floats. (Right now it just uses linear search which can be quite inneficient for transports with a lot of keyframes). Any ideas?
Hm, is the issue in sorting them?
No, the issue is in figuring out which keyframe the current time falls in.
Wouldn't it essentially be the same as a normal binary search, but with slightly more care in splitting the set?
Probably. I've just never implemented a binary search manually before. It probably isn't too hard though. I just wondered if there was an easier solution first.
The binary search built into Rust's standard library only returns Some for exact matches (though I might be wrong on that).
actually I think you can use the error variant
If the value is not found then Result::Err is returned, containing the index where a matching element could be inserted while maintaining sorted order.
Oh, sweet
I can also just assert that all values are valid finite number with no duplicates in the constructor.
Ok, I've made those changes.
Waiting for the CI tests to complete and then I'll publish a new beta release.
Oh, I've thought of one more little thing. I want to future-proof adding realtime logging. It won't actually be realtime yet, but it will be there once we do set that up.
ya that'd be super neat
Actually while doing this I realized it was actually pretty simple to implement a realtime-safe logger, so I just went ahead and did it!
And one more thing I've thought of. I thought it would be handy to have a midi message type (using the wmidi crate), so I also went ahead an added a feature flag for that.
And now 0.6.3-beta.0 is published!
@ionic sedge I'm trying to make firewheel-core no-std compatible. I've pretty much got it, expect for this one line here with the bevy_reflect feature. Any ideas on what we can do here? https://github.com/BillyDM/Firewheel/blob/3f749e9fbd291d387eff3d426963c080c22ca4ff/crates/firewheel-core/src/diff/notify.rs#L234
Oh nice! Hm, I'm pretty sure bevy_reflect has been made no_std compatible.
Let me see if I can find what they do in no_std contexts for that
Yes, bevy_reflect itself is no_std compatible, it's just that that line in firewheel-core uses Cow.
Yeah, that's nearly what the macro produces, but they may export a Cow type for no_std.
I just cleaned up the macro output for that part.
mm, looks like they have a doc_hidden internal type for it. It's a little ugly, but you should be able to do
bevy_reflect::__macro_exports::alloc_utils::Cow::Borrowed("T")
Oh interesting, thanks for looking into it!
Hopefully we'll be able to clean up that whole implementation eventually. We only need to change a couple lines from the derive macro to preserve Notify's invariants.
Ok, that worked!
And with that, I think the API is actually more or less finalized! At least with firewheel-core.
I'll go ahead and publish one more beta version.
Oh, apparently I need to use libm to get float functions in no_std.
Yeah a fair amount are std only. If you don't want it to be annoying, you can use num_traits, which provides a float trait that basically provides the exact same methods that std does.
(via libm)
Ok, I'll look into that.
@ionic sedge Ok, got it fixed and now 0.6.4-beta.0 is published!
I've also went ahead and made firewheel-nodes and firewheel-graph no_std compatible. I'll wait to publish a release for that though since it doesn't affect much.
Moving everything to no_std is exciting, it’s making me want to build a little synth with a teensy or something. Does that mean you don’t have a hard requirement to allocate anywhere?
No, alloc is required.
Ah fair, nvm then 😅 Still nice to support no_std
Yeah, I make extensive use of Arc, Box, Vec, and HeapRingBuf.
And also String for the realtime logger.
Yeah I thought boxed trait objects were a pretty fundamental part of the system and getting rid of allocation would’ve been a big change
a teensy can totally do allocation though
I mean, it’s probably possible to make allocation optional but I don’t really know why you would
Yeah, implementing an audio graph engine without alloc is a tall order.
If you’re a touch careful with it, you can probably do very reliable processing on microcontrollers.
I made one ages ago but it’s a complete labyrinth of trait magic
with firewheel and a bit of alloc
But anyway, this may end up being super helpful for future wasm (which may forgo std) and console support
consoles generally don’t provide targets with standard library implementations
True
Though one hurdle is that Symphonia isn't no_std compatible (and neither is rubato, at least I think). Though you can replace it with other audio sample loading libraries.
ya we could definitely work around that
and anyway if we do start getting serious about console support, we could probably help symphonia work towards no_std
I also just remembered that the sampler pool currently depends on the context provided by firewheel-cpal. I definitely need to fix that.
Had a check back and it does allocate in a few places to work around the really early state of generic associated types and type alias impl trait at the time, here’s the repo if you’re interested but it’s an unreadable nightmare and I feel like I wrote it in a fugue state https://github.com/eira-fransham/octahack-rs
A fast, efficient, modular music creation toolkit, designed for live performance - eira-fransham/octahack-rs
It does work at least
Great point
Oh also, as it turns out, the Fyrox developer made a pure Rust HRTF implementation a long time ago. Pretty impressive that Fyrox has had it since day one! Anyway, it uses the ircam database, which provides the impulse responses in a very simple format. Ideally we'd support SOFA like Steam Audio and most other modern audio software, but Fyrox's approach would certainly work in the meantime.
I asked the dev if he'd be okay with his hrtf crate ending up in Bevy, and he gave me an approval. I've actually already written an integration for bevy_seedling, so I can include HRTF support in the next release!
Neither Fyrox nor sofar handle fast-moving sources all that well, so we'll probably need to invest a little time fixing that up in the future.
@ionic sedge While working on making the sampler pool generic over the backend, I've ended up making even more changes. I'm quite happy the way it turned out! https://github.com/BillyDM/Firewheel/pull/59
Oh nice! I should update the audio demo with this
Oh one thing I didn't think about until now @static quest is the logger might be problematic for Wasm. At least, by convention we might want to be able to completely cfg out logging. If you so much as sneeze at a string in an audio worklet, it'll throw a JS exception.
Okay maybe this is hyperbolic -- I'm not sure exactly where the error comes from. But wasm-bindgen tries to access a JS TextDecoder when handling strings, which isn't available in audio worklets. This happens when the worklet panics, for example, making debugging very hard.
If we're unlucky, it might even be merely constructing a String or manipulating it in normal ways that causes the exception. I haven't found the root cause of it myself. I just send a closure from the audio worklet that formats a string on the main thread if I need info from a worklet.
Oh ok, interesting.
Oh something else that would be really helpful for artists and just people in general is a quick and easy way to profile the audio processing. In fact, it would be amazing if there were a mode breaking it down by how long each processor took. I don't think it could be supported on all platforms (read: Wasm), but yeah I think people would love it. I see people regularly praising the profiling capabilities of middleware.
I don't think it needs to happen right now since it should be possible to introduce without any processor API changes. Maybe I'll make an issue for it.
This'll be especially useful as we start to introduce more involved processing like HRTFs, convolution reverb, and so on.
Ah yeah, that is a good idea.
Have you figured out the problem with logging in wasm? Or do I need to disable that feature in wasm?
no hopefully i can get to it here in an hour or two
oh btw @static quest where does the logging go? do I need to activate a feature for it to print somewhere or something?
oh nvm i was missing a bevy feature
okay apparently it works even with string formatting
i wonder if it's the string allocating that causes panicking to fail
anyway this is great
personally, I would love if the internals of the debug functions are feature gated rather than the whole thing
that way, you don't have to also apply #[cfg(debug_assertions)] at the callsite
the migration is also a little annoying when a new argument is added to process
(nothing crazy of course, it only took a few seconds to move all my nodes over)
would it be crazy to just have one big "context" struct for this sort of thing?
technically adding fields to that would also be breaking, but I think it might tend to be less so?
not a big deal either way
Actually, that's probably a good idea. I'll work on that.
By "feature gated" do you mean adding a debug_logging feature to the Cargo.toml?
no i mean it’s fine to use debug_assertions imo
Oh ok, I get what you mean.
there’s a small possibility that the code at the callsite won’t get optimized out occasionally if you do that, but I don’t think that’s worth worrying about personally
I think it's likely it will get optimized out.
ya in almost all cases id expect
Hmm, though the lifetimes of buffers may make this complicated. I'll see if I can get something to work though.
hm, you could just add some more, right? although it already has quite a few!
Possibly. I also don't want to have double-indirection for the common stuff, so I don't think I'll put the input buffers, output buffers, and the event list in the context.
ya id say it makes sense to separate those anyway tbh
ah but I see — I guess I was just assuming the buffers would be “flattened” inside the context, and in that case the lifetimes aren’t too crazy (if verbose)
@ionic sedge I've come up with this, and I'm quite happy with it. I've actually managed to simplify the lifetimes by moving the scratch buffers into a different struct. https://github.com/BillyDM/Firewheel/pull/60
oh that looks nice!
hm, do the processors need a mutable reference to it? (i didn't look at the internals or anything)
if you want to get fancy with it, you can also add a #[non_exhaustive] annotations so adding new fields isn't a breaking change
Mutable reference to what?
Yeah, it has to be a mutable reference, otherwise Rust won't let you borrow any of the fields as mutable.
I mean as opposed to passing it by value
idk if that makes sense (i.e. if it's stored in a way that makes that annoying, then disregard!)
similar to the proc buffers
Ah, I see. Passing it by value would add an extra 16 bytes to the function arguments. (The size of the function arguments is already 56 bytes).
It's probably not that big of a deal though.
ah i see
for objects that do use the extras, I'd expect passing by value to be faster (I'm sure it's much more expensive to chase a couple pointers), but for objects that don't, the reference should naturally be faster
it would be cool if we had a good end-to-end benchmarking suite
that would make it super easy to validate this sort of thing
how many registers can the x86 abi use before it has to spill arguments onto the stack?
(and aarch64 ig since that’s what i’m on)
oh i think it’s 48 bytes
Ah, so either you're an apple user or you're a bleeding edge linux enthusiast. 😁
sadly im away from my linux machine for a while 
Ah ok, then making the function arguments smaller probably won't make much of a difference then.
but apple’s core audio has been nice for end-to-end profiling
oh i think it’s only 32 bytes on windows
I probably don't event need to make ProcEvents borrowed either.
Hmm, nevermind, that would make cleaning up the event buffer more complicated.
Oh wait, no there is a way around it.
Or actually, wait a minute. I'm not sure the problem is just a lack of registers. The larger the function arguments, the more memory that has to be copied into the function stack. So it still might be better to try and keep it smaller.
I think it's probably fine the way it is then.
Oh actually, I can just store the extra stuff directly in the ProcExtra struct. No need to have a struct of references.
While true, that copying has got to be faster than following references to the data. But I’m just making assumptions 
But the majority of nodes probably are not going to use it.
Still, we can have the best of both worlds by storing the extra stuff directly in ProcExtra.
ya i think that makes sense
I think it can use a couple more registers if it’s using fastcall (which the vast majority of calls in Rust will be)
At least, I believe that Rust uses fastcall by default. I might be misremembering though, it’s been quite a while since I’ve needed to know that info
Ah, nvm. This article from last year says that Rust uses C ABI by default https://mcyoung.xyz/2024/04/17/calling-convention/
I think that was just a case of misremembered wishful thinking 😅
Alright, I've stored the extra stuff directly in ProcExtra.
Oh, I can actually store the fields in ProcEvents directly as well.
I'm realizing now that having a struct of references is probably code smell.
Does the user need to use the struct-of-references anywhere that they need to explicitly annotate the lifetimes (mostly thinking of struct fields)? If not then I don't think it’s too bad, you see that kind of struct-of-references a bunch in e.g graphics programming
If you want to make the fields generic over either by-ref or by-value (for example, if you don’t want to force the user to spill something to the stack and enforce its lifetime) you can have fields of type T: Borrow<Foo> instead of &'a Foo. No idea if that is useful info here and it can make annotation burden worse (because now you need to annotate explicitly in fn args not just struct fields) but it’s a pattern that’s come in handy a fair bit
Well in my case the fields were owned by the same struct that creates the struct of references.
How do you mean?
I mean that the processor struct already owns all of the fields in the struct of references, so there is no need to create that temporary struct to send them to nodes.
Ah, can you send a reference to the struct instead? Is that what you mean? Sorry if these are tedious questions, I’m a bit out of the loop
Yeah, I can store the relevant fields in a smaller struct and just pass a reference to that to the nodes.
Ahhh I see, yeah that makes sense
Oh wait, the ProcEvents struct is defined in a separate crate from firewheel-graph. I suppose it could still be possible without exposing the internals to nodes by doing some std::mem::move shenanigans. However, I also realized that the event buffers would have the same level of indirection whether it was a reference to a slice or a reference to an owned Vec. While I can save one level of indirection to theindices field, the user only interacts with it via drain, so it probably would hardly make a difference.
I'm going to call it good as it is right now.
@ionic sedge Oh yeah, do I need to disable logging in wasm, or is that feature working?
no to my surprise it seems to work perfectly
Cool.
Say, do you know the semver rules for prerelease versions? If I had a version 0.7.0-beta.0 and I created a breaking change in 0.7.0-beta.1, is that breaking semver?
I'm not totally sure to be honest
im tempted to say it's not as important for a beta release like that
Just gotta say, y'all are killing it