#bevy_seedling
6985 messages · Page 7 of 7 (latest)
Hm, we have (in the default graph configuration) a limiter, which I suppose serves a similar role. Although maybe I should also enable clipping too for those who leave off the limiter.
Awesome! So glad to see this working for people with channel needs other than stereo 
yep I'm working on a show for a 93 speaker dome! Want to do everything in seedling with a lot of audio synthesis. I'm using the SPATGRIS software for spatial audio, you feed it individual audio feeds and OSC messages for location for each feed
Oh that's so cool 
The OS and/or sound card probably does its own hard clipping anyway.
I think it should be hard-clipped both at the point that it’s emitted from the app and at the point where the final OS-mixed output gets sent to the sound card, yeah, although every time it gets resampled to another sample rate you need to clip again because interpolation can create samples that are larger than any sample in the input. I think it’s better to warn the user if they’re emitting clipping samples but not to clip them in software by default, because then they might get clipped twice when the OS resamples to driver rate. If they want to remove the limiter they should figure out their own solution to handling clipping. I’d only force the output to be software-clipped if some platform misbehaves if you emit samples outside of -1..1
Off topic, but what is your profile picture supposed to be?
It’s a pic from a promo shoot I did
oh i see it now, wow that’s a good shot
@obsidian tusk OOOOH, now I see it!!! I was seeing some kind of weird bird thing with its head tilted sideways like this. 🤣
Omfg ahahaha thank you for the diagram 😅😅😅😅 I’m going to think about that when I see this pic now
You're welcome! 😎
A friend of mine took it! He’s great
Here's the glorious SVG file if you ever want to make that your profile pic. 😛
Aha amazing, thank you
(Made in Inkscape)
@ionic sedge Oh yeah, what do you think of the new audio backend API? I'd like to get moving on getting Firewheel ready to be upstreamed.
ya im sorry, i haven't given it an examination yet in context with bevy_seedling
i really should get to it today
i don't want to hold things up
Okay after a bit of my own tidying (forgive the delay), I've just started taking a look. I'm liking it! This will make profiling and testing so much easier (among other things).
I'm sure I could determine why soon enough, but can you elaborate on this constraint?
The only small catch is that the user must make sure that the CpalStream is dropped before the FirewheelContext is dropped. But this is a worthwhile tradeoff IMO.
I'll start evaluating how I can incorporate this intobevy_seedlingtomorrow.
The Firewheel context waits for the Firewheel processor to be cleanly dropped before dropping. (This is needed for CLAP plugins if and when we add that feature).
If this context is dropped before the stream, then the processor will never get dropped.
Though now that I say this, I just rubber-ducked myself into figuring out a solution, which is to just wrap the stream handle in an Arc<Mutex<Option<Box<dyn Any>>>> that is shared between both the context and the stream handle context.
Or actually, a cleaner solution would be to have a Box<dyn StreamHandle> trait that has a stop_stream method.
Oh wait a minute, there's even a better way to do this. I'm already using a wrapper for the FirewheelProcessor, so I can just add an atomic flag that makes it drop its contents if it still exists on the audio thread.
Ok, I removed the drop ordering requirement! As a bonus, you can now deactivate a Firewheel context without manually dropping the FirewheelProcessor.
Oh nice, the horrendous trait facade I had for the context can go away with the new API too. Very nice.
I'm getting familiar with seedling and see things are very centered around the sample player as a sound source, I come more from a synthesis/procedural sound background and wanted to know what your thoughts are on how procedural sound sources fit into the architecture?
We have a lot of abstraction around samples and pools, but synthesis should fit very naturally.
What kinds of procedural sounds are you thinking of creating? What would you like it to look like?
oh right this is you!
yes! I'm looking to do synthesis with a bunch of techniques(that ive been developing in supercollider and other similar environments), usually its chaining nodes together, for example starting from a sine/saw wave oscillator, adding filters and effects, modulating with envelopes and automating with values from the game world
for context for now im working off of your fundsp radio example
but am interested in developing native nodes for seedling
Oh nice! So I'm curious what your thoughts on it are right now.
how procedural sound sources fit into the architecture
I guess from my perspective, it seems pretty straightforward at first glance.
For example (imagine these nodes existed), you could set up a voice like this:
#[derive(NodeLabel, Debug, Clone, PartialEq, Eq, Hash)]
struct SawGroup;
fn saw_voice(mut commnads: Commands) {
let saw_node = commands.spawn(SawNode::default()).id();
// you could relate these nodes however you like!
// but as children, they'll be easy to find
commands.chain_node((AdsrNode::default(), ChildOf(saw_node)))
.chain_node((SvfNode::default(), ChildOf(saw_node)))
.connect(SawGroup);
}
So now, you could query over all SawNodes with children, modulating their values however you like.
You could spawn these voices all upfront, or over time (at the cost of some graph recompilation).
If you wanted to get fancy with it, then you could construct a lightweight pooling or queuing mechanism for these voices.
The above example expresses things in small nodes, but you could also write all of it into a single node like the fundsp example. Either style is fine -- and of course you could freely mix them.
I'm not totally sure this addresses your concerns -- feel free to correct me.
Absolutely, I'm still getting familiar with the architecture, so my questions are still a bit uninformed. Would the sawnode be similar to the custom node example ? Or would it have to be similar to sampleplayer to be a source (with no input)?
I think BillyDM's noise generator example is a great demonstration: https://github.com/BillyDM/Firewheel/blob/main/examples/custom_nodes/src/nodes/noise_gen.rs
As a generator, there's no need to take any input. It just writes to the output.
awesome, looking into it!
i am doing strictly synthesis in my use of seedling and have found that i prefer to write monolithic synth nodes that i connect to the higher level nodes provided by seedling/firewheel. as opposed to connecting very many synthesis primitive nodes together in the firewheel audio graph as you would in supercollider
so e.g. i have a modal synth node that takes a half dozen parameters that i use for collision sounds, which responds to strike events by exciting the set of active modes and outputs the full waveform of the collision
i think chaining smaller primitives is neither as ergonomic nor performant
though i will note that smaller nodes may be more performant than you might think
we should be able to prove this out soon with the new backend changes (it’s much easier to set up a benchmarking harness)
cool, that brings another thing I was wondering about, what is the state of profiling for bevy seedling ? should i go for tracy or is there dedicated audio profiling ?
We don’t have tracing set up ourselves. Tracy might also be a touch problematic in the audio thread due to syscalls.
This is something we’ve been thinking about. It would be great if the Firewheel processor could be made to profile the execution time of each node.
Yeah, I highly recommend doing the monolithic synth node approach. Firewheel isn't really designed to be a modular synth engine.
The only way to do that is to use Instant::now() (which I learned is one of the few syscalls which is realtime safe). I'm not sure if that's accurate enough for every node, but I guess we could try.
Yeah it depends on the OS for sure, but at the very least it's monotonic, so it should be okay. Per-node + total time would be perfect (and not just for installations or anything, it's super important for real game studios).
Ok, that should be easy enough to add! Once I'm done updating fixed_resample to use the new rubato I'll work on that.
Man, this ended up being a lot more work than I thought! I should have it done by tomorrow though.
Oh nice!
(The main problem was that rubato completely changed their API to use this new audioadapter crate for the user's input/output buffers. Oh man was that a pain to integrate into fixed_resample and symphonium!)
queue xkcd comic about competing standards
Though on the plus side it does mean that rubato now natively supports both interleaved and deinterleaved buffers. So that should slightly improve sample loading performance!
Funnily enough, we might want to make the firewheel processor an audio adapter if I'm reading the crate right 😅
seems like it would make creating a platform abstraction for bevy (such as what charlotte wants) a bit easier
Oh, you mean for the input/output buffers for FirewheelProcessor::process_interleaved?
Yeah, that could be done.
Maybe, although I don't know if it actually makes sense.
But if would be nice if we could plop anything in the "processing position" of such an abstraction -- Firewheel included.
Anyway, might be interesting to think about as the upstreaming progresses.
Things are taking even longer because I realized I could rework the API to support non-integer sample rates. 
oh but that sounds cool
Also family is over, so I don't have that much time to work on it.
oh that's okay, i think things will really get interesting for the upstreaming once 0.19 is out the door anyway
note that im still hot on a more incremental approach not a big PR doing everything at once
do yall know how long it takes cpal to get around to things https://github.com/RustAudio/cpal/pull/1127
My understanding is usually a while -- they are looking for maintainers after all.
roderickvd seems to maintain it now, https://github.com/RustAudio/cpal/commits/master/ he has been doing reviews/merges recently
oh my godddddd no way
sintel???
sorry, they're only one of my favorite musicians ever
so cool to see them contributing
oh right no threading in theads rip
what's a good track of theirs to check out?
sorry for the double ping lol
Code Parade did a fractal render over one of their tracks: https://www.youtube.com/watch?v=EkZsPcsV7yE
This video took almost 40 hours to render in 4K resolution, but it can run realtime at lower resolution and quality settings. Everything was made with with the PySpace library. It's not really user-friendly, but if you're an experienced Python programmer that's interested in rendering your own fractals, you can check it out here:
https://git...
this was how i found them -- cool collab
sintel seems to have niche appeal but ho boy is it made for me!
roderickvd is sintel?
nu but sintel is the latest commit to cpal
well, i was just looking at https://github.com/NiklasEi/bevy_kira_audio .. is there a reason to use this over that? i don't know anything about kira, seedling, firewheel
oh lol when i loaded this topic it only had one post
kira is rather limited compared to seedling but might be able to meet your use case
i havent used kira in a bit, but I also think seedling does a good job of leveraging the ECS in a really nice way instead of as incidental and is generally more expressive and enjoyable to use (at the sacrifice of maybe a bit more complexity?)
really all i need is panning controls. fundamentally all it's doing is basic 2d spatial audio anyway, so maybe it's better to just use the builtin spatial audio
Yeah, if your needs are relatively simple, they may already be met by bevy_audio!
bevy_audio appears to have no way to pan audio
so if i'm using seedling, do i disable the bevy_audio default plugin?
My preferred approach is to completely remove bevy_audio with feature flags. The getting started section goes over this. You'll end up with a toml looking something like:
[dependencies]
bevy_seedling = "0.7.0"
bevy = { version = "0.18.0", default-features = false, features = [
# 2d
"2d_bevy_render",
"default_app",
"picking",
"scene",
# 3d
"3d_bevy_render",
# ui
"ui_api",
"ui_bevy_render",
# default_platform
"android-game-activity",
"bevy_gilrs",
"bevy_winit",
"default_font",
"multi_threaded",
"std",
"sysinfo_plugin",
"wayland",
"webgl2",
"x11",
] }
You can enable both -- they don't conflict. The above should represent the exact set of default features (minus audio-related ones).
aight
i think i need to use a different font system anyway lol
maybe not .. i wanted to set kerning
For simple, direct panning controls, you can use the VolumePanNode. Or you can just use the SpatialBasicNode, either way.
the sounds play and not play kind of at random?? i'm doing e.g.
commands.spawn((
SamplePlayer::new(assets.load(spec.soundData)),
PlaybackSettings::default()
.with_play_from(PlayFrom::BEGINNING)
));```
shouldn't that play on the default pool? i can't tell when sounds play and when they don't but it feels like random whether they do or not
i mean, i can tell when they should be playing, but i can't discern any pattern as to when it plays and when it doesn't
it shouldn't matter if i'm doing that spawn in an observer. . . . . ?
this is with GraphConfiguration::Game and On<PlaybackCompletion> is firing with PlaybackComplete every time
it looks like you are loading the sound while spawning the sample player. im not too sure of the interactions here but your app needs to load the file and then play it which may cause delay. you might wish to load the audio handle first and then spawn the player when you are sure it is loaded.
thats my best guess, could be something else (corvus yell at me if i am wrong).
im assuming your error is sounds either play late or not at all. that sounds like the audio is trying to be played before it is loaded to me 🤔 but i actually dont know how the sample player handles unloaded assets.
maybe see if adding assets.load(spec.soundData) to a Startup system makes it work by hackily preloading (assuming this sample isn't played the first frame of the game. the second time the asset is loaded in your spawner code it should just return the already-loaded handle). if it does, you might want some states to preload all of your assets.
Try running in release as well.
What does happen if the asset is not finished loading but is added to the sample player? My expectation is that it would be played once the asset is loaded but I'm not sure 🤔
I made an API choice that is clearly a mistake in hindsight (and will be fixed soon). When a sample is loading, the entity keeps track of how long it’s taken. Upon loading, the entity is queued and the playhead skips to where it should be had it loaded immediately.
So without optimizations and with short sounds, they can be skipped entirely.
The idea was to guarantee sounds are always strictly in time.
ahh okay makes sense. yeah that feels more like behavior id want in special situations
oki what the heck im gonna go fix it now and release a patch
i wanna definitely make sure moon doesn’t hit this papercut
oh but yes to be clear, preloading the assets will fix this!
in the meantime
It would still be good to have this behaviour as an option but yeah, in a lot of cases playing late is better than not playing at all
Probably playing late should be the default
I think I'll move the behavior to a marker component.
It shouldn’t be on playback settings?
Well, that would be a breaking change. Of course, I could make a new minor release.
But I'm roughly 50/50 on whether one is better than the other.
Maybe playback settings should be non_exhaustive so you can add stuff like that
hmm
i feel like the behavior is uncommon enough to where a marker component is fine, but also not super opinionated
Yeah, plus it may be useful to query over these precisely timed samples.
already am ye
i should be preloading the sounds
#[derive(Debug, Resource, Reflect)]
#[reflect(Resource)]
struct MascotSounds {
sounds: Handle<LoadedFolder>,
}
fn init_sounds(mut commands: Commands, assets: Res<AssetServer>) {
let sounds = assets.load_folder("sounds");
commands.insert_resource(MascotSounds { sounds });
}
i can tell it's doing the preloading at least to some degree because there's a lot of ```
2026-03-15T11:35:13.648236Z INFO symphonia_format_riff::common: ignoring unknown chunk: tag=smpl, len=60.
in the logs during startup, and not during actual attempted sound playback
Ah, yes that looks correct!
so yeah i'm not sure what else would lead to playback being erratic in the way i described
Is this with volume modulation going on, or merely playback?
i'm just trying to get this to work at all before applying volume effects. i tried spawning SamplerPool(DefaultPool), sample_effects![VolumeNode::default()] components with the sound just to see if anything would affect it
Hm, would you mind sharing your code? Feel free to make a gist if it's a little big!
it's very big :( i can try to make a minimal reproducing example, but when i tried running the examples in the bevy_seedling repo everything seemed fine
This makes me a little worried, to be clear; if this is getting spawned with every sound, it'll be constantly recreating the pool!
is it possible it could be because i'm implementing AssetPath?
It should be
DefaultPool,
sample_effects![VolumeNode::default()]
SamplerPool(SomeMarker) creates a new pool, or replaces the old one if it exists
in assets.load(spec.soundData) that soundData thing is my own struct
ohhhh
So that might be why it's so intermittent.
like, i'm not sure if LoadedFolder is somehow preloading things with a "different" path or not
I think it should log an error if it fails.
Yes, the asset server will log an error.
but, the thing that's even more confusing is that sometimes a sound will play, and then trying to play the same sound again later won't play, and then later still it will play again
because, shouldn't preloading it once be enough?
3–4 at most
Oh okay that should be fine.
The default voice limit is 24 simultaneous sounds per pool.
yeah i'm not anywhere near that
this all feels sufficiently weird i guess i need to try making a test case
sigh
Sorry for the trouble! Hopefully it's something simple.
and, like, spawning sounds from an observer system shouldn't be different in any way?
Yes that should be totally fine.
as a last-ditch effort, if i turned up logging inside bevy_seedling, might it spit something out?
Hm, possibly, although there aren't too many logs below info.
can confirm haha
i know it's using other crates which might be doing their own logging
Yes that's true.
well, symphonia isn't logging anything either
aight yeah i'm going to eat some breakfast then attempt to reproduce this minimally
Okay, thanks! I'd love to get to the bottom of this.
mhm
@ionic sedge wild-ass guess but as i'm putting this example together .. the ordering of add_plugins calls shouldn't matter, right?
Correct it shouldn't change anything.
In general this does have an effect
But you should try and write your plugins such that it doesn't
@ionic sedge ok well i have something. github gist of just this one source file? bc there's also like the Cargo.toml and the assets. i can probably make a self-contained tarball and post it here
A gh repo would be perfect if it's not a problem for you.
gists are repos too ;3 unless their hooks reject branches with non-flat hierarchy now
i'm curious what will happen if i put a wav file in a gist, but we'll see
yeah no
remote: Gist does not support directories.
remote: These are the directories that are causing problems:
remote: src, teraknorn
i can try minimizing this more, but i was surprised at this behavior from this code, so
i don't know if there's something obvious to you first
minimizing it more and it isn't playing still :T i even set it to play looping and it thinks it's doing something but i don't hear any audio
i've been wondering if this is a macos thing
i can also get on vc if that would help lol
thanks! i shjould be able to get to this in a bit
Hm, oddly enough I'm not getting any sound at all. My setup might be messed up though. I'll investigate more tomorrow.
yeah, i'm not getting any sound either
that's why it's so puzzling
Regarding "no sound"... maybe a completely separate issue, but I am running into complete silence, on Linux, but only in release builds. (bevy 0.18, firewheel 0.10, bevy_seedling 0.7.0). I doubt it's a system issue; a previous build of my game jam game using the same dependencies does generate sound in release, though a fresh release build does not.
The default log messages don't differ between debug and release. But rebuilding with firewheel/log shows consistent differences in debug and release. Perhaps there's some unexpected node ordering issue that breaks things? I don't know how to interpret these logs yet.
See https://gist.github.com/eswartz/c62b87518312f336fc741640a5059969 . I am able to use bevy-inspector-egui in release if there are ways to analyze this at runtime.
bevy_seedling/firewheel silence in release mode (https://github.com/eswartz/bevy-game-jam-7.git) - debug.log
Hm, a semver-compatible update in some dependency may have broken something, just yesterday even!
Well, only firewheel-graph changed from 0.10.0 to 0.10.1 for the example above. I'll try downgrading it.
Aha, I get audio when using [email protected] ... but the logging doesn't change at all O.o
(FWIW if anyone is testing my git repo in the gist, you only need to visit the startup menu to hear/not hear sounds)
@static quest is it possible that the clamp somehow..... made everything silent? Is the hardclip change the only thing that made it into 0.10.1?
Hmm, it shouldn't.
I know right, haha. Although I noticed things suddenly going inexplicably quiet in my own refactoring last night too.
Hmm, strange. Is this on the current crates.io version, or the master branch?
I figured it was just a mistake I made somewhere, but it would be quite the coincidence now that others are reporting it in the published crates!
Yes, the published [email protected] seems to be implicated.
I suppose it could also be spurious, and maybe coincidentally triggered by 0.10.1. Maybe some of that UB is finally causing problems?
Only being silent in release supports this possiblity.
Have you tried running cargo update?
Yes.
I can also report hearing sound in dev, and not in release.
And then cargo clean?
Yes. (I know what I'm doing 🙂 )
So does bevy_seedling add the nodes in the same order every time, or does it add nodes asynchrounously?
Anyway, I'm almost done with the fixed-resample update, so I'll look into it when I'm done.
It depends on the order of entities in their tables, so it may not be deterministic.
Although I think it should be effectively deterministic in practice.
As a note, the ordering of the nodes does not seem to be an indicator of anything. I got the same log differences in debug vs. release builds wrt logging (before and after the firewheel-graph version change), so I think that was a spurious difference. And yes, the order was consistent between runs.
any update? or anything else i can do?
The current lack of sound may be upstream of bevy_seedling. Looks like we'll have to investigate for a little bit. I haven't seen this happen before!
strange! a regression? i could try an earlier version or even bisect on git
Can you try pinning firewheel-graph to exactly 0.10.0 like Ex-Sorbitol?
(Note that when running without --release, the repro seems to work as expected for me. Not sure if that's what you're seeing.)
i've only been running it with --release
i'll try without
that pin fixed --release instantly
bizarrely, unpinned without --release also plays sound
I'm seeing the same.
@ionic sedge So does it work if you disable hard clip in the settings?
i'll have to do a quick test in a bit
You could also try adding assert!(s.is_finite()); here to see if there is any funky float business going on. https://github.com/BillyDM/Firewheel/blob/f25b53a5df68e983e602cf62473528b60757f1ad/crates/firewheel-graph/src/processor/process.rs#L170
also, when i turned off default-features for bevy to disable bevy_audio, some of my graphical rendering stopped displaying. is there a way to diff enabled features? i'm not sure what feature it was using
feature management can be tricky! there's not really a built-in way to do this kind of feature diffing
you could probably compare lockfiles though
i was trying to do that .. but do lockfiles list what the enabled features are?
i don't see any plausible key in Cargo.lock that contains features
Ah, sorry. It's purely dependencies.
Ok, the new version of fixed-resample and symphonium is finally finished!
is there a way to anti-pin a version? in python you could do dep = "0.11.*,!=0.11.1"
i don't see cargo.toml having the same
I don't believe so, unfortunately
It would probably be a good idea to add a debug feature that checks if any output sample is not a finite number.
How would you prefer that feature be implemented, as a feature flag, or as a boolean in FirewheelConfig? And is it ok if it panics if a non-finite number is detected, or do you think it should be an atomic flag that the logging system can poll?
It would also certainly be helpful to have a tool that can turn the debug output of the Firewheel schedule into a visual graph. But I'm not sure where I would even start with that.
I believe people have done this with graph visualization tools for Bevy schedules.
Though I would be kind of suprised if there was a mistake in the compiler algorithm. It's pretty much just Kahn's algorithm with a reference-counted buffer allocator.
Panicking is pretty bad in my opinion -- can the processor reasonably recover from nans?
Yeah, the OS might just output silence if an application gives it invalid samples, which is why I'm suspecting it.
And I suppose it could be the UB in the schedule module (making an &mut from an &).
Just wondering, since you seem open about knowingly doing fishy operations :), have you seen runs of cargo miri for analyzing UB?
Though we did fix the UB in a recent PR, so it might not be it.
ddid that make it to 0.10.1?
It would be quite funny if it finally triggered now, of all times.
No, it was one of the recently merged PRs.
I probably should add the fix to 0.10 though.
Hm, that should be included in my refactoring work I'm doing, so it looks like none of that resolved it.
I'm working on top of main right now.
@ionic sedge Alright, I added some stuff to help debug the silence issue: https://github.com/BillyDM/Firewheel/pull/119
thanks! i'll pull this when i get back to refactoring later
Ah, sorry it looks like I actually was behind still on my local branch. Pulling the latest changes resolved the silence issue. Of course, since it was spurious, I can't exactly be sure these commits truly fixed it.
I was in fact at this commit, before vero's latest UB PRs https://github.com/BillyDM/Firewheel/commit/d0251f05317f5316dd2fbb1fa4db32a5ebf1a464
would PlaybackSettings::despawn_at make sense to have? if i'm fading some audio out, i want to despawn the player when it's done
you can just despawn the entity when volume is silent
well.. yes.. but how do i know when that's happened?
if volume <= Volume::SILENT ?
i mean, do i make a system for this checking every sample player every update?
Some sort of arbitrary event-handling seems to be a common desire here. In an ideal world, this would be handled by a unified Bevy animation system.
In the meantime, though, maybe it would be a good idea to provide the capability in seedling.
Ah ok, it might have been UB problem then. Hard to tell though.
@ionic sedge Ok, I patched the UB in version 0.10.2 (and yanked 0.10.0 and 0.10.1). Hopefully that fixes the issue!
Thanks! I'll give it another test later today to see. I suspect the code changes would make it disappear incidentally anyway, since we only managed to trigger it just now!
Do any of you (BillyDM perhaps?) happen to know how DAWs estimate the latency of the output audio device?
Usually the OS's audio API tells you. I'm not sure if CPAL does though.
But if you know the block size, then you know that it is at least sample_rate/block_size seconds.
Oh wait, no, it's block_size/sample_rate seconds.
Yeah I guess that’s better than nothing. I could also have a calibration screen like in guitar hero
Isn't there also hardware estimates? The web audio API actually provides this.
Yeah, rhythm games typically feature calibration settings.
I know RtAudio provides the latency. I'm not sure about CPAL.
Maybe I’ll just use the audio engine latency for now since that’ll make sure I actually have some kind of latency handling and do something smarter later
Interesting, I’ll look into that
Ok, it looks like CPAL does provide something similar, just in a different way. Though strange design decision sending this information to the audio thread and not the main thread where you would actually want to know it. https://docs.rs/cpal/latest/cpal/struct.OutputStreamTimestamp.html#structfield.playback
Oh that’s really interesting, well the main thing I want it for (synchronising inputs and animations with audio) requires sending info from audio to main thread anyway so it’ll do
Any idea how I’d access that from seedling?
Ok yeah, that makes sense.
Yeah, Firewheel currently doesn't expose that. It might be possible with a custom CPAL backend.
Oof, well that’s a bit out of scope for now unfortunately
Though I suppose it wouldn't hurt to add an "playback_instant" field to ProcInfo.
Yeah that would be nice, I can’t be the only person who cares about that. I actually just saw someone online complaining about audio latency for non-rhythm competitive games since human reaction time to audio is much faster than for visuals
Oh wait, shoot. It's not a standard Instant, it's just the number of seconds that have elapsed since an "unspecified origin point, usually at or before the stream starts". Well that's useless. https://docs.rs/cpal/latest/cpal/struct.StreamInstant.html
so, right now, most things get cleaned up bc i’m just fading a oneshot sound to mute and it “finishes” like normal. music and other looping sounds is what i’m concerned about
Oh actually, no it can be used. You can subtract the callback timestamp from the playback timestamp to get the amount of delay between the process function and when the data will be delivered to the DAC.
also, whilst i’m thinking about it, is there a pattern for “play after loading” other than checking the AssetServer before a spawn?
i guess i can have a unified schedule system of my own that starts playback after load and despawns after fadeout
well, that’s what it does now! if you just load the handle directly
but doesn’t the play head advance by the time elapsed during the load?
not in 0.7.1 ;)
yep!
ok
let me know if it results in any improvements to your project
probably! i’ll try now. i still need to add despawning tho
Ok, I added a playback_delay field to ProcInfo!
Wow, nice!
Do you know if CPAL includes the delay from the buffer size in that? If not, it might be worth calling it output_delay or something to make it a bit clearer that it isn’t the entirety of the latency. Maybe worth having a simple helper method for the buffer delay and for the sum of the two if you’re in the area anyway, that’s easy to implement in user code though ofc
Anyway, the info's there now so I can dig into it when I get to figuring out the audio subsystem
Well it specifically states that it's the delay between when the process method was called and when the data will be delivered to the audio device. The delay between the main thread and the audio thread is not part of that delay.
Ok cool, yeah that makes sense
I could call it process_to_playback_delay to make it clearer.
I think that playback_delay is fine if the only other delay is between the main and audio thread, I don’t think it would be expected that firework would (or could) handle that
I kind of like the clarity, so I think I'll go ahead and change it.
So long as the delay is starting from the start of the process call and not the end I think the naming is fine
Sure, better more explicit than less
Yeah, it's the start of the process call. (The OS would have no way of knowing how long you code will take to process in the audio thread. 🙂 )
@ionic sedge Oh yeah, thoughts on this PR? I'm fine with it. https://github.com/BillyDM/Firewheel/pull/117
lemme take another look right now
Oh looks like I messed up the formatting when fixing merge conflicts. 💀
Oh, I messed up the imports too.
Ok, fixed them!
I want to give one final thought to what value we should use for the DEFAULT_DB_EPSILON (as in how quite must a signal be to be considered "silent").
Right now it's at -100dB, but that might actually be a bit excessive. Apparently ffmpeg uses a default of -60dB: https://ffmpeg.org/ffmpeg-filters.html#silencedetect
ya i see -60 pretty often
We could split the difference and go for -80dB. Though if -60 is commonly used, then maybe we should use it too. I'll do some more research to see if I can find anything.
Hmm, apparently the KFR DSP library uses -100dB as the default.
Hmm, JUCE uses -100dB as the default as well.
That's probably fine then, to be honest.
Yeah, I'll ask in the audio programmer discord to see what they think.
Worked for me too. Thanks!
Oh sweet! Well, that's the first time that using unsafe actually came to bite me in the butt later. I have now earned the right of passage as a Rust developer! 🦀
I’m like 80% sure that ableton uses -60 but I can’t remember for certain and afaik it’s not publicly available info, my last company used -80 but that was for live audio where the dynamic range can be higher and the users are expected to be professionals
Both of those are expected to work with audio input though, you can probably use a much lower value for an all-digital system
Oh wait no scratch that, the -60 was related to some max/msp thing. I don’t know what ableton live uses
I’m kinda surprised to hear that, -100 is crazy low. Maybe because it’s a framework they’re expecting devs to have their own silence detection and that’s just a fallback?
Like, 100dB dynamic range covers a whisper on the other side of the room to instant hearing loss
I went to go check my numbers on that and got this incredible AI summary that classifies rustling leaves as "mild hearing loss"
To clarify: I mean that you can expect digital effects/samples to produce true silence if that’s their intention. That’s maybe a justification for JUCE using -100 but it still seems kinda unnecessary, -80 seems more reasonable to me. Def get a second opinion though
I'm setting up audio buses and pools this way:
for bus_id in AudioBusId::iter() {
commands.spawn((VolumeNode::default(), EffectsBus(bus_id)));
commands
.spawn(SamplerPool(EffectsPool(bus_id)))
.connect(EffectsBus(bus_id));
}
Attaching sounds to pools this way:
fn attach_pools_to_new_sample_players(
newborn_sample_players: Query<(Entity, Has<MusicTrackPlayer>), Added<SamplePlayer>>,
mut commands: Commands,
) {
for (sample_player_entity, music_track) in &newborn_sample_players {
commands
.entity(sample_player_entity)
.try_insert(EffectsPool(if music_track {
AudioBusId::Music
} else {
AudioBusId::SoundEffects
}));
}
}
Yet, I'm gettiing the following error:
Queued sample "audio/sound_effects/card/card_ui/card_hovered/cardHovered2.ogg" with effects in an effect-less pool.
What could be the cause?
any reason your EffectsBus and EffectsPool specify an id instead of being simple markers? I honestly don't know if that is the issue but it seemed out of the ordinary to me, unless i missed something
I like having enum-based markers so that it's easy to make more
Also I can just tell something to go like:
Are you a sound effect? Route yourself to AudioBusId::SoundEffects (which I know if Pool(SoundEffects) routed to Bus(SoundEffects))
well, i think that seedling might be operating on those at a component level, so i'm unsure if an enum of the same type would work (?) i am very unsure about that though
idk why it would result in that warning either way
Worths a check
Thanks
if that doesn't work i'm sure corvus has some better advice
Other than this warning:
- It seems like buses don't affect all instances of pools connected to them
- I have a loop that should keep ongoing silently (it has a silent volume node) unless specified otherwise. Yet, when I connect it to the music bus, I can hear it as loudly as the rest of the ongoing music instances. Why?
did you by chance create your own type called MusicPool
potentially see: #1378170094206718065 message
Nope, my only pool type is EffectsPool(AudioBusId) (named after the one in pools and buses example)
i'm trying to figure out how to have an effect that i can tune how 'dry v. wet' it is. like if i have a LowPassNode and by default 0% of sound goes through it (totally 'dry') but sometimes i want to hear 80% that and 20% not-that (80% 'wet')
it seems like SendNode would be involved in some way, but i can't quite put it together in my head
like, i guess i can have a SendNode chained to a VolumeNode, but then i have to change the parameter in two places to make sure i don't have >100% of the sound. unless that's the best i can do
Dry/wet for filters is notoriously finicky because filters shift the phase of the input audio, so mixing it with the dry signal will cause interference. If you just want to get it working though, the way you’d do it is to basically have a send node with a linear volume of your wet amount and then have a volume node after the send node with a linear volume of your dry amount, and then have both the return and the dry signal go to the same mixer node
yeaaaahhhhh ok
Yeah you just need to do that. You can build your own abstraction though. Like, you could have a DryWetNode<T>, with the type parameter being the node you want to wrap
i understand if the filter changing the phase might make it sound off tho
It’ll be alright, it’s something you’ll notice if you’re a music producer but it’s an effect that people use deliberately sometimes
heh
This is an interesting thought — you really could just wrap a node like this 
Honestly it wouldn’t be the worst idea to just have an abstraction in seedling, since it’s such a common thing
i haven't tried it yet but i expected i could make the generalization extend to schedule_tween too
Nu -- you can use them as such, but they're designed to differentiate by value if you want. This is what the dynamic pools do (pools created on-the-fly when you just want to slap down some arbitrary effects).
At least, that's how it's supposed to work. Seems like something might be going wrong.
Both a node label and bus label will insert a type-erased interned trait object, which is what I actually query for.
neat ok
(What should I do then)
When does attach_pools_to_new_sample_players run?
on Update
I wonder if it's getting to them after one cycle.
I also tried putting it in an observer, which changed the result but it was funny still
At which point they may have already been assigned to the DefaultPool
Interesting
But that shouldn't happen when running in an observer I assume
(Thinking)
Okay to guarantee it, you can put this in Last and order it before SeedlingSystems::Acquire. Or PostUpdate should be fine if you only spawn stuff in Update.
I'll PostUpdate it
Ah, actually this wouldn't guarantee it. I need to update the ordering.
(It'll take a few minutes to compile since my project is already dozens of thousands of lines and it's down below in the systems crate)
PostUpdate will work though.
Yes it's actually almost at demo state (I posted it on #showcase )
I'm closing a few last things, mainly settings, where I want to allow users to control the music and sfx settings and that's where the problem arises
An observer should work well too, btw. I wonder what issue you ran into there 
I don't remember exactly, but it's currently staring-at-screen time
Also I should ask- if a track plays with VolumeNode is set to 0.0 (linear)
And I route it to a bus where the VolumeNode is set to 1.0
It should result in it still being 0.0, right?
yes yes mhm 
We'll see
In a few minutes🥹
So um
If I put the sound effects bus on 1.0
All of the sound effects play at 1.0 (including, say, ones that should be entirely silent and are also very far away)
I'm not sure they play at 1.0 exactly but they're loud when they should be completely silent
The nodes are multiplicative, so it can't be raising the volume of previous nodes! Seems like something odd may be happening.
oh no
What it feels like is that it overrides their audio
Like, the volume of each player
let me check that my queries are absolutely correct
Ah yeah I could totally see a rogue query messing them up.
pub struct AudioBusUpdater;
impl Plugin for AudioBusUpdater {
fn build(&self, app: &mut App) {
app.add_systems(Update, update_audio_buses.run_if(resource_changed::<PlayerPreferences>));
}
}
fn update_audio_buses(
mut buses: Query<(&mut VolumeNode, &EffectsBus)>,
player_preferences: Res<PlayerPreferences>
){
for (option_tag, bus_id) in [
(OptionTag::MusicVolume, AudioBusId::Music),
(OptionTag::SfxVolume, AudioBusId::SoundEffects)
]{
let value = player_preferences.get_or_default(&option_tag);
for (mut volume_node, EffectsBus(bus_id_of_effect)) in &mut buses{
if bus_id == *bus_id_of_effect{
volume_node.volume = Volume::Linear(value);
print_info(
format!(
"Setting {:?} audio bus volume to: {:?}",
bus_id, value
),
vec![LogCategory::Audio],
);
}
}
}
}
That's exactly how buses should be updated, right?
I'm updating the buses, not the pools
Yes this looks like it shouldn't do anything problematic. As long as the EffectsBus components are only on those bus nodes.
I'm still getting these (the sounds listed here are some of the ones that should be completely silent but aren't)
looks like they aren't silent because they have no effects thus no modifiers
I should probably report the pools and effects involved 
There's one effect bus for each bus id (see spawner above)
I assume this is what's happening
But why would they disconnect?
Hm, I'll have to take another look in a bit here.
I doubt it's a problem in the crate if it works for everyone else
ECS APIs can have some hard-to-anticipate bugs!
Yea I learned that in the last couple of... years 🥹
Still, whenever I make a game in not-bevy I miss bevy
I can't wait for archetype invariants 
(I don't know what that is so I guess I'll just have a fun surprise one day)
The idea could encompass many tools, but the idea is that you can enforce invariants about archetypes. For example, A and B can never exist on the same entity.
Reverse #require it is (sounds very useful)
Anyway I activated the log here and it seems to work just fine
Other than all the things that are for some reason disconnected
would remove so many of my system ordering declarations
"yes i know these two systems are ambiguous because no entity should ever have these two components simultaneously"
so, here's a question.. what frequency should i use for a low-pass filter that's passing everything? i tried f32::INFINITY at first and that seems to uhhhh make lerp unhappy
i hardcoded 48_000 which is good enough i guess
20k is probably fine, although technically there's some attenuation there
you could calculate it based on the filter response but it really doesn't matter much, maybe 24k if you want to be careful
Maybe there's something wrong with the way I connect the pools?
For the record, I just replaced them with empty labels (without enum field) and the warning persists
commands.spawn((
SamplePlayer::new(asset_server.load("audio/sfx/surface_impact.wav")),
Transform::default(),
RandomPitch::new(0.15),
PlaybackSettings::default().preserve()
))
- - - in another system some time later - - -
playback_settings.play();
is this not the correct way to play it?
It plays once at the beginning but then .play does nothing
also I'm using bevy 0.16 so bevy_seedling 0.5
I think they clear automatically
I spawn a new sound every time
to my knowledge .preserve() makes it so they don't clear automatically
I was doing what you said before but there's this TINY amount of delay before the sound starts playing and it's just about noticeable so I wanted to know if it was caused by spawning a new entity and waiting for a command flush, or if it's just audio latency - and if the latter I'll probably be right back here asking how to reduce that
I think I need to reduce the buffer size or something but no clue how
I also tried spawning the sample players with their pools
I think my next step would be to copy my setup to a clean project and see if it still happens
I have no idea what the problem is
Is there a way not to use pools and still use the bus?
Maybe if I automatically .connect() all volume nodes?
Just reinsert the SamplePlayer! This will acquire a new node in the pool. At least for now (and in 0.5), preserve doesn’t mean they’ll hang on to their allocated sampler.
The delay may very well be loading the sample — are you preloading them at all?
Yeah, sorry for all the trouble here! A repro would really help narrow down the problem.
This intuition is interesting to me though. 
I think I should probably make it more clear in the docs why that won’t work
I was preloading and storing the handle yes
I just now realised my issue and man, I am livid
to give some context I've been trying to solve this all day, didn't want to come in here and ask right away
and it just dawned on me I'm wearing bluetooth headphones aaand I'm pretty sure that's the reason
I record it with obs, 0 delay (which is actually strange given I was still wearing bluetooth headphones watching the obs clip...)
does the mp3 player know that there's audio latency and compensates, I've begun to question everything now omg
Oh, yeah they introduce quite a bit of latency! Youtube and other players can compensate for this by momentarily holding back the video stream for a moment when it starts playing.
I'll give it another go
Thanks a lot!
I still didn't manage to replicate them but I did notice someting new. Audio players are time-scaled, so that different ones would change their speed based on the context in which they belong. However, they no longer respond to the time slowing down/pausing.
That's the query for slow down:
(&TimeScalerSubscriber, &mut PlaybackSettings, &SampleEffects),
Added<TimeScalerSubscriber>,
>,```
Which means one of those (sample effects?? because of what it says in the warning) is not present
Not all SamplePlayers will necessarily have SampleEffects. Maybe you already knew this -- if a pool has no effects then the player won't either.
It's just a Bevy relationship, so an empty set will mean the component doesn't exist.
Are the effects of the pool overriding the effects of the player?
When applied to a SamplerPool, SampleEffects serves as a template for all samples played in the pool.
Samples played in a pool don’t need to respect the ordering or presence of effects; when a sample is queued, missing effects are inserted and the order of effects is corrected.
But no, the effects are never overwritten if they're already present.
I see
It could be useful for performance (less entities)
But that doesn't explain why I'm getting the warning 🥹
commands.spawn((VolumeNode::default(), EffectsBus(bus_id)));
commands
.spawn(SamplerPool(EffectsPool(bus_id)))
.connect(EffectsBus(bus_id));```
The pool DOES have no effects though
right
Well if it doesn't, and you queue a sample with effects into it, then you'll get the warning.
Ok so that explains it after all
um
So how should I approach having a music bus and a sound-effects bus?
Put another way, the set of all available effects must be declared when spawning the pool.
Well what do you want them to be able to do?
Oh
I want to be able to control all music volumes and all sfx volumes from a slider in the settings
So I'm spawning a bus with a VolumeNode for each
Not each sample
I'm spawning an sfx bus and a music bus
Then a pool for each of the buses
Then I add the "pool subscription" to each new sample player
Well the pool routes into a single node already (by default a volume node), so you can query for that and change it directly!
This approach is correct, right?
Could you please clarify?
Does that mean I need no bus?
You don't necessarily need the buses if you just want to control the volume of the pool.
Yes.
For example
So what I should do instead is spawn each pool with a sample_effects![VolumeNode ... ] then control their volume nodes directly?
commands.spawn(SamplerPool(MusicPool));
fn adjust_all_music(music_volume: Single<&mut VolumeNode, With<SamplerPool<MusicPool>>>) {
music_volume.volume = Volume::SILENT;
}
Oh so not as a child even
All the samplers route into the implicit VolumeNode of the pool.
I could put it right on the entity
To be clear:
// this
commands.spawn(SamplerPool(MusicPool));
// is precisely equivalent to this
commands.spawn((
SamplerPool(MusicPool),
VolumeNode::default(),
));
// though you could put any node on the pool if you want
And all the samplers are routed to the SamplerPool<T> entity.
Another important point here
I can see that it's a SamplePool<T> but if my T is not a tag (my pools are something like Pool(EnumVariant)) I should be able to access them through a field in the SamplePool<T> to check what enum variant they have?
ya you can just check the value (.0)
great
Ok, so that should solve it
I'll let you know how it goes
Thanks
That would not silence the warnings though I assume
Although the pool does have an "effect" that is the volume node
So the non-spatial sounds seem to be fine now
However
Does the pool of players with ItdNode and SpatialNode and such also need to have those?
And also the error where the music that should be silent plays is still there
I take it back, I thought that it changed but it seems to be the same
Currently the setup is:
Two pools (sfx, music), each is spawned defaultly (with a volume node on it)
Each player has some effects attached as children (ranging from just Volume to 5 different effects) and has a pool it subscribes to
ranging from just Volume to 5 different effects
Are these specified in the pool?
I'm not sure what you mean
Each player has its own effects
I could make different pools for each combination of effects (but, there are many combinations, so I'd rather not)
(For example, regular enemies have volume and spatial nodes. If an enemy is elite, their sounds also get lowpass and freeverb. So having the effects on the pools would mean having two pools for the enemies only, and having to route between them)
I could do that, but it would take time
Is there no other way to control all the volumes?
I feel like we're continually talking past each other on a couple points. But I'll address this one first!
I could make different pools for each combination of effects (but, there are many combinations, so I'd rather not)
This has been brought up before in here actually. I would recommend adding all effects you might want and then having the default values be effectively disabled by default. For example, you might have a low-pass filter at 24kHz by default.
This is a massive advantage for performance. It allows you to not recompile the audio graph for every sound played, and Firewheel can skip a lot of effects that are disabled.
Maybe we can figure out a nice way to preserve performance without requiring this kind of planning, but it's a touch tricky. Note that dynamic pools give you the ability to describe sets of effects on the fly, but the routing is more tricky.
(Sorry, I'm trying to focus but I'm tired)
No worries -- if anything I take it as an admonishment against my APIs!
Ideally they should be easy to understand.
So I think they could use some work.
Ok so let me see if I understand now
What you suggest is:
Two pools (sfx and music)
Each one has all possible effects
Sound players that don't want to use a specific effect should have it disabled, those which need an effect with different values than the one in the pool (say, something should have a more aggressive lowpass) should have that effect as a child(?)
How would that work with reverb for example?
Is there a "neutral" reverb?
What I mean is:
If we look at volumes, I could have the pool's volume be 1.0
This way, if an effect has 0.3 volume. or 0.0, or any volume, its final value stays the same
How can that be done with the others?
Reverb should generally never be used per-node -- that is, you should have some reverb bus that you route to with, for example, a SendNode.
A convenient node for routing to sends.
Ok, let me lay out the plan and I'll start working on that first thing tomorrow:
- Add all possible effects for the two pools in a default state
- Instead of putting on nodes only the effects you want, put on them the effects you Don't want as well, but have them disabled
- For reverbs, instead of specifying a reverb for that node, create nodes (pools?) for each possible reverb, then connect each one to its relevant pool (so sfx reverbs to sfx pool etc), and then whenever a sample needs reverb, connect it to the relevant reverb node using SendNode
A convenient node for routing to sends.
@ionic sedge (is that a good plan?)
Instead of putting on nodes only the effects you want, put on them the effects you Don't want as well, but have them disabled
If the default state (i.e. the values as provided in the pool entity) are disabled, you don't need to explicitly mention them every time you spawn aSamplePlayer! That is, you'd only have to mention the effects you care about.
create nodes
Just a simple node will do! You can create node labels for the reverb or simply useEntityto connect to them -- whatever suits your preferences.
If you need more context on the reverb setup, let me know and I can show you in a bit more detail.
Oh so actually all I need to do is add all the effects disabled to each pool
and then also the reverb-send thing
But I'd still have to create a different node for each "reverb config"
possibly yes, depending on your needs
Ok so, let's revise my task list haha:
- Add all possible effects for the two pools in a default state, just disabled
- Instead of specifying a reverb effect for each node, create nodes (and tag them with ReverbConfig(ReverbId)) for each possible reverb, then connect each sample player to its relevant node using SendNode. Each one of the reverb config nodes still has to be routed to a pool so they can be controlled from the settings, which means I might want to have some with the same Id, just different ones for music and sfx.
Ok now
Nice!
Thanks for the help
Of course 
@ionic sedge um
How do I disable a node?
depends on the node
we only have ad hoc methods frankly
maybe we should add a bypass
what kinds of effects are you arranging
I'm looking for a way to "disable" lowpass and freeverb so I can put them in the pool
Well freeverb is tricky, but you can disable a lowpass by setting the cutoff beyong the audible range!
Firewheel's lowpass also just has an enabled param
I need another node consolidation pass to remove seedling's old stuff.
I see
Well, I guess the solution for now would be to
Also make a different pool for things with lowpass, and for freeverb
Still getting warnings and all the bugs are still there (I assume, due to the effects thing still)
Currently spawning spatial sound pools with the spatial sounds nodes on default config:
fn spawn_audio_pools(mut commands: Commands) {
for pool_id in AudioPoolId::iter() {
let mut pool_commands = commands.spawn(SamplerPool(AudioPool(pool_id)));
if pool_id.spatial() {
if let AudioPoolId::SoundEffectsSpatialWithExtras = pool_id {
...
} else {
pool_commands.try_insert(sample_effects![
SpatialBasicNode::default(),
ItdNode::default(),
BlockedSoundNode::default(),
]);
}
}
}
}```
The problem was solved by adding VolumeNode in sample effects
Thanks for all the help and patience
The main problem is declicking. If we add a bypass method in the firewheel engine, we will also need to add a declicker. Maybe it could be done, but we should figure out if it's really necessary or not.
The core of the problem here is that the pools need to have all of the effects their users have and some of the effects don't have a "neutral" node.
If we do, we would also need to add an on_bypassed trait method to nodes so that nodes can clear their buffers or whatever they need to do.
Yeah, I agree that's a problem. Maybe it could be worth adding bypass at the engine level. The main tricky part is event scheduling, but it might be possible.
I'll look into it.
i kinda have always thought manually needing to add a bypass/enable whatever is a little weird but also idk if it applies to every node
i keep getting a crash when i try to pause a player:
Encountered an error in system `bevy_seedling::node::follower::param_follower<firewheel_nodes::volume::VolumeNode>`: Failed to apply audio patch to `firewheel_nodes::volume::VolumeNode`: InvalidData
3: bevy_seedling::node::follower::param_follower
at /Users/hab/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy_seedling-0.7.1/src/node/follower.rs:81:13
this is the pause code, and it still crashes if i remove the volume change too
playback.pause_at(time.delay(FAST_FADE) + DurationSeconds(0.01), &mut events);
volume.fade_to(Volume::SILENT, FAST_FADE, &mut events);
i didn't think i was changing the volume from another system, but i might be
Ah, I have a suspicion. I'll investigate today and publish a patch!
is there something else i could try in the interim? i'm just muting the music on 'game pause' now but obvs it keeps playing. that's not the worst, but ..
oh, that's a thing i've been meaning to fix, too—my bevy app on macos requests access to the microphone if i have bevy_seedling enabled, even if i don't seem to be requesting audio input. maybe this is a cpal thing? or is there some default i'm missing?
I do believe it’s a longstanding cpal issue, yes.
bah
oh I see the isse
Make sure you’re using the events from the right entity!
well, i already have an issue on my list for "make gilrs able to submit rumble to controllers on macos"
The volume node and sample player mode have their own AudioEvents components
ohhhh they're separate entities
i have been getting turned around a few times over in terms of when components are put on the same entity or not. i know it's something about filter chaining
but sometimes you can put a VolumeNode right on an entity
Ah, to be clear, nodes (like VolumeNode and the special SamplePlayer) can never coexist on the same entity.
Though this isn’t strictly enforced by seedling.
what about e.g.
commands.spawn((SoundEffectsBus, VolumeNode::default()));
Mhm! The SoundEffectsBus component is just a node label — it labels the VolumeNode in this case.
But, for example, putting a VolumeNode and SpatialBasicNode in the same entity will cause them to conflict.
ok. you might recognize this from the default routing examples. i see things get connected to the SoundEffectsBus label, so i assume that label is strictly being resolved to the entity. and then it itself isn't connected to anything, so i assume it's being implicitly connected to the MainBus
Ya
ok cool
still been thinking about making the generic effect split send thing mentioned the other day
need to focus on other parts of the project at the moment. just keep getting reminded that the 300Hz low-pass on the music sounds ok just probably more muffled than i want
changing the frequency doesn't have the effect that i want.. it feels like what i need is that low-pass filter to be only partially 'wet'
t @ionic sedge ok well here's the full system, just for context https://gist.github.com/habnabit/8ba3104314798eb0f25ea2ab5acbc48d
because of the get_effect_mut part taking extra queries, i was trying to consolidate some of the logic. i can try to fix this up myself; i just wondered if there would be some clearer idiom than what i can eke out
that's re: the pause/fade thing
Started work on updating the convolution node. Though I realized I have to revert some of the changes I made 🥲 . I'll finish that tomorrow. https://github.com/BillyDM/Firewheel/pull/123
Ok, the convolution node is done!
awesome!
oh i can finally provide some feedback on some of the prs today if you'd like
Oh wait, did you have feedback on the convolution node? (No pun intended)
oh no probably not hehe
Ok cool, because I already went ahead and merged it.
Oh yeah, need to add the "noise gate" option to graph inputs.
Oh, actually I'll wait for the audioadapter crate to update since there would be merge conflicts. The author said they will update it soon.
One thing I'd like to note is that the PR seems inverted from what I'd want from it. That is, the processor takes types that implement that trait, but I was maybe hoping we could make the processor itself implement it.
I don't know if that's possible or makes sense, so I'll take a deeper look and see if what I'm saying makes any sense. Sorry for being so vague!
Yeah, I'm not sure what you are getting at.
Oh yeah, I need to add the test to enforce the clap threading model. I'll do that next.
Oh okay it's fine actually. What I want is to basically support something like this:
struct TypeErasedProcessor(Box<dyn FnMut(&dyn Adapter, &mut dyn AdapterMut)>);
because then, with our platform abstraction in the ECS, we could slot anything into that position. Charlotte would appreciate this, as she sometimes has processing needs that would not call for Firewheel's graph. She's mentioned that she wants this many times.
And the way you've done it absolutely supports that.
Good thing I added a test for the CLAP drop ordering, turns out I missed a step in FirewheelContext's drop method that made it fail the test. 😅
@ionic sedge I've started adding bypass support for nodes! Let me know what you think of the API! (It required a breaking change in the node's process method). https://github.com/BillyDM/Firewheel/pull/124
Actually, I just thought of a better API. Instead of sending events to the node's process method, the events could be sent to a separate "on_events" method. That way bypassed plugins can still receive events when bypassed without compromising the API of the process method.
As a bonus, it would also help node authors separate their state update logic and their processing logic, instead of having it all in one big function.
I'll work on that tomorrow if I have the time.
ooooohhhh i realized what i want might not be strictly a low pass filter? or does firewheel let you change the eq beyond cutoff frequency? i realized you can usually change q and gain too
parametric eq might give me what i want more readily
The SvfNode gives you a lot of control like this.
You can choose between different filters
or a high shelf
oh nice ok. bc i was looking at https://docs.rs/firewheel/latest/firewheel/dsp/filter/svf/struct.SvfCoeff.html#method.high_shelf
The coefficients for an SVF (state variable filter) model.
so yeah if svf is already exposed then perfect
An SVF (state variable filter) node
You'll have to register it for the channels you want manually, though.
It's not registered by default.
if i’m tweening/animating the gain, is updating the svf parameters ok? or does that mean it has to drop state
i haven’t seen the generalization of svf before
Hm, sorry I don't quite follow. Can you expand on that?
like, right now, you can tween/animate a volume node’s gain
mhm
can i animate an svf node without audio cutting out?
Hm, like animate in what way?
changing the gain by regenerating the svf parameters. keeping everything else the same except gain
this takes gain
Oh yeah it shouldn't be a problem
okie
@ionic sedge As for managing the state of bypassing for nodes, how would you feel/would it be possible to use macro magic to automatically add a bypassed field to each node that derives AudioNode?
Or do you have a better idea on how to manage the bypassing state?
I definitely wouldn't like to do that sort of thing. Could this be a property of the node that the firewheel processor keeps track of maybe?
Yeah, the Firewheel context can have get/set methods for the bypass state.
I just wasn't sure if that fit well with Bevy's ECS design.
actually it would work great for the ECS -- we can just have a marker component that syncs state with the graph directly
should work great
But yeah, sorry if I was a little short here! We definitely want to limit the scope of our macros to the minimal surface area. Generally, the less macros, the better!
Are you still happy with the Diff/Patch macros?
Yep! I think they're actually quite lovely (not to glaze myself haha). They're quite minimal aside from creating the patch enum, but the enum is so convenient and powerful that I'm okay with it.
@ionic sedge Actually, are you able to just use Memo<bool> on your end to keep track of the bypass state?
Because I realized we would have the same state desyncrhonization issue with scheduled events if I tried storing the state in the Firewheel context.
@ionic sedge What do you think about this?
#[derive(Debug, Clone, Copy, Default)]
pub struct Memo<T> {
value: T,
baseline: T,
bypassed: bool,
baseline_bypassed: bool,
}
impl<T: Diff + Clone> Memo<T> {
/// Construct a new [`Memo`].
///
/// This clones the provided value to maintain
/// a baseline for diffing.
pub fn new(value: T, bypassed: bool,) -> Self {
Self {
baseline: value.clone(),
value,
bypassed,
baseline_bypassed: bypassed,
}
}
/// Generate events if the inner value has changed.
///
/// This will also clone the inner value and assign it to the baseline.
/// This may be inneficient if cloning is slow.
pub fn update_memo<E: EventQueue>(&mut self, event_queue: &mut E) {
self.value
.diff(&self.baseline, PathBuilder::default(), event_queue);
self.baseline = self.value.clone();
if self.bypassed != self.baseline_bypassed {
event_queue.push(NodeEventType::SetBypassed(self.bypassed));
self.baseline_bypassed = self.bypassed;
}
}
pub fn bypassed(&self) -> bool {
self.bypassed
}
pub fn set_bypassed(&mut self, bypassed: bool) {
self.bypassed = bypassed;
}
}
impl<T> core::ops::Deref for Memo<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T> core::ops::DerefMut for Memo<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
Hm, and why does a memo need bypassed state?
Because the bypass state needs to be treated like a parameter, because like I said we would otherwise have the same state desynchronization issue with the bypass state like we did with parameters.
Hmm......
It seems worryingly invasive
The idea being that you wrap parameters in this type?
like VolumeNode
Yeah, isn't that what it's for?
well ideallyit would be transparent
You can't query for VolumeNode if it's wrapped in Memo
can we accept bypass desync? what are the downsides
I mean, the downside is that some nodes could be bypassed/unbypassed when they aren't supposed to be.
Just to clarify, your queries and spawning would look like this if i understand correctly:
fn system(volume: Single<&mut Memo<VolumeNode>, With<MainBus>>) {}
commands.spawn(Memo::new(VolumeNode::default());
That is, it would (in my opinion, please interject if you feel differently) really downgrade the ergonomics and understandability of the ECS APIs.
ughh it's tough
Put another way, this would be potentially another manifestation of this issue https://github.com/CorvusPrudens/bevy_seedling/issues/87
Well, different of course (this is due to smoothing I believe), but a similar idea -- when swapping out samples, if users are bypassing some nodes and not others, we need to make sure the bypassing state updates at the same time as the state changes of the parameters.
Another option could be to make bypass events non-scheduled only. Though that would limit the utility of it.
Hmm, another option could be to internally use an Arc<AtomicBool> to let the context keep track of the actual state the processor is in.
Is it a bad idea to making bypassing completely invisible to the processor itself? This would mean there are no performance benefits to it, but I think it removes a lot of these API issues.
That is, the Firewheel processor could crossfade in and out when a node is enabled or disabled. Presumably.
Yeah, that's already what I'm doing.
It already requires no extra work on the node author's side.
Oh doesn't it? I thought the buffers were an Option now
In any case, I'm so sorry -- can you restate the current issue with the API? It's not currently schedule-aware, right?
Actually, this is the better API I was talking about. I've implemented it, but currently there is no way to persist the bypass state reliably.
pub trait AudioNodeProcessor: 'static + Send {
/// Called when there are new events for this node to process.
///
/// Unless this node is bypassed, then [`AudioNodeProcessor::process`] will be
/// called immediately after.
fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, extra: &mut ProcExtra) {
let _ = info;
let _ = events;
let _ = extra;
}
/// Called when the node has been fully bypassed/unbypassed.
fn bypassed(&mut self, bypassed: bool) {
let _ = bypassed;
}
fn process(
&mut self,
info: &ProcInfo,
buffers: ProcBuffers,
extra: &mut ProcExtra,
) -> ProcessStatus {
let _ = info;
let _ = buffers;
let _ = extra;
ProcessStatus::Bypass
}
fn stream_stopped(&mut self, context: &mut ProcStreamCtx) {
let _ = context;
}
fn new_stream(&mut self, stream_info: &StreamInfo, context: &mut ProcStreamCtx) {
let _ = stream_info;
let _ = context;
}
}
Oh okay thank you
Can we store it in the (audio thread) Firewheel processor based on scheduled messages?
Sorry, I hope I'm not being dense here.
@ionic sedge And this is what I was talking about with the "macro magic" thing:
/// Using this struct allows us to create a custom Diff/Patch that uses
/// a special "bypass" parameter event that the Firewheel processor can
/// detect and intercept.
pub struct Bypassed(pub bool);
impl Diff for Bypassed {
// ...
}
pub struct VolumeNode {
pub gain: Volume,
// Automatically added by the macro.
pub bypassed: Bypassed,
}
impl AudioNode for VolumeNode {
/// Can be used to get the bypass state of a type-erased node (useful
/// for pools).
///
/// This also can be auto-generated by the macro.
fn is_bypassed(&self) -> Bypassed {
self.bypassed
}
}
It would actually be a pretty simple macro.
Yep! That part I followed. It
oop
It's not so much the simplicity of the macro that I'm worried about, but the adding of fields to a struct. Not only is that potentially a touch confusing to new users/contributors, it also requires an attribute macro -- it can't be done in the derive position.
I absolutely understand the simplicity of the implementation on your side within Firewheel though. That is very attractive.
Let me know if I'm off base @viscid plank, I'm sure you have opinions about this sort of thing.
@ionic sedge And of course another option is just to have bevy seedling keep track of the bypass state for each node. You treat it like any other parameter, and if the bypass state has changed (or if scheduled events have been cancelled), you can use this method added to ContextQueue:
impl<'a> ContextQueue<'a> {
pub fn time(&self) -> Option<EventInstant> {
self.time
}
/// Set the bypassed state of this node.
pub fn set_bypassed(&mut self, bypassed: bool) {
self.push(NodeEventType::SetBypassed(bypassed));
}
}
I haven't dug deep into this, but I share your taste there about macros 🙂
oh yeah sorry, I realized there was a bit of an implicit ask for more context gathering
but thank you!
Yeah, if you want a real decision from me I'm going to ask you for a quick design doc :p
Okay now I'm starting to get excited!
oaky no worries 🤭
That is, this is something that we could trivially do I think.
Yeah, that would actually be even easier to implement on my end.
Ok cool. I'll just do that then!
Okay so just to get on the same page -- this style of API wouldn't require changes to the processor impls, but can achieve scheduled bypassing?
I suppose its essentially recreating the logic of Memo, just in a more manual way.
Correct! (Well aside from events now being separate from process, that was needed to get bypassing to work.)
hmmm i see, do you feel like splitting them up could be annoying or should it be okay?
i think we currently have a few variables on the stack gathered from the event draining in nodes, right?
frequently
Actually, I think it would lead to cleaner code, since it separates the state update logic from the process logic.
oaky
Correct, but I already fixed those in the PR I'm working on.
mm yes yes this seems like a good approach all around
I guess when I was experimenting with creating a custom GUI library in Rust a while ago, I used macros that automatically inserted fields very extensively to achieve a sort "inheritence" scheme. But yeah, that's a much different coding style than bevy.
Yeah if it was common in Bevy, I'd be all for it! But I suspect cart would feel it's out of place and it could become a sticking point.
@ionic sedge Ok, the node bypassing support is more or less done! https://github.com/BillyDM/Firewheel/pull/124
It ended up being a bit more work than I thought, but I think it's worth it. I need bypassing support in my DAW engine anyway.
Also, while editing all the nodes, I noticed that the freeverb node doesn't have a "wet/dry mix" parameter. I guess that makes sense if we only want to support using reverbs as send effects. I suppose if that's what we want to go for, then I could remove some complexity from the convolution node by removing the "wet/dry mix" parameter.
What do you think?
It's quite a hefty boi
oh boy haha
yeah since we can do wet/dry in the graph itself, i feel like we should take a more holistic approach in that direction
rather than offering it for each node that we decide ahead of time should have it built in
im sure i could come up with a nice API for it in seedling
that would be nice
(i wanted wet dry when i used reverb on my jam game :))
dw i'll come up with something cute
whats the perf consideration of doing wet/dry in the graph vs in the node?
Depends on the graph overhead I suppose, but Firewheel's volume nodes are quite efficient.
okay
Though you could get away with a single additional node.
Ya Firewheel's volume nodes bypass themselves in each case.
sweet
then we can make nodes not have dry/wet built in
and have seedling "extract" to multiple nodes
for dry/wet support
Oh the refactor will allow me to benchmark this pretty easily to confirm.
im really excited to upstream all of this lmao
we should have a tracking doc of "are we upstream yet" in #1236113088793677888
oh yes
theres a handful of things floating around that still need doing
Oh wait, the bypass declicking isn't actually declicking. Guess I need to do some more debugging. 🥲
alas
Ok, fixed it!
What's up with the apple?
tastes good
it compiles faster
it must grab people's attention haha, a few people ahve pointed it out
me and @astral gust are onto something
Hmm, I wonder why a bright red object would grab people's attention...
🍎
how did you even see that it's supposed to be silent
oh
i thought we had something special
we do, that's why i wanted to come clean
oh, that's in firewheel_nodes, not bevy_seedling. uhh does that mean i have to impl AudioNode myself?
no, you can just register it!
oh ok
app.register_node::<SvfNode<2>>();
ok cool
yeop that was all i needed. even worked right away with schedule_tween
@static quest @ionic sedge did you get anywhere with the “sound sometimes plays quietly” issue (#87)? If it’s the same as the bevy_seedling issue it should still be tracked, but if it’s different maybe I need to create an issue so it doesn’t get lost in here.
I haven't come up with any particular solution myself yet. I'd have to tinker with it a bit, which I really should get to.
Right, the problem turned out to be that seedling should be queuing events as "immediate" by default instead of "scheduled".
Though I suppose another way it could be solved is to add a parameter to the Sampler node to control whether or not samples should be "played in the past".
@ionic sedge How would you feel about adding a boolean parameter to the sampler node that switches between "playing the sample in the past" and "playing the sample as soon as the event arrives"?
Hm, If they’re scheduled in the past, wouldn’t it make sense to always play them in the past?
what does playing in the past mean?
like is that just skipping ahead in a sample? or
But we discovered that's what's causing the "samples are sometimes played quiet issue". The declicker is engaged when playing a sample in the past, which causes the transient of the sound to be lost.
That might just be something that past players have to deal with
. I’m working on removing the scheduling-all-events-by-default currently, so it shouldn’t be difficult for people to avoid it going forward, at least I think.
For seedling anyway
Basically, I think we should get to the point in seedling where a scheduled event is very intentional, and thus there’s no need to carve out a special case for scheduled sample events. Does that make sense?
Oh ok, yeah, that's a better solution.
It worries me a bit, but I'm not sure there's a perfect solution to achieve everything I want either way.
0.19 bump commit: https://github.com/CorvusPrudens/bevy_seedling/pull/106
ohhh i was wondering because i have been experiencing this. but i didn't think i was playing samples in the past ..
can i turn off the declicker to test?
I should have something to look at in a couple hours if you don’t wanna mess with it. We’ll see though, I might have to finish it tomorrow.
Ah, @frail flax, you should be able to disable timestamping globally right now with the ScheduleDiffing resource. (Just set it to false.)
what exactly will that do for me?
Prevent the declicker from activating.
It disables all (normal diffing) scheduling though, so if you have some precisely timed sequences it might throw things off.
it's all pretty rough
I'll set it up so that individual entities can opt into or out of scheduling.
so, what, expect it'll be there during Startup?
or insert it myself?
You can mutate it or (re)insert it after the seedling plugin is added.
Oh actually I use init_resource so it doesn't matter when you insert it.
that particular sound is still quiet, so i think it might be unrelated
Do you know which version you're on? (Just double checking!)
bevy_seedling v0.7.1
Okay that's good. That is -- if it were just the declicker from playing in the past, disabling the scheduling should have fixed it.
ok yes i wasn't expecting this sound was being played with low volume, but when i turn it up, it's much more audible
i wonder if this is expecting a different loudness curve than linear
Actually, it could have more to do with just the fact that the transient is being cut off, not that it is ramping up.
It depends on what kind of sound. If it's a short sound with a loud transient (like a impact sound), then cutting off the transient will make it very quiet.
Does seedling have a good way to "fast forward" the playback to render out audio? I want to prebake some audio but still use the ECS to define the graph while developing
you could make a little backend to do this!
(it would be easier to manage it on master for sure, but im not sure when that'll end up published)
Ah nice! I don't mind using upstream for my usecase, so long as it doesn't require updating bevy itself (which it doesn't seem to)
Is there a way to switch backends at runtime? I don't mind having either a separate binary or a separate mode that the main binary starts in, so if not then that's ok
I don't think multiplexing backends would work here though (in case that's possible). If only one of the backends can "fast-forward" then it'll still run at regular speed in order to support the non-fast-forwarding backend(s)
hmm is startup runtimey enough?
Aha it'll do
Oh you mean for this particular feature.
Hmmmm
you might be able to write a backend based on the current cpal one that can change its timescale!
and like resamples the output accordingly
I think it'd be better just to have an entirely separate app just for baking with whatever the minimal set of plugins is, then I can spin it up in a separate thread. Basically the same approach as having a separate binary but with the possibility to transfer entities across
Like, do whatever runtime authoring I want to do and then start a baking task which just creates a new App, does whatever the process is to copy the nodes etc to be baked across, and then manually drives the app until the baking is done
Then I can do whatever custom entity handling I want too
Out of interest, why is this easier on master?
The backend previously was coupled to a somewhat heavy abstraction that made it difficult to manage less conventional setups. Now you can basically do whatever you want.
Oh, and that reminds me -- you can certainly switch the backend while a program is running, even after startup. In theory anyway, we might need to improve the API a bit in seedling to make this work properly.
But it's something that ought to work. The capability is there in Firewheel.
-# i think
Cool! I think having a separate app is a better choice anyway since it means I can do the rendering in the background, but I'll keep that in mind!
Yeah, it should be possible. It might be easier if you can keep the sample rate the same though, that way you wouldn't have to reload sample assets.
Most modern systems support both 44100 and 48000. It's mainly just older systems and maybe consoles where you might run into issues.
Does anyone get this repeated error on linux?
ERROR bevy_seedling::platform::cpal: unexpected audio backend error: A backend-specific error has occurred: alsa::poll() returned POLLERR
My system uses Pipewire and I sometimes get this error with alsa when developing. Audio doesn't play when this happens. I can try rerunning several times and it would eventually go away, but this happens more often than I'd like. Until the CPAL version with Pipewire host gets released, does anyone have any ideas whats with this error? I think I never got this when I tried out bevy_kira_audio (or at least it doesnt log error?).
Hm, interesting
Can you make an issue for this? I’ve only seen people report issues on startup.
hmm what do you mean on startup?
my error gets printed every update i think
As in a few messages when the program starts, but otherwise no complaints.
I’m not familiar with alsa — I wonder if this is even an error 
ye. just rechecked to be sure and i have my error handler to panic in dev so the stream probably started successfully
i'll make an issue in a bit then
Double check the existing one too! It may cover parts of this already! Let me link it
but yeah. im not familiar with alsa either so I was gonna wait for the pipewire host. cuz the pipewire-alsa bridge seems quite finicky. i met another error where the cpal alsa stream fails to start (causing a panic) during a short period after I change the device output volume with pavucontrol (it's like the system audio config needs time to settle or sth). i was going to file an issue as well, but apparently it's reproducible in all kira and cpal as well (just that seedling panic due to the fallible start_stream system)
that doesnt happen with the new cpal pipewire host
so maybe this is a same thing. but im not sure since i havent been able to reliably reproduce it
Oh I think I know what's going on. The new version of CPAL sends warnings to the error callback in addition to actual stream errors. I wish they would document which variants are actually errors and which are just warnings.
@ionic sedge Ok, this commit should fix the issue! https://github.com/BillyDM/Firewheel/commit/f1d5c42daefa1a696cba7cd9c6d841cda3006e74
Hm, so this filters out the warnings?
No, it changes the way you respond to errors. Instead of telling users to stop the stream (or panic) as a result of stream errors, it tells the user to just log them, and to use the new more reliable is_running() method to actually check if the stream had stopped or not.
Ah, the current system on (seedling) main looks like this:
fn poll_stream(mut stream: ResMut<CpalStream>, mut commands: Commands) -> Result {
let stream = stream.get();
if let Err(e) = stream.poll_status() {
match e {
StreamError::StreamInvalidated | StreamError::DeviceNotAvailable => {
warn!("Audio stream stopped: {e:?}");
commands.trigger(RestartAudioStream);
}
StreamError::BufferUnderrun => {
warn!("audio stream encountered underrun");
}
StreamError::BackendSpecific { err } => {
error!("unexpected audio backend error: {err}");
}
}
}
Ok(())
}
which i think basically does that, right?
now we just have a way to check in the BackendSpecific error if it's resulted in a stream stoppage, right?
Correct. Although note it is not gauranteed that the stream will have stopped before an error is returned. There may be a case where an error is returned, and the stream is still in the process of stopping. This unfortunately seems to be a limitation of CPAL's API.
I suppose we could also trial and error every BackendSpecificError to see if the stream actually stops as a result, but that sounds like a pain to test.
ya that sounds annoying
And the BufferUnderrun variant is definitely not an error, just a warning.
I guess the only issue is if cpal for some reason doesn't drop the the data callback as a result of an error, but that doesn't seem likely.
(Essentially I'm just setting an atomic flag on the data callback's Drop method.)
Man, if only cross platform audio was easy and reliable.
Then a lot of people would be out of a job lmao, it feels like that’s what 50% of the work in digital audio goes into sometimes
i would cosign their termination letters if it meant platforms could agree on sane standards :3
At the same time, I wish cross platform graphics was also easy and reliable.
Oh yeah, and windowing.
(Shout out to the winit team for all their hard work!)
And the wgpu team!
:3
Oh yeah, and the cpal team too!
so, i've been getting deadlocks on app start, randomly, only after/when i have the bevy_seedling plugin enabled. i don't know if this is because of interactions with bevy_audio. i haven't been able to disable bevy_audio on the feature level, but .disable::<bevy_audio::AudioPlugin>() doesn't seem to be enough
is this a known issue at all? i'm trying to dig into a backtrace and i'm starting from nothing
No, I haven't heard of this related to audio specifically.
hm ok
But you've had trouble disabling it via features? Would you like some help there, or is it something that's not easy with your project?
the one time i tried to disable it, i tried to enable the other default features, but some parts of my rendering code weren't running
so, i'd need to figure out what's going on there, but i can try
Did you follow the recommendation in the seedling docs?
[dependencies]
bevy_seedling = "0.7.2"
bevy = { version = "0.18.0", default-features = false, features = [
# 2d
"2d_bevy_render",
"default_app",
"picking",
"scene",
# 3d
"3d_bevy_render",
# ui
"ui_api",
"ui_bevy_render",
# default_platform
"android-game-activity",
"bevy_gilrs",
"bevy_winit",
"default_font",
"multi_threaded",
"std",
"sysinfo_plugin",
"wayland",
"webgl2",
"x11",
] }
this should cover everything!
yeah i did. i'll try again now
Ah, okay. It could be some papercut then.
lol dang it i just realized. i think it's because i didn't enable 3d_bevy_render. i'm only doing 2d, but ..
that did, in fact, fix my rendering issues. sob
.. but it's still hanging
there's background audio playing, but the mainloop is wedged
the title/loading music is playing
Hm, does it also occur when you don't add SeedlingPlugin at all?
let's see!
it occurs at random, so it's hard to "test", but
well, i have some ideas about what it might be actually. less convinced it's an audio thing
i’m pretty sure i resolved it and it wasn’t an audio issue. it was just exacerbated by the overhead from audio
I left my game running overnight and saw these logs spammed (100s):
...
2026-04-09T01:05:21.173991Z ERROR bevy_seedling::node: graph error: MsgChannelFull
...
And then after all of those about 10 of these in a row:
2026-04-09T01:17:20.634270Z ERROR firewheel_graph::context: Firewheel scheduled event buffer is full! Please increase capacity to avoid audio glitches.
Is there anything that seedling or firewheel should do here? Note: I only realized these logs existed after I exited the game, so I don't actually know what the user impact was here, if any.
Hm, I wonder if very occasionally some messages get stuck or something
Or maybe you could just increase the capacity a bit. We're soon reducing the number of messages that'll be scheduled at least, so the symtoms should be much less noticeable.
Worst case scenario the allocation causes a stutter. Though the majority of the time you'll likely get away without any hitches.
The event buffers have a maximum size that by default can grow (in an overflow situation). It requires an audio thread allocation. You can set the default size in the FirewheelConfig struct.
I can look into the internals/knobs-to-tune myself and see if there's anything I could be doing but naively is there any way for this simply to not even be a userland problem to think about? If my game is essentially just sitting on idle and it has sound effects that are spawning and despawning and has music samples that are spawning and despawning, does this indicate a "leak" somewhere, or a lack of reusing/recycling resources? I guess what I'm trying to say is that unless I happen to have a silly leak of my own which manifests after hours, shouldn't seedling/firewheel be able to reuse/cycle its own resources? Again, this is just a naive view, I haven't made an effort to understand the internals here.
Oh to be honest, I doubt there's a leak. What's most likely is that we're a bit conservative with our event storage size.
Although, there may be a degenerate case where events are getting absolutely spammed. I wouldn't expect to see that message a ton...
The messages would imply that it grew a ton, which shouldn't happen.
Even if sample players are lying around, unless they're actively connected to a Sampler entity via that relationship, they shouldn't contribute to node events.
Wonder if there's a relatively simple stress test example you can add the the repo and then just leave it running to see what happens.
Yes that's probably a good idea.
Hmm, the first error message is indicating that far too many update messages are in the queue. Unless bevy seedling is spamming the context's update method, the only way for that to happen is if the audio thread itself is stalled for some reason and is not draining the message buffer.
It's called once per frame, however fast that may be.
Hmm, I guess if you were running at a very high framerate it could be a problem.
It would probably be beneficial to add logic to bevy seedling to limit the rate at which parameters are synced and updates methods are called.
Something as simple as "has it been more than 10 milliseconds since the last parameter sync/update?" would do. (Keep in mind the nominal "frame time" of the audio thread is about 23 milliseconds, which is already larger than the nominal 16 milliseconds of a 60fps frame rate.)
Of course we could play around with the exact number to find the best balance of latency.
Oh yeah, I still need to add the profiling capabilities. While I'm at it I could add a "heartbeat" indicator to detect if the audio thread is being stalled for some reason.
@next otter what are your typical frame rates, if you know off rip?
It would be nice if we could reliably estimate the block rate so we could set a reasonable default for the diffing rate.
Unscientifically but also emperically usually around 60 fps on my monitor and around 120fps on my MBP.
Because I left this open all night I could very well have been streaming while it was open which I’d guess would have dropped the frame rate at various times.
Hm, I imagine there could have been moments where the frame rate shot way up.
A diff limiting system would mitigate this.
Obviously we couldhave a very rough system, maybe controlled via a resource, that sets the minimum diffing period. I'd prefer if we didn't force people to mess with it unless absolutely necessary, though.
@static quest submitted a PR for firewheel-cpal. was crashing when using an asio interface. RME MADIface USB (tested here) delivers ASIOSTInt32LSB → SampleFormat::I32, so firewheel-cpal's f32-only path was failing
Did you happen to test this on bevy_seedling's master branch? Just curious if anything there improved this situation or if this fix is still required.
I haven't yet, I was using seedling 0.7.2 when I had this problem, is that something that was worked on? will try it
The latest code is much more flexible, though it may not have leveraged it in a way that solves this problem yet.
app.add_plugins(SeedlingPlugin { config: FirewheelConfig { num_graph_outputs: ChannelCount::new(64).unwrap(), ..FirewheelConfig::default() }, stream_config: CpalConfig { output: CpalOutputConfig { host: Some(HostId::Asio), device_id: Some(DeviceId(HostId::Asio, "JackRouter".to_string())), ..Default::default() }, input: None, }, ..SeedlingPlugin::default() });
this is how im configuring my sound card
but substituting jackrouter for rme madiface usb sorry
oh nice! on master it would be
app
.add_plugins(SeedlingPlugins)
.insert_resource(AudioContextConfig(FirewheelConfig {
num_graph_outputs: ChannelCount::new(64).unwrap(),
..Default::default()
}))
.insert_resource(AudioStreamConfig(CpalConfig {
output: CpalOutputConfig {
host: Some(HostId::Asio),
device_id: Some(DeviceId(HostId::Asio, "JackRouter".to_string())),
..Default::default()
},
..Default::default()
}));
if you're interested in testing c:
Ah, I was under the impression that CPAL automatically converted the sample format for you, my bad. Could you add the same treatment to the input stream too please?
(looks like the new code still needs this treatment hehe)
Yes! hoping to have the time in the coming days and will test
(I was posting build issues over in Better Audio because Discord hid this channel :-p )
So I wrote my pool update fn to only update on the next frame
But I can't recall why I did that
here it is:
mut pooles: Query<(&mut VolumeNode, &SamplerPool<AudioPool>)>,
player_preferences: Res<PlayerPreferences>,
mut should_update_next_frame: Local<bool>,
) {
if *should_update_next_frame {
*should_update_next_frame = false;
for (option_tag, sound_effects_pool) in [
(OptionTag::MusicVolume, false),
(OptionTag::SfxVolume, true),
] {
let value = player_preferences.get_or_default(&option_tag);
for (mut volume_node, sampler_pool) in &mut pooles {
if sound_effects_pool ^ sampler_pool.id().of_sound_effects() {
continue;
}
volume_node.volume = Volume::Linear(value);
print_info(
format!(
"Setting {:?} audio pool volume to: {:?}",
sampler_pool.id(),
value
),
vec![LogCategory::Audio],
);
}
}
} else {
*should_update_next_frame = player_preferences.is_changed();
}
}
Anything about the way seedling works I might have forgotten? (Going to put a comment there, which is something I very rarely do, because I have absolutely no idea why I did that)
It shouldn't matter for seedling itself. All updates happen in Last, so unless you schedule something in there, it shouldn't miss it.
Could this have been for your own ordering needs?
Probably, yea haha
But why 🥹
(Thanks for the help, time for me to think and see)
It was there because the pools take a frame to init
Hi! I'm curious if there is any work being done right now to making multi listener spatial audio work properly? I've been prototyping a doppler and propagation delay effect and they both are listener dependent (so are itd and spatialbasic).
Everything seems to be calculated off the nearest listener rn
If anyone is working on this or has ideas id love to know!
Yep! The docs mention this: https://docs.rs/bevy_seedling/latest/bevy_seedling/spatial/struct.SpatialListener2D.html
What are your constraints? How many listeners do you expect?
Also, what should the output look like? Two speakers still? Or multiple different devices?
That's where "properly" comes into question in particular!
My game doesn't currently need multiple listeners 😅 (though the use cases i imagine are split screen multiplayer and something like spectating a esports match (where there are many cameras and listeners))
ideally both would be possible? im also imagining recording a musical performance in a virtual space (many instrument sources and 'mics' recording different perspectives)
but thats a good question im not sure what would be best
There's not really much to be done if you expect to still produce a stereo (or otherwise normal) output. Consider what happens if you process the full chain for two listeners: when one is close and the other is far, the closer listener would dominate the output. When both are far, the sound is quiet. When both are close, the sound is loud, with the only different being panning.
In other words, this is almost indistinguishable from simply choosing the closest listener.
As far as I know, this approach is standard. It seems like UE5 and FMOD also simply choose the closest listener.
It certainly gets interesting in a true multi-listener setup. But I suspect that's better left to a custom approach, possibly where sampler pools are completely ignored in favor of bespoke solutions.
And by "true multi-listener," I mean where the graph has more than one destination. For example, a bunch of dedicated listener sinks, perhaps for recording from different positions as you mentioned.
But I'll note that when spectating, there's still one graph output.
I could imagine something like a virtual performance that you want to record in realtime being an interesting use case.
ok that makes sense, maybe ill not put too much thought into it now and just work on getting doppler and propagation delay working nicely and make a pr
the way it works for each now is as a smaple effect like this
let sound = commands
.spawn((
LevelEntity,
SamplePlayer::new(props.handle.clone()).with_volume(Volume::Linear(props.volume)),
sample_effects![
SpatialBasicNode::default(),
ItdNode::default(),
DopplerNode::default(),
PropagationDelayNode::default(),
],
Transform::from_translation(relative_position),
PlaybackSettings::default(),
))
.id();
do you think this is roughly the right shape?
Oh I have a simple doppler implementation if you'd like to look by the way. It makes no concessions for propagation, but may be interesting nonetheless: https://github.com/Some-Bevy-Jam-7-Team/jam/blob/main/src/audio/doppler.rs
The above is quite simple -- rather than another node, it simply modifies the sample player's playback speed.
You could take a more physically-inspired approach and combine both doppler and propagation into one node. Imagine a ring buffer to which you push samples. Assuming some maximum distance between source and sink, you could simulate both doppler and propagation delay by using the distance as an index into the buffer.
Cute, but expensive. Much, much more efficient to simply delay (initial) playback and modify the pitch of the sample player directly.
agreed
what do you think the user surface should be for setting these effects?
also one other thing is that itd, doppler, propagation delay (maybe also spatial muffling?) all rely on speed of sound
so some resource holding a speed of sound value that each of them pull from makes sense to me (you would want to make the speed of sound higher if a player was in water for instance)
since doing it in a sample effect is more expensive
should it just be part of spatial basic? (but then theres hrtf to think about)
not super familiar with the best practices with firewheel/seedling
I think the code I linked demonstrates my thinking here -- since it controls the PlaybackSettings component directly, they should probably be companion components.
Yes we can provide a speed of sounds resource!
Thus, they wouldn't appear as effects withinsample_effects!.
And I don't think they should be fields on PlaybackSettings -- they're much less frequently needed.
this makes sense
ill work on making that happen and make a pr soonish!
thanks for your help! 
Absolutely!
I have a question. A while ago I updated and reset my feature flags for bevy_seedling and now occasionally see
Firewheel node profiling buffer is full! Please increase FirewheelConfig::initial_node_capacity to avoid audio glitches..
This used to be a field in FirewheelPlugin (before the s) directly. I had to dig through git to find how to actually set this up now:
.insert_resource(AudioContextConfig(FirewheelConfig {
initial_node_capacity: 1024,
..default()
}))
I'd recommend SeedlingPlugins, or FirewheelConfig, or the warning message point a little closer to the solution 🙂
Hm, that may be something that needs attention regardless 
Not really. I'm (ab)using things by spawning e.g. 1000 cubes with embedded custom midi synths 😆
oh i see
Sorry, I'm used to seeing that show up for messages.
The docs haven't been fully upgraded for the resource approach yet. That should help a little bit!
if this also included itd maybe it'd be worth doing something like this? since itd, propagation delay and doppler are all essentially different descriptions of the effects of wave propagation/measurement?
pr for doppler effect and propagation delay, lmk if theres any changes needed!
Sorry if this has been posted here already, I searched but didn’t see anything. Maybe worth having a wrapper impl to use a truce effect as a firewheel node? https://truce.audio
(Also just potentially interesting for the people here)
They also have a rack-like plugin host library although it seems relatively new so I’m not sure how it compares to the original. Might be useful for you BillyDM https://github.com/truce-audio/truce-rack
Btw since I’m on the subject of writing a VST, does anyone know if there’s an efficient Karplus-Strong and/or waveguide-based string synthesis crate in Rust? I found a couple of crates that claim to implement it but don’t, and one crate that has an implementation but it was, frankly, barely a proof-of-concept (unoptimised, does a bunch of allocation in the audio thread, kinda looks like AI code). If it doesn’t exist then I’ll write a crate myself, maybe it’ll be useful for someone. In case it’s interesting, it’s for transient synthesis for a kick/tom VST.
Hmm, interesting. My AI slop senses are tingling a bit though. Hard to tell with this one.
Hmm, the age of the repository and the sheer number of crates isn't the best sign. https://github.com/truce-audio/truce/tree/main/crates
Oh interesting that you say that, I was kinda suspicious of it being slop because it’s only been released recently so I dug into everything I had time to look at and it seems totally legit. Everything looks carefully-designed and well-documented, AI tends to be pretty good at implementation but bad at API design and has a signature way of writing docs and I didn’t see any of that. The website looks like a standard template too, rather than an AI design
Hmmm, remains to be seen then
I was gonna build my VST with nih-plug but I’ll give this a go and report back
Oh yeah, we're actually working on an updated community for of NIH-plug. https://codeberg.org/BillyDM/nih-plug
Have they just made this public? I feel like they would have at least said something in the Rust Audio Discord server by now.
Hmm, yeah. They have a very odd an inefficient implementation of a ring buffer. https://github.com/truce-audio/truce/blob/main/crates/truce-dsp/src/audio_tap.rs
Ok yeah, people in the Rust Audio Discord are saying it's definitely slop. (I also found em-dashes in the doc comments, lol. https://github.com/truce-audio/truce/blob/d7a6e7dfa21646675ae514725d688e9776865777/crates/truce-driver/src/lib.rs#L211)
Also, lol, they're using their own fork of baseview with a very LLM explanation of the patch. (The patch is the incorrect way to do fix this by the way.) https://github.com/truce-audio/baseview
Damn, I got got 😅 And here I was thinking I was alright at detecting slop
Yeah, it wasn't immediately obvious (it's becoming harder to tell 💀 ). I had to do some more digging than usual.
I mean honestly though I do like the API and the general design so it could serve as decent inspiration if nothing else. The way they handle in-out buffers and transitioning from an auto-gui to a more complete UI is pretty clever conceptually
NIH-plug also has "generic slider widgets" that can be used for a quick "generic" gui.
Oh nice, I’ll give it a look
(Though I still need to add the generic slider back for the Iced backend.)
And we're actually currently discussing a new name for the fork to avoid confusion, so prepare for the name of the repo changing!
If I know open source, bikeshedding the new name is going to take longer than writing the library
I think we've mostly landed on "nice plug".
qDot is gonna love that
fix for toggling effect bypass not working consistently + example with 3d spatial sound where you can the toggle effects
You know, it’s been a long time since I’ve looked into nih-plug so I don’t know how much of this is only in your fork, but it looks like Truce is way more similar in design than I thought. I think if I was more up-to-date I wouldn’t have been so excited about it 😅 Looks great, hopefully I’ll get to start on my plugin this week
Well that's because Truce was also a fork of nih-plug. Still, I wouldn't use it, one user in the Rust Audio Discord said that the AU code was one of the most atrocious pieces of code they have ever seen. 😅
Oh I’m not saying that in favour of Truce 😅 I’m saying that it stole even more than I expected and there’s even less reason to pay it any mind
Last time I looked at nih-plug the ergonomics were pretty rough and I thought that truce had improved on that if nothing else
Hmm, what do you mean by that? NIH-plug has one of if not the best ergonomics of any cross-platform plugin development framework. (Well, maybe aside from GUI.)
Yeah, I just last looked at it a long, long time ago and was under the mistaken belief that it basically hadn’t been updated since then. It’s a pebkac bug 😅 My message earlier was saying that nih-plug actually does have good ergonomics after all, even though I thought it didn’t when I was looking at truce
Does that make sense? 🙈
Yeah for sure