#bevy_seedling
1 messages Ā· Page 6 of 1
Aha! It's because cargo test itself always has the standard library enabled, even if your crate is no_std. Running cargo build instead produces no warnings.
I actually found quite a few no_std issues in Firewheel, so I went ahead and fixed them. https://github.com/BillyDM/Firewheel/commit/48aa13f108a7edab48f57def337eeabb2474e868
I also found that the fft-convolver dependency used by the convolver node is not no_std compatible. We should probably ask them to add support for that at some point (or create a PR ourselves).
And TIL that f32::round() is not in the core library (it also requires num-traits).
Just waiting for the CI to finish and I'll publish version 0.9.1
yeah theres a couple things that would be nice to change in that repo. the author neodsp is on in RA discord, i can always ask if they can take a look (or if theyd consider a transfer to the RA org as my PR on the repo is 1M old)
oh huh didnt know that
Oh, it's because the rustfft crate isn't no_std compatible. That would probably take quite a bit more work to make no_std compatible.
ahh
It also appears that fft-convolver doesn't expose an option to enable the experimental wasm_simd stuff. That would be pretty important to get good performance on wasm (FFT is very SIMD-heavy). https://github.com/ejmahler/RustFFT/blob/4758ab0dd6f256c50ac8987c75c9cb96152dc2ca/Cargo.toml#L33
Ok, Firewheel 0.9.1 is published!
Oh yeah, we need to get the issue with clack sorted out.
@lapis stone What is the status of your echo node? Does it just need the merge conflicts to be resolved, or are you still working on the node itself? https://github.com/BillyDM/Firewheel/pull/84
i still need to fix up some clicking when changing delay times, and last time i checked there was an issue where itll notify stop the first time any param is changed. hoping to have that done this week
I sent an owner invite a while back
might be a bit buried, idk if they expire?
oh, worked now!
I wanted to rename it to use underscores instead of dashes
but that only works when there is one owner, so I had to kick you, sorry :<
then I tried quickly deleting the old crate and pushing the new name to get the underscores, and would you look at that
this is like the ruby takeover
@ionic sedge i may have discovered another issue with Notify (or an issue with my usage). It appears that the first time update_memo is called, Notify<()> is always triggered. It behaves normally after that first message, though. I stuffed some debugs into your freeverb Reset param notify code and found it does the same.
oh huh i should have guessed, but this also goes for ceil and floor. that's rather annoying... those are the only stdlib ops needed for this echo effect. what's the best way forward here - gate behind the std flag, and wait for the core operations to stabilize?
Congrats on the release āØ
I was wondering- currently when my game is paused I just set all audio players speed to 0.0
Would setting the reverb to paused make a difference?
if you want to keep the tail of your reverb after unpausing, and stop it immediately when pausing, then yes. this would be particularly noticeable with longer reverb times. if that doesnt matter, just adjusting the sample players should work.
Yeah not pausing the reverb may give it a subtly unpolished vibe.
Some games do weird stuff with reverb though. The first dark souls just applied it to all sounds, so even menu navigation was reverberating.
(But I would argue that's lacking polish.)
So I'd want to pause both the sound speed and the reverb node
Luckily, this is Bevy, so I can just query for them
Man, I love this engine
Yeah, your only option is to use the num_traits crate behind a std feature flag.
they may take a long time to stabilize š
i saw there was a small crate for floating point stuff without std but bringing in an external crate seems heavyhanded
and kinda unsure how stable it would be across platforms
libm is the standard here (which is what num traits uses in no_std contexts) and i believe it's perfectly stable across platforms
i should probably look into it before asking but - does this mean we can enable that? as opposed to gating num_traits under std - can I just remove the std feature from num_traits? š¤
hm, not sure i follow
Yeah, either method would work.
(But you would need to create a libm features that enables the libm feature in num_traits)
It was literally two lines of code
That's also thanks to your crate being so
Smooth š
okay cool, should be working with no_std now and I think i fixed the clicking by increasing smoother time. the last issue is Notify<()> immediately triggering on first param memo update for some reason. im not super sure whats going on there but ill see if theres anything i can parse from the notify code in firewheel.
Clippy complains about unnecessary parentheses in sample_effects! , but I don't know enough about macros to judge if it's valid or clippy getting confused.
send snippet š
macro_rules! sample_effects {
[$($effect:expr),*$(,)?] => {
<$crate::pool::sample_effects::SampleEffects>::spawn(($($crate::pool::sample_effects::Spawn($effect)),*))
};
}
And the code is just the getting started example on bevy_seedling repo
commands.spawn((
SamplePlayer::new(server.load("my_ambience.wav")).looping(),
sample_effects![LowPassNode { frequency: 500.0 }],
));
And children! didn't do that in 0.16? (I just copied the macro definition basically.)
Anyway we can move the comma inside the paren to force the single-effect case to be a tuple.
Interestingly, it seems to have gone away after I inlined the macro, and then reverted it
restarted my editor and now it's back. Even after inlining it.. odd š¤·
sample_effects should be updated anyway for 0.17 to use the recursive_spawn macro. I doubt people will run into the 12-item tuple limit for audio effects very often, but someone will do it eventually!
adding another effect fixes the warning, so it seems to happen when there's only one effect
Ah yeah, that's what you said above. Nevermind me š
I'll just disable the warning for now, it's no big deal especially since it's just when there's only one effect
I see it has been changed since 0.16.1, maybe it's a recent issue?
the macro in 0.17.2 now looks like this
#[macro_export]
macro_rules! children {
[$($child:expr),*$(,)?] => {
$crate::hierarchy::Children::spawn($crate::recursive_spawn!($($child),*))
};
}
recursive_spawn! .. yeah
Yeah that was to increase the upper limit for spawning, since the previous method just made a single flat tuple (and the relevant traits are only implemented up to 12).
tbh this does kinda feel like clippy being a bit too trigger happy
Is there an easy way to change the pitch of an audio clip?
The speed of a sample is inherently linked to its pitch. A sample played twice as fast will sound an octave higher (i.e. a fair bit higher-pitched).
Hopefully these answer the question! TLDR; set the speed.
Perfect thanks
is there docs for this project yet?
has oneone done a tutorial on spatial sounds?
The docs for the spatial module give some information here https://docs.rs/bevy_seedling/latest/bevy_seedling/spatial/index.html, and the spatial examples should give a bit more info on how to use it in practice https://github.com/CorvusPrudens/bevy_seedling/blob/v0.6.0/examples/spatial_basic.rs.
nice got the spatial basic running here. pretty cool. so that looks like a 2d example. 3d should be good too right?
Yes, for example foxtrot uses 3d spatialization.
If you want to get fancy with it, you could look into bevy_steam_audio as well. It can do a bunch of simulation like occlusion, reflections, and reverb.
is the steam audio using seedling or working with it?
Yeah it's built on top of it.
I'd consider that a bit later though -- the docs on it are still a work in progress.
(But the fact that it exists means you can go pretty far with audio spatialization if you ever need it!)
this is pretty cool stuff.
So i see the GraphConfiguration, looks really cool.
how do i use the Game setting and what setting is used by default?
CraphConfiguration::Game is the default, and you'd supply it to the plugin directly: https://docs.rs/bevy_seedling/latest/bevy_seedling/struct.SeedlingPlugin.html#structfield.graph_config.
ok im stumbling through the spatial. got simple sound effects working easy. nice one.
im trying to turn the sound up and down for a looping engine noise. based on wheather or not there is input from the controller and i keep getting
thread 'Compute Task Pool (0)' (23041318) panicked at /Users/pro/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy_seedling-0.6.0/src/node/events.rs:381:15:
an event timeline should never be empty
Ah, I see how that can sneak in. I believe these lines here are the issue, where if the value is the same before and after applying the change, the resulting timeline will be empty.
I should have time later today to create a patch for this. Are you using the scheduling directly?
ahh hmm im setting the volume directly every frame so it can be eventaully based on the speed. but right now its setting it to the same value over an over.
not using scheduling directly that i know of.
pub fn update_boat_audio(
movement: Single<&Action<Move>>,
boat_audio: Single<&SampleEffects, With<BoatAudio>>,
mut volume_nodes: Query<(&VolumeNode, &mut AudioEvents)>,
) {
let fade_duration = DurationSeconds(0.5);
let input = **movement;
let input_magnitude = input.length();
if let Ok((volume, mut events)) = volume_nodes.get_effect_mut(&boat_audio) {
if input_magnitude > 0.01 {
volume.fade_to(Volume::UNITY_GAIN, fade_duration, &mut events);
} else {
volume.fade_to(Volume::SILENT, fade_duration, &mut events);
}
}
}
I suppose it's possible those fades are the issue as well (they also use the scheduling), although we already have checks for those.
In any case, I'll get this sorted later today. The scheduling is the most recent addition to the crate, so there's a lot of surface area there for issues at the moment.
rad. so the scheduling handles the fades and other actions over time.
i guess i could try setting the volume directly to avoid the scheduler.?
Yeah that should avoid it entirely.
volume.set_linear is what i tried because i wanted to try setting it directly
volume.set_linear(0.0);
but im not sure how to setup the volume node if i use it in that function above i get
cannot borrow `*volume` as mutable, as it is behind a `&` reference
`volume` is a `&` reference, so the data it refers to cannot be borrowed as mutablerustcClick for full compiler diagnostic
systems.rs(587, 16): consider changing this binding's type to be: `&mut bevy_seedling::prelude::VolumeNode`
Oh yeah that's just Rust stuff -- try
mut volume_nodes: Query<&mut VolumeNode>,
ohh right i dont need events if im not doing the fade.
and that way works fine.
ooohh sound is so fun
gonna be dope when we get those fades
volume.set_percent(100.0);
Okay @near dagger feel free to cargo update bevy_seedling -- 0.6.1 should resolve the issue you ran into.
it works. thanks much.
Would it be worthwhile to add a state (bool/enum/?) to PlaybackCompletionEvent as one currently does not know whether the sampler actually completed (as stated in the docs). Or is there another way, in my event handler, to derive/query this information?
I could duplicate the poll_finished system from pool/mod.rs and fire my own event but that seems wasteful (and annoying wrt scheduling)?
Do you mean as opposed to simply interrupted or skipped?
yes, i would like to know when a SamplePlayer reached 100%. For looping you can argue when it should be send, probably not at all, but i'm not particularly interested in looping either way.
Yeah it wonāt be triggered on looping samples unless you stop them.
FWIW i would also be interested in a PlaybackStartedEvent, similarly only fired when first audio is being played (do not care if it later interrupted/skipped/restarted). Currently i fire my own event when i spawn the SamplePlayer. Close enough but probably not very accurate (did not check the seedling code yet on this one). The PlaybackCompletionEvent request comes ~free as the code is already there. I believe PlaybackStartedEvent would be new? Or is there a way to do this already?
My questions boil down to an "audio synchronization" API (events). Or how it can be achieved with the current API (example).
Hm, how accurate does your synchronization need to be?
I do not have a hard requirement at the moment, still early days. I also do not know what the price of high accuracy would be (implementation/performance) wise.
Well, in the ECS you can only get so close. For example, these sorts of events won't necessarily produce sample-accurate synchronization between tracks. (For that, you should schedule playback at exact times.)
But if what you're doing can accept "close enough," then it should work well.
is seedling appropriate for doing microphone input stuff? if so where should i be looking for examples/docs?
i'm currently trying to just use CPAL directly, (by scooting samples around via a crossbeam channel) but as soon as i start trying to empty that channel from bevy-land, all of my (unrelated, system) audio output goes crackly. so i suspect i'm doing something wrong wrt crossing my Ts and dotting my Is audio-wise
(so i figure i ought to turn to a library that knows how not to anger the audio gods)
In a perfect world yes! However, we don't currently have a good, cross-platform solution to handle duplex audio streams. That is, audio input from a device passing through the audio graph and out on another device.
cpal allows you to start an input stream or an output stream, currently, and even with bevy_seedling, you'll end up just creating your own input stream.
I should probably put together a crate to handle this since it's pretty common
.
As for the cracklies, is this running with optimizations?
yeah
if i just read from the channel directly (to eg. draw a bar in my terminal) with no bevy involved, everything's fine
Is the channel fixed capacity?
but if i start trying to read it from a system in the Update schedule, my background music starts getting the cracklies
yep! fixed size of 10. using try_send to just give up if there's no space
i'm currently trying to fill a VecDeque in a Resource so i can e.g. draw a waveform or whatever, but even if i replace that with something that just empties the channel and drops everything i still get the cracklies in my unrelated system audio
happens using jack or alsa as my host
Oh, well it's not blocking in that case. You might consider the fixed_resample crate by the way -- it's used in Firewheel's stream nodes and is pretty much hand-crafted for this use case. That is, assuming you want to do the processing outside of the audio callback.
oh yeah. realtime spsc channel that auto-resamples sounds real nice
Oh, Firewheel already synchronizes a CPAL input to the output with fixed-resample. Do you have that exposed in bevy-seedling @ionic sedge ?
@frosty folio ^
As in the stream node?
Oh, my bad, I thought you were talking about just getting microphone input. Yeah, you would use the stream node for getting samples out of the audio thread.
Though actually, for visualizations it probably makes more sense to use a triple buffer. I should make a triple buffer node for that.
I was. But I'm not sure it makes sense to expose fixed-resample since it's just a transitive dependency. Depending on it directly as a user seems fine 
Ah, you're saying you manage a duplex stream here?
You can configure the inputs and graph I/O in bevy_seedling, so unless it's gated by a feature that I missed, it should indeed be possible to set up.
Yes
Hey BillyDM, did you evaluate pyedifice for your DAW UI? I just started work on my own DAW-esque project and I ended up going with Slint, but I discovered edifice while researching and it seems like it might fit your needs since (at least based on your article, which is probably outdated) one of your biggest issues with Qt seemed to be signal spaghetti
Ehh, I don't really want to use Python for the frontend. Also, my biggest issue with Qt is the rendering performance (it renders on the CPU unless you use QtQuick). I have also considered QtQuick, but the C++ to Rust bridge is pretty janky.
I'm actually experimenting with a custom bespoke ultra-optimized immediate mode API for my custom GUI library. If it works, then it would make both using and implementing the library much simpler.
Yeah my second choice after slint was to use qtquick for all the pieces and then tie them together with edifice and bind to rust for the high performance audio etc, but it was too much potentially-jankily tying together of multiple languages and systems and it wasnāt worth it
For my part I kinda need to have the behaviour/state be react+redux-style and thatās pretty much non-negotiable so I only have so many options
Thatās pretty sick, would love to see what youāve got going on there if you ever release it
Like, you can use qtquick components from python but you need to generate bindings and stuff and at some point it just becomes a nightmare
I actually didnāt know that qtwidgets were always cpu-side, I saw people saying qtquick was more efficient but didnāt see any elaboration on that
Yeah, QtQuick uses an entirely separate rendering engine. I guess there's too much technical debt for them to use the new renderer for QtWidgets.
Yeah that makes sense
Iāve only just started work on my slint ui but I spent days reading every word I could on the subject. Really smart approach and Iāve been really happy with it in my limited experience so far
returning to redux when you have ECS must feel awful š
zedās gpui is on crates.io also
Itās Non Send
It might need more
maturing though.
Ahaha I think for this project itās a better state model than an ECS, ECSes are incredibly powerful when you need to iteratively update data with a lot of modularity but I donāt really need that here. Redux is good when your state is monolithic and thatās the case for this project
Hi everyone, I am looking for advice on implementing a rhythm game with Bevy/Rust. From what I understand the biggest hurdles are having accurate time and input latency. I'm looking for a starting point on how I could learn how to do this well, and if all goes well I'll hopefully be able to create a PR to put this into Bevy š
With seedling at least, you should be able to have perfectly accurate relative timing. You can schedule events like audio playback at precise times relative to each other down to the sample.
The biggest hurdle would probably be input latency and balancing that with audio latency. Right now Bevy seems to have a surprising amount of input latency, although it may be tolerable with something like frame pacing.
With seedling as-is (which isn't part of Bevy itself to be clear), you shouldn't need too much to get going.
Thats good to hear! I just want to confirm I am understanding correctly. So lets say this was a guitar hero style game, just to keep things simpler to talk about. The backing song could be perfectly timed to any other sample I would want to put on top of it, lets say one corresponded to a button press?
And then I would have to use something other from whats built into bevy to handle my inputs.
Well that part I'm not sure about. There's some proposed reworkings of the windowing that should vastly improve this, but I don't know when they'll come around.
I'm also curious how seedling differs from Kira. Again, just so I can build my understanding of things
Also can you use seedling with out bevy? I don't think I'll go that direction, but just curious
No, it's a binding library between bevy and firewheel
But all the magic is in firewheel, so also yes
Kira does scheduling in a slightly different way, where each parameter like playback speed or filter frequency are self-managed. Firewheel's events are a type-erased, centralized system. They have their pros and cons. I imagine you can schedule exact times for playback with kira as well, but I'm not sure how. It's been a while since I've used it.
Firewheel's node graph architecture is a bit more flexible than kira's track-based mixing, which is particularly convenient for representing audio processing nodes as a graph of entities.
For input latency, you might want to look up bevy_framepace. It reduces the time between input polling and processing
If youāre doing something like guitar hero where you only care about the time delta between the input and the target you could also do input polling multiple times a frame. You might want to look into input latency calibration too
Also this might be overkill for a game but you might want to handle time based on a float number of beats, with +- duration so you can handle groove consistently. It makes it easier to ensure that youāre not losing precision and makes the microtiming easier to dial in bc you can make the musical time independent of the groove.
Ok actually computers donāt have fine enough input latency for that to matter, ignore that tip š
Thank you all so much, this all helps a ton. Gives me plenty to look into
Absolutely want to do this btw. I will not ignore it š
I would say something guitar hero-ish is a stepping stone, my aim is something much more complicated
Sure š Would love to see what youāre working on if you get anywhere with it, Iāve been wanting to make a rhythm game for forever
It has potential to be something because I have the opportunity to pitch this to the right publisher in late 2026. And I would actually like to work with others. But I can't afford to pay anyone until I get that funding š
@slim pulsar this may be interesting to you ^
hehe I've got some decent code for a metronome that will also test user input latency if you need :)
These are ideas only.
- bevy_time used by bevy_seedling
- scaling to 80 m stop playing sound instead of -24 dB
- maybe add tight room for 2D top down etc to change sound decay parameters
- 2D effects corvy had TODO: I might have a sketch
- limiting sounds to quadrants to 3 or 4 would MAX (4x4) at 16 + loop track as starting point
Again, just ideas
This is just so people can just stop wondering why their 192 tracks arenāt playing
how can i allow SfxBus to have a dynamically changable volume? using commands.spawn(SamplerPool(SfxBus)) doesn't work
Well that shouldnāt compile; a bus label like SfxBus isnāt the same as a pool label (like MusicPool).
With the default setting, SfxBus is a volume node. In other words, the entity with the SfxBus component also has a VolumeNode component. So you can adjust the busās volume like this:
fn update_sfx(mut bus: Single<&mut VolumeNode, With<SfxBus>>) {
bus.volume = Volume::UNITY_GAIN;
}
or you know however you want to mutate it
thank you
how can i implement a proximity voice ? when player is distanced to the transform volume is same
You need to use the spatial node + spatial listener system, thereās an example in the repo one sec
Make sure your transforms are set up correctly too, usually youāll have the listener and emitter both have a null transform but be parented to an object in the world. There was someone who had a bug because they were transforming their emitter twice (bc they copied the transform from the parent and then parented the emitter too)
No problem!
I can't remember how you add a source node to seedling, maybe Corvus has an idea
but it should be possible
Ah yeah, looks like you want StreamWriterNode. I canāt figure out how you send data to it from the docs either though
It looks like StreamWriterState is the way but itās not appearing in the docs so Iām not sure how to use it
Streams help for longer files and having less memory overhead. A voice chat having multiple voices mixed in, you need a bus to feed those voices into.
And that source would have to be static to remain always on at the top of your program.
Also, they are not asset sounds. They are mic inputs that require an output and cpal is not duplex like that.
Not sure what you mean here
Cpal supports audio input
So that when no voices are there, sound source still remains active and not dropped. This would the main bus where all sounds go into.
Duplex is input to output. That output of the input cpal does not have. Maybe you can hack it yet duplex is the easiest, meaning you can get the output of that input without introducing more chaos.
That isnāt necessary I donāt think, Iām pretty certain that buses can have a variable number of inputs, so they can be added and removed as necessary
Yes, however that bus has to remain active so that itās available
Ja, I still donāt know if I understand the issue š So long as you can get audio from the mic with e.g StreamReaderNode, pass it to something like libopus for encoding, send it over the network, then decode it and pass it to a StreamWriterNode
There are many ways. I just want it active all the time.
You should be able to just set up the graph with an input as discussed earlier #1378170094206718065 message
Clearly we should have an example for this!
I wonder whether it could be useful to have a separate repo with a full voice chat example with all the networking etc (probably with only basic encoding rather than opus or whatever youād need in a proper impl) since youād need several external crates that wouldnāt otherwise be used, I think a lot of people would want to know how to do networked voice chat specifically. If you fork that networked box game example (Iāll find it in a sec) that also gives you two movable players so you can show off spatialisation too, I think itād be useful as a way to collect a few advanced usecases
In my previous post, Introducing Matchbox, I explained how Matchbox solves the problem of setting up peer-to-peer connections in rust web assembly for implementing low-latency multiplayer web games. I said I'd start making games using it and I figured it's about time I make good on that promise, as well as write a tutorial while at it. I'll expl...
Great starting point!
Well, besides being bevy 0.12
Has anyone implemented automatic delay compensation for firewheel? I need to implement it anyway so Iām happy to contribute it for others to use but I donāt want to reimplement it if someone else already has
I seem to remember billydm saying they didnāt want delay compensation upstreamed, is that still true? (or was it ever true? I might be misremembering)
@static quest I made a vector_2d_node and example using your spatial_basic_node system and ui I want in to introduce in the bevy demo. Thanks. I really appreciate it. Demo needs to shine, Billy, so me pressing is mostly about that. It isnāt me.
@ionic sedge ^
His cat emoji oohāed!
I just want it in Firewheel if thatās ok.
I built it in Firewheel.
Well the main thing is that it would make the audio graph compiler quite a bit more complex. And in the context of game engines, you generally already know the structure of your graph upfront, so users can just manually add the delay compensation nodes they need.
(Corvus is in charge of the bevy demo, not me)
I def donāt think it should be implicit in graph comp but just a destructive graph.add_delay_compensation() call wouldnāt hurt right? Thatās basically how Iād do it as an external lib. Anyway, I donāt really mind if itās upstreamed or not for now, just whether thereās already an implementation I can use.
You can just add a delay compensation node like you would any other node.
I mean, yeah, but the part that needs implementing is calculating where and how much compensation to add
Anyway, Iām happy to implement it if it doesnāt already exist
Corvy is interested so I wanted to add to Firewheel first.
Oh, well doing that automatically will involve graph theory stuff. I'm not sure what an "easy to use API" for that would look like, but you're free to take a crack at it if you want!
Oh, I see. Yeah, you can send a PR to Firewheel that adds an example. Though I would prefer if the example didn't depend on bevy.
Itās based on your spatial_basic. Same ui same system.
Ja Iāll have a go, the nodes already have latency metadata so I have the info I need to do it
I have to clean it up. Thanks a lot.
Oh yeah, recently I found out that the implementation of my one pole iir highpass filter was actually incorrect (it was behaving more like a low shelf filter, which explains why it wasn't attenuating low frequencies very well.) I just pushed a fix for that!
Also, I think I'll go ahead and work on a "triple buffer" node today. Such a thing would be a lot easier to use to create visualizations than the stream nodes.
oh nice
Ok, I've added a triple buffer node and published Firewheel 0.9.2!
How can i configure the SR of the TripleBufferNode output/buffer? And/or resample it to a fixed SR?
Not sure whether this is more a Firewheel or seedling question.
Ah yeah, I suppose it would make sense to add an option for resampling.
You can use the fixed-resample crate in the meantime. https://crates.io/crates/fixed-resample
how can i make audio go to a different audio output?
Check out the output selecting example: https://github.com/CorvusPrudens/bevy_seedling/blob/v0.6.1/examples/select_output.rs
You can query for outputs and set the device name like in this observer: https://github.com/CorvusPrudens/bevy_seedling/blob/c2556c220d8109a869e23978d3fd1d3b32638dfe/examples/select_output.rs#L75-L93
is there a replay gain node?
Btw minor typo: https://github.com/BillyDM/Firewheel/blob/87738f981e8fd1161939fe24ae4e03a523eb0af5/crates/firewheel-nodes/src/fast_rms.rs#L34 loudness
As in ReplayGain? Iām not sure how you could do that in real time since, as far as I can tell, it requires access to all samples of all tracks.
hi @ionic sedge , thanks for your work on this crate. i've been reading thru the wg thread and this one and have a few questions (so far):
for my project, i'm interested in doing some procedural audio. above, you mention making that easy was a design goal. in the wg thread, someone mentioned that e.g. funDSP could be suitable for use inside a single node. am i correct in my understanding that the graph models of funDSP and firewheel operate at somewhat different levels of abstraction such that using them together wouldn't be misguided? or do you have a different recommendation?
what do you think of tunes?
i believe you also mention somewhere doing voice synthesis a la celeste. i'm also interested in that. is any of it public?
i believe you also mention somewhere doing voice synthesis a la celeste. i'm also interested in that. is any of it public?
Yeah I think I slotted it in the pretty text demo actually: https://github.com/void-scape/pretty-text/blob/fbcc8c0e8d6e7fdc76277ca51fbf94d6bd034f99/demo/src/audio/formants.rs#L148-L281
It's inspired by the vocal quality of Celeste's dialog sound, but the implementation differs of course (I think Celeste actually did it largely by hand, or they at least hand-tuned it).
Here's an example of integrating with fundsp for a super basic radio distortion effect: https://github.com/CorvusPrudens/bevy-radio/blob/master/src/radio.rs
I believe you could pretty much build the same graph with fundsp and Firewheel assuming both had equivalent processors. The only thing difficult with Firewheel is single-sample feedback across nodes.
In practice, you'll probably want to author advanced, involved processing by hand in a single node in Firewheel. And that's a great place for fundsp! Although we may see more and more people composing effects in the Firewheel graph as the number of nodes increases over time.
gotcha, thanks. i don't understand any of the crates well enough yet to know, but i wonder whether reimplementing fundsp functionality in firewheel over time is worse than writing some integration bridge? again, i just started looking at this yesterday. but i'm imagining perhaps a blanket trait impl for fundsp types that turn them into firewheel nodes (extremely half baked spitballing)
I think an adapter crate would be pretty straightforward and would only be slightly less ergonomic than first-class integration, and IMO reimplementing similar functionality doesnāt really make sense. I had an integration somewhere in the history of seismon but I donāt have access to my laptop to find it right now unfortunately and I donāt remember how generic it was
I didn't comment on tunes in #audio-dev because I think it's a cool project, and the author seemed really excited to share it! However, my impression was that it's a not a better fit for Bevy than something like Firewheel. It's a little rigid and has a lot of code that isn't all that useful for gamedev, even if it's cool for art installations and composition.
thanks - this is exactly the kind of insight i was looking for
The radio repo has an example of how you might integrate fundsp in a generic way: https://github.com/CorvusPrudens/bevy-radio/blob/master/src/fdsp_host.rs. It's what allows the other file I shared to return a FundspConfig that "just works."
However, there's an impedance mismatch between the APIs of the two crates, and maximizing the performance of a Firewheel node necessitates hand-authoring.
So I wouldn't personally pursue it as a long-term solution.
For end users though it oughta be decent for the moment.
I'm also not super excited to pursue it because I have frequently run into inexplicably missing effects with fundsp, even something as basic as compression.
Also, the operator syntax is quite difficult to reason about. I've experienced it myself and seen others trip over it. It can be fun, but I also have to frequently pull up the reference and puzzle over exactly how I'm supposed to arrange the statements to get the graph I want.
interesting. i have no attachment to using fundsp in particular except that it seems like the most featured procedural audio crate. do you reckon there is room for or utility in adding some basic e.g. oscillators to firewheel/seedling?
I think @static quest wants to keep Firewheel itself lean, but I would love that in bevy_seedling or (even better) a third-party Firewheel crate for music-oriented DSP. We've got a few things that could go in there already.
i'm actually primarily interested in sound effects (like for collisions) for now, tho may be interested in music later on. would you mind linking me to those few things you'd want to put in there so i have an idea of what others would look like?
Yeah like this envelope PR from eira and this loudness node although that's not necessarily for synthesis.
I don't think we have any generators lying around at the moment.
Feel free to ask for more help or suggestions though, I'd love to see the number of effects and sound sources in the Firewheel ecosystem grow! I've been talking about creating a repo to collect these nodes for a while, but there hasn't been quite enough activity to justify it yet. If you get on a roll, though, it may finally be time.
ok great. i'll have a look at all this tomorrow. thanks for your help!
ok i think my first question is about the focus on samples in bevy_seedling and whether there ought to be some preliminary work done to better support non-sample sources. e.g. the only pool available is a SamplerPool but i think similar functionality in a SynthPool could be important?
If an API could be written that does not compromise the ergonomics of sampler pools, I'd be happy to pursue it. I'm not sure that's possible, though.
Sampler pools are natural to prioritize because almost all games use only samples for all audio. I could probably count on one hand the games I've played that don't do this. That's not to say this isn't valuable, but it should support my assertion that samples are the most important part of the whole crate.
As-is, bevy_seedling doesn't even use Firewheel's SamplerNode directly. It exposes an abstraction over it that simplifies the pooling. Additionally, the pools have some bespoke behavior around sample completion and prioritization that seem a little difficult to generalize.
Furthermore, it's not always clear what a single unit of a generic pool would be. If these synth voices are monophonic, maybe they could receive individual notes. But they could be polyphonic. There could be any number of "units of work" for any given pool.
Put simply; I doubt it's worth it from a complexity and maintenance standpoint to pursue this kind of generalization for bevy_seedling. Focusing on sampler pools allows us to take shortcuts for the most important part of the crate, at least for most users.
For those that need other kinds of pools or queuing, my hope is that the rest of the crate provides an easy enough base to work from.
I should probably reemphasize that it's possible a simple, convenient, and more general abstraction is just waiting to be realized. It has not been my priority so I may be blind to it.
makes sense to me - thanks for the thorough explanation
i agree it's not clear what a synth pool would look like. i suspect that if it did exist it would not be an instance of a more generic pool but rather a separate type entirely
I do not know the right terminology, bear with me. Is the internal update rate of the node graph constant? Or does it depend on hardware/settings/...? (ignore web)
So if my source is fixed, let's say a file, will the graph will be evaluated always in the same time interval (e.g. 8ms, so the amount of samples, in ProcBuffers, i get in AudioNodeProcessor process(...)) will always be the same depending on the sample rate of the source file and), assuming the system is not congested. OR, if not, is it possible that for the same audio file it runs at 450 samples per process() call on my machine and e.g. 550 on another?
It's dependent on hardware/settings.
The operating system is in charge of invoking the application's audio thread at the necessary rate.
All nodes need to be concerned with is processing the audio buffers they are given. Keep in mind that the host may not necessarily be rendering audio in realtime. Another use case is exporting the audio output to a file, where the host will keep calling the process method as fast as possible.
does it make sense to compare two firewheel::Volume by comparing their .linear()? or is there a better way
it should be lossless so thatās fine!
Ah, right. The auto-derived partial equals would treat the linear and decibel variants as separate. Though I imagine it would be quite rare to ever work with values of both types, usually you choose one type or the other for a given project.
probably! I was surprized to fing out I cannot compare Volumes tbh, maybe it makes sense to derive PartialOrd?
Oh, does Volume not already derive PartialOrd? That's an oversight if that's the case.
yep, thats why I was asking in the first place š
you can't do node1.volume >= node2.volume
mic inputs and handling bussing around live inputs? yeah would be great.
Yeah so actually we do as BillyDM mentioned. You can just add an input to your stream config in bevy_seedling. The main downside with the current implementation is that you'll get a bit of latency on most platforms.
ok š like more latency than in a daw with a slowish builtin soundcard?
cpal does not have a universal duplex audio abstraction, so we manage it ourselves. That requires separate streams for the input and output, which is where the latency comes from.
Naturally, the latency does depend on the block processing size, which relates to both the audio hardware and platform.
is there all list of sound fx usable and is there fx that are not part of seedling?
I don't think there's a definitive list anywhere. Right now in Firewheel we have:
- Algorithmic reverb (freeverb)
- State variable filter
- High pass, low pass, and band pass filters
- Volume, panning, and basic spatialization
- Noise
You can find them here. Not all of them are registered by default in bevy_seedling currently, but they'll be added soon.
bevy_seedling also has
- HRTF spatialization (via
firewheel-ircam-hrtf) - Limiter
There is also a convolutional reverb.
*cpal
Ah I didn't realize the reverb for convolution was already merged.
I think it could be useful to have a limiter as part of the Firewheel repo.
yes a limiter is crucial. i thought i saw one somewhere
The default setup includes one as the final node before the output in bevy_seedling.
thanks.
so is there fx that are not part of seedling that we can manually use in our project? or make our own?
I'm not sure if there are any big ones that aren't already in there, but you can certainly make them.
You can see how firewheel-ircam-hrtf does it for example. It's a third-party Firewheel node that's bevy_seedling agnostic.
I donāt think there are any cross-platform crates that provide a duplex interface for I/O right? The jack crate has an interface that hides enough of the implementation details that the API could serve as a good basis for something cross-platform, maybe thatās another thing to add to the long list of "boy it sure would be nice if someone could make this" š
Yes, as far as I know, no oneās done it yet.
Well I need that at some point anyway for two of my projects so if no-one does it by the time I need it then Iāll make it and ping here
If you are ok with depending on C++ libraries, I have created rtaudio-rs.
Oh yeah, I still need to add that as a backend option for Firewheel.
@sly crown has also been working on a new crate called interflow.
C++ is fine, my only restriction is that I canāt use gpl code
Rtaudio is MIT, so you are good there.
Checked this out and it's exactly what I need, thank you!
i got my web audio working using the firewheel web audio backend. works well!
@ionic sedge at the risk of starting a terrible bikeshed, I recall from a year or two ago that cart was in favor of naming things Music and SoundEffect (rather than Sfx). See his comment on a PR in bevy_new_2d.
And link to the source showing some code references.
Would this suggest bevy_seedling should also use the term "sound effect" in code rather than "sfx"? Might be one less hurdle to upstreaming. It already uses the term "music" so that's good.
I think Sfx is used only for the SfxBus. It doesn't need to be that short; SoundEffectsBus is perfectly acceptable.
I'm also in favor of this
Note the lack of s: SoundEffectsBus -> SoundEffectBus.
Well, SFX translates to "Sound Effects."
That style would also match some of the other naming decisions in bevy_seedling I think, such as SampleEffects.
Does "bus" imply plural? I thought it did.
You could probably argue in a number of ways, but what prepends "Bus" here is the category of sound. In other words, it's not SoundEffects because there are multiple effects, but because it's a bus for the sound category of "Sound Effects."
For example, we frequently have the categories of music, dialog, and sound effects in games. Thus, I'd name their buses MusicBus, DialogBus, and SoundEffectsBus.
If you have a list of cars you would say CarList not CarsList, so if you have a list of sound effects you would say SoundEffectList not SoundEffectsList. Now, that in that list of cars, it's very similar to a "category" of cars, so by that logic I think you could still have SoundEffectBus.
I likely shouldn't make the call here and could see it go both ways, so happy to leave it with you though š
The tricky thing with this is that the singular of Music is Music and the singular of Dialog is Dialog.
Do you work with audio software much @next otter ? This isn't a challenge, I'm just curious.
Nope. I'm no expert. Hence being happy to defer to you.
If you'll excuse my use of LLMs, I tossed the question at two different ones. Interestingly, they both settled on the opposite term, suggesting there may not be a strong consensus in their training data.
Either way, we'll definitely want to expand the abbreviation. I'm personally leaning towards SoundEffectsBus, though I acknowledge it can feel a bit awkward, and perhaps SoundEffectBus would feel more comfortable to most programmers. There are some other names that may feel similarly awkward, such as SamplerPool over SamplePool, but that I nonetheless prefer.
Even if there's friction on these choices at the point of upstreaming, I think there's still value in moving away from Sfx now.
Yep all good. And LLMs use is fine, it's good for bike shedding.
And yep I just suggested without the s because SoundEffectsBus looked awkward to me: my programmer eye always notices "double plurals" used in things being awkward.
Anyway, one step closer to perfection! š
@ionic sedge unrelated, I noticed that AudioSample isn't reflect (it's just TypePath), and doesn't have register_asset_reflect. This means when you look in inspectors, e.g., inspector egui, you can't actually see your loaded samples. Note: I don't expect to actually do any reflect things on it, it would just be nice to see the asset loaded in the inspector list (you'd just see the asset path).
Is this something you think is worth adding (if it's possible)? The use case is simply that when debugging why a sample isn't playing, one useful thing to do is check the Assets in the inspector and see if it's actually loaded (I know this isn't the only way to debug it).
Yeah I think the issue there was that it was just a little annoying to manage Reflect (or at least PartialReflect) for the inner trait object.
Though I don't recall the exact issue.
Yeah I tried to do it quickly and struggled due to the ArcGc<dyn SampleResource> and couldn't work it out (even when trying to just reflect(ignore) it.
I'm much more familiar with bevy_reflect now than when I started with bevy_seedling, so I may be able to write a reasonable manual implementation if necessary.
I'll make an issue
BTW I'm currently migrating off bevy_audio, bevy_kira_audio and bevy-kira-components š
. Excited to get back to one audio crate. It's early days but seedling is great so far, so thanks for the work.
All three at once? Like in three different projects?
(That's a lot of migrations haha.)
I'll definitely be interested to hear how it goes!
No lol all the same project.
bevy_kira_audiowas first, because at the time that had the closest chance to being upstreamed and generally seemed favored overbevy_audio.- Then I introduced
bevy-kira-componentsbecause it looked like it could be close to being the "way forward", but then was deprecated. - I then thought going to
bevy_audiowould be a safer choice, but as I started the integration I ran into a weird bug, which I believe was arodioone. Can't quite recall, but something to do withrodiodoing an incorrect sample rate conversion so the sound played weird (butkiradidn't have the bug!). - Been delaying the migration, but the latest
kirachanges meant it was annoying for me to upgrade my fork ofbevy-kira-components, so I decided to bit the bullet and go tobevy_seedlingnow. No pressure!
Early on in the above journey I introduced features to control the crate's audio lib, and that has helped a bit.
oh i see it's been quite the journey
I think I ran into an issue with rodio over the summer where it would distort the first few hundred samples of a sample, making short samples or those without leading silence sound a bit weird. Maybe it was related to that (I believe they've fixed that).
(ah, https://github.com/RustAudio/rodio/issues/811 is what i ran into)
Maybe that was me as well:
I'm trying to get precise scheduling working so that I can play the next sample of my music when the current one finishes without (i.e., so they play seamlessly).
I have it close to working except the transition between my first and second sample is weird (the second sample plays too early).
That's just some background. Happy to share code later, but I'm fiddling with the precise_scheduling.rs example and just trying to make it start playing after ~15s so I can prove that this works. So I changed that line to:
settings.play_at(None, time.delay(DurationSeconds(15.0)), &mut events);
(And I removed the other settings.play_at() call and the fade in, fade out stuff.
However, two things:
- The sound starts playing earlier than that unless I construct the settings with playback=false:
let settings = PlaybackSettings::default().with_playback(false);. Why is this? - Without the
with_playback(false), the sound starts playing earlier, i.e., sooner than 15s. This is strange because with my Airpods in, the sound plays after ~7-8s, but with them out and playing through my Bose, the sound starts around ~2s. Maybe these are just audio things and it's fine, but I wanted to share. I think 1) is the confusing thing for me. I can't get the sound to play at 15s unless I constructwith_playback(false).
This is on c2556c2.
Side note: Would be good to have an example showing how to seamlessly play one sample after another (where the samples are meant to be "joined" together seamlessly, i.e., an original sample was split into two.
There are two main things happening here, I think.
The first is that play_at merely schedules an event. You can think of it as setting PlaybackSettings::play to true at exactly 15 seconds from the point of scheduling. (In fact, the event will do that in the ECS while simultaneously sending the event to the sampler). That doesn't mean much if the sound is already playing! Thus, it needs to start paused.
The second probably just relates to asset load times. Right now, we don't do any streaming, though that's one of the top priorities in the near term. That means if you just spawn a SamplePlayer with an asset you're loading for the first time, it has to wait until the entire piece of audio is read from disk and decoded before it can start playing.
You can mitigate this delay by preloading assets, and this should be almost a non-issue once we get first-party support for streaming going.
Are you airpods running at a much higher sample rate, maybe? I could see it taking a bit longer if it has to resample to 96kHz, maybe.
Note that, by default, we compensate for this load time (assuming it should be brief or non-existent following best practices), starting the audio where it should be had it actually started playing when you spawned it. That means long load times may appear to "cut off" the beginning of samples.
This is intended to make it easy to schedule things properly. If you spawn a SamplePlayer and it takes a moment to find space in the sampler pool or takes a while to decode, you can nonetheless rest assured that anything scheduled relative to it will be perfectly in sync.
This isn't directly related to your issue, but I thought I'd mention it in case you run into it.
Ah, I can see where some clarity is lost with the naming. It's quite affirmative, even though it's merely scheduling the event. I should probably also explicitly indicate that it's a scheduled event in the docs.
A name like schedule_play_at works of course.
FYI
Hm, seems a bit odd it would take that long (unless you aren't applying any optimizations, in which case it could take quite a while).
Plus, if the sample is at 44.1k, then even though 48k isn't much higher, it still has to do additional resampling in addition to decoding.
The first is that play_at merely schedules an event. You can think of it as setting PlaybackSettings::play to true at exactly 15 seconds from the point of scheduling. (In fact, the event will do that in the ECS while simultaneously sending the event to the sampler). That doesn't mean much if the sound is already playing! Thus, it needs to start paused.
Yeah OK I think that makes sense. It was clear to me that play_at schedules it (after all I was trying to schedule it at 60s!). I think what is not clear is that the default playback value hidden in PlaybackSettings::default() makes it less obvious you also need to call with_playback. But maybe that's just teething issues and once you know you know (or if you think more on it which maybe I didn't do).
First log below happens when the program starts. Second is when I hear the sound.
Airpods logs (7s):
2026-01-13T06:41:10.122881Z INFO symphonia_format_ogg::demuxer: selected vorbis mapper for stream with serial=0x9b62f472
2026-01-13T06:41:17.043927Z DEBUG bevy_seedling::pool::queue: queued 1 sample in bevy_seedling::pool::label::DefaultPool (4 total, 4 inactive, 4..=32)
Bose logs (2s):
2026-01-13T06:41:42.740395Z INFO symphonia_format_ogg::demuxer: selected vorbis mapper for stream with serial=0x9b62f472
2026-01-13T06:41:44.544602Z DEBUG bevy_seedling::pool::queue: queued 1 sample in bevy_seedling::pool::label::DefaultPool (4 total, 4 inactive, 4..=32)
Again, could just be audio things, all good.
Oh, yes so it's just taking a while to decode the audio (and seemingly to also resample for the airpods).
Side note: Would be good to have an example showing how to seamlessly play one sample after another (where the samples are meant to be "joined" together seamlessly, i.e., an original sample was split into two.
BTW, in my previous impl (bevy_kira_audio) I conveniently could spawn/create the new sample immediately after I determined the current sample had finished, and there was no gap in audio.
With bevy_seedling, using that same mechanism, I had a gap, so that's why I changed the code and instead reached for precise scheduling, which I'm fine with, but just wanted to note why an example could be useful.
Ah, I'm curious how you were checking for completion with bevy_kira_audio.
Just checking if playback is stopped on the instance (a system that ran every frame...):
fn update_pattern(
mut commands: Commands,
mut rng: Single<&mut WyRand, With<GlobalRng>>,
mut audio_instance_assets: ResMut<Assets<AudioInstance>>,
audio: Res<Audio>,
sound_config: Res<SoundConfig>,
script_assets: Res<Assets<MusicScriptAsset>>,
mut query: Query<(Entity, &mut Music, &mut PlayingPattern, &MusicState)>,
) {
for (entity, mut music, mut playing_pattern, music_state) in query.iter_mut() {
let Some(script) = script_assets.get(&music.handle) else { error!("Music script not found"); continue; };
let Some(instance_handle) = music.audio_instance_handle.as_ref() else { continue; };
let Some(instance) = audio_instance_assets.get_mut(instance_handle.id()) else { continue; };
// If the music is not stopped, we don't need to update the pattern.
if instance.state() != PlaybackState::Stopped {
continue;
}
// Can now spawn/start next sample...
Hm, and how fast were your frames compared to the processing rate?
Maybe that's a little low-level of a question, but the concept should be no different in bevy_seedling. We're also just polling the playback in the ECS to see if it's done, and queuing up a sample (that isn't delayed by loading) should start playback on the same frame.
I don't expect you to look at the code, but just threw up the code at https://gist.github.com/mgi388/6ec29517f13c34aca4020c63c4526d6f.
With that said, I'd still recommend scheduling playback. I don't know how broadly you tested this method of queuing playback -- maybe it's fine in practice -- but I'd expect it to be unreliable across platforms and even just hardware.
Since the update rate of the ECS and the audio thread are completely unsynchronized, it's likely that gaps will sneak in if the two differ significantly.
By default in Firewheel, the audio buffer is 1024 samples (though it is configurable), which results in an update rate of 48Hz at a sample rate of 48kHz. That means new events from the ECS are only accepted 48 times a second.
kira should have similar constraints, though it's been a while since I looked at their implementation. I do think they'll have a buffer size of 512 by default though, which may be influencing things.
Hm, so is this bit still problematic?
(Note that starting playback at the exact time provided by Time<Audio> happens to be the default behavior when spawning a SamplePlayer.)
/// TODO: Does this gracefully handle when the player changes the volume while
/// a fade out is in progress?
https://gist.github.com/mgi388/6ec29517f13c34aca4020c63c4526d6f#file-bevy_seedling_impl-rs-L539-L540
This actually depends on what you're changing exactly. If fades are applied directly to the effects of SamplePlayers, and the user-controlled volume is applied to the pool as a whole, then the two will mesh together perfectly.
However, if you want to change the volume of a node that has an ongoing fade, you'll need to cancel the fade (by clearing out the AudioEvents).
One simple way to sidestep this fade-cancelling issue is to just add another volume node in series! One could be dedicated to fades while the other is dedicated to manual control.
With that said, I'd still recommend scheduling playback. I don't know how broadly you tested this method of queuing playback -- maybe it's fine in practice -- but I'd expect it to be unreliable across platforms and even just hardware.
Iām glad you said that. When I saw the scheduling thing I thought āoh I bet this means my original implementation was childishā
Yup
Got it. I just did this to try and see if it helped. I was trying to create parity between the code for the first and subsequent samples.
Thanks for the tip. Per the TODOs I need to revisit the volume stuff now that Iāve seen seedlings examples, e.g., settings menu
As an aside, a more sophisticated animation system that can blend different sources like this will probably wait until Bevy has something with nice UX built-in.
Iāll try and break down this issue into a tiny repro and share it once I have it and that should eliminate user land issues here. If it turns out useful and you want it maybe you can keep it as an example for playing seamlessly.
Hm, is it possible the issue could be related to this also just using the default initialization (without pausing before scheduling playback)?
https://gist.github.com/mgi388/6ec29517f13c34aca4020c63c4526d6f#file-bevy_seedling_impl-rs-L290
Hehe that was it.
@@ -291,28 +287,31 @@ fn spawn_scheduled_sample(
sound_config: &SoundConfig,
) {
let mut events = AudioEvents::new(time);
- let settings = PlaybackSettings::default();
+ let settings = PlaybackSettings::default().with_playback(false);
I wonder if we could make it a little less touchy
although I'd probably consider precise scheduling of this sort to be moderately advanced usage.
Yeah was gonna say if there was some way to make the ābadā code an impossible state that would be good. But maybe this is also just a docs / learning thing.
It's actually quite difficult (maybe even impossible) to avoid this issue given the overall design of bevy_seedling (and Firewheel). I'll spare you the details, but I do agree the docs could be improved here. Scheduling was the latest addition to the crate, and involved a surprising amount of complexity to implement even as it is. The docs haven't quite caught up yet.
Although if we have a nice, unified animation API in Bevy, that might alleviate some of the annoying aspects of directly scheduling these events.
Ack. Donāt know if itās a bad API/idea but if there was a settings.play_at_and_playback_will_be_set_to_false_when_you_call_this() I guess I wouldnāt have run into this?
ya you could do that
Obviously name was a placeholder. Needs to be pluralized š
Part of why it's fiddly in my opinion is that there's a moment where all the bits are kind of just on the stack instead of neatly arranged in an entity. You need a SamplePlayer, PlaybackSettings, and AudioEvents, and the latter two need to be initialized with a particular sequence.
I expect scheduling a precise, initial time for playback like this will be fairly common, so we could specifically streamline it.
Imagine, for example, an associated function on SamplePlayer that does all of it in one swoop:
fn new_at_time(sample: Asset<AudioSample>, time: InstantSeconds)
-> (Self, PlaybackSettings, AudioEvents);
We could also have a builder-style API, either as an extension trait for Commands or even as a standalone object.
I think any of these approaches would help streamline the common case without placing any constraints on power users.
Yeah for sure. I could see a builder thing here that forces you to pick playback true or false even. But yeah thereās options to consider isnāt there.
Just wanted to note that the reason I did this was so that I could capture the scheduled_start time of the first sample because the next sample needs to refer to it. I suppose I could just pass scheduled_start: time.now() but maybe it's better to be explicit by calling play_at with the same time value. Anyway just an FYI.
Also FYI, even though this fixed the early playing of the second sample, upon closer listening I've realised the transitions between any sample are still not 100% seamless. Going to try and get a minimal example running and go from there.
@ionic sedge got an interesting bug for you. Could be in firewheel land, so you may need to redirect it to Billy, but hopefully the PR lets you both repro at least. https://github.com/CorvusPrudens/bevy_seedling/pull/76
Hm, I wonder if that's cpal itself.
what's your OS?
If that is the case, it looks like it should be resolved when the Bevy 0.18 migration is complete, as Firewheel's latest version updates cpal to 0.17.
Oh so it's a known cpal issue? Edit: Clicked the link above.
Do you think you'd consider doing a Bevy 0.17-compatible release with the upgraded firewheel + cpal versions or too much effort?
Hm, yes that should be okay.
Ah, although I don't think Firewheel has made a release to crates.io yet with the new cpal version.
It would have to be a git dependency without greater coordination.
Either way, I think I'll have to work through the new release over the next few days, so it won't necessarily happen today or tomorrow.
Ofc, didn't expect it immediately, take your time! And yeah whatever you can do I appreciate. If there's a git-dep version in a branch I'm fine with that too.
oki then that should be doable since Firewheel has a version on git with the new cpal and old bevy
@ionic sedge added a seamless example repro showing the issue https://github.com/CorvusPrudens/bevy_seedling/pull/77
I hope this is useful to you and that the approach itself isn't flawed and leads you astray...
Note: Example "doesn't work" yet. PR created for repro.
I took one of the audio files from this repo and used Audacity to split it. I roughly split it into 6s chunks to ge...
Thanks! We should be able to pin down the exact issue here, and if it's in bevy_seedling or Firewheel itself, it should help us verify a fix.
There's a couple of UX things that fell out of writing the seamless example (and my own seamless music code). Feel free to look at this when you look at the seamless example, but posting here not the PR in case it's worth a discussion.
-
If there was a way to avoid having to reach into the tuple in calls like this it could be cleaner:
let time_until_end = scheduled_end.0 - time.now().0;. But maybe it's fine as is. -
Getting the duration of the sample:
This is how I get the duration in seconds for the sample (and assuming this is right):
sample_rate: Res<SampleRate>,
// ...
let sample_resource = sample_asset.get();
let sample_duration_seconds = sample_resource.len_frames() as f64 / sample_rate.get().get() as f64;
a) If there was any way to just say foo.duration_seconds() to get it that would be great. I guess you could add a method to SampleRate like duration_seconds_of(SampleResourceInfo).
b) The sample_rate.get().get() is unfortunate. This would go away if we did something like in a).
Yes I think these could be addressed nicely. I mean ideally we'd actually be able to do asset pre-processing on the samples to get their length more quickly, but that'll have to wait.
If the sample rate changed at runtime would that impact that idea though? I assume sample rate can change at runtime.
Yes, absolutely it can. Switching between your hardware devices at runtime, for example, will force it to change. bevy_seedling triggers an event in this scenario, though.
Ah, but to be clear, we resample the assets when the sample rate changes, so the time shouldn't meaningfully change.
Ah ok cool.
There may be a very small amount of imprecision though.
corvy, I have MissingEffect {
/// The [EffectOf][crate::pool::sample_effects::EffectOf] entity missing
/// an effect.
empty_entity: Entity,
}, it compiles fine yet errors. It is a basic_spatial node clone and it has its own example just like basic_spatial. Is this error a connecting to Bevy or a connecting to Firewheel after connecting to Bevy? I used a basic_spatial seedling example.
What's the full error?
Can you post the message verbatim?
Error::bevy_seedling::pool::dynamic Expected audio node in āSampleEffectsā relationship. Sorry
in error.rs
I just switched basic_spatial node
And it errors after compiling just fine
Started cpal
Started output stream
Started physical stream
Selected vorbis mapper for stream
Then it errors
And I can run my example just fine
On my own
I did diff skip in node struct so it might be error there? Firewheel did not complain.
I assume you're talking about a custom node you made here? Did you remember to register it?
if it doesn't do diffing
I forgot that. I added it now. Thanks. Iāll read amazing docs.
A playtester asked me if I could make the app switch the audio output when the system does (they plugged in headphones and the game stayed on the default speakers)
Is there a way to do that with seedling (or a plan to implement one)?
Hm, I think you could do that in user space, although it would rely on polling.
Currently, I think we only switch devices automatically if the current device disappears.
But if you polled every once in a while and checked what the default device is, you could restart the stream on the new default.
Interesting
Do you know if there's a... "system hook/event" I could listen to?
I'm not sure. I don't believe cpal provides anything.
And what do you mean by "doing that in user space"?
Like, in an options menu?
Well, I mean you as a user of bevy_seedling could do this in the same way that the crate itself would.
(via polling)
There's no privileged or private API in bevy_seedling that prevents you from doing it, in other words.
I see
(I have no idea how to do that, but I'll do the research tomorrow)
Thanks
If you want to use the ECS API, you can trigger the FetchAudioIoEvent, which will update the device entities.
You could then observe On<Insert, InputDeviceInfo>, and if a device is inserted with is_default: true, then write to the AudioStreamConfig resource with the new device name.
This is basically a wrapper around cpal's APIs with the default backend though, so you could skip the ECS stuff and just fetch the devices directly. You'll still need to update the AudioStreamConfig resource though.
FYI when programmers talk about āuser landā or āuser spaceā it usually means do it in your own program rather than relying on the dependency to support it.
Thanks
Right now it seems I'll just do that every frame haha
It works flawlessly, thank you
Sending here in case someone else would want to put that in their code
For what it's worth, I just tested this briefly on macos. At least with my setup, when I switch from MacBook Pro Speakers to my Bose speakers, the audio output switched correctly and automatically without any intervention from me in my Bevy code.
@ionic sedge I am implementing a spatial sound effect type which is "random looping":
/// A sound effect that randomly selects one sound from a list to play, with
/// the selection looping.
///
/// E.g., used for a "birds" sound effect where one sound is randomly picked
/// from the list, it is played, then the sound effect loops and randomly picks
/// another sound from the list, and so on.
I am using observers to listen for when I need to spawn the next sound. Now, like before I could look at doing precise scheduling, but at least for now, I don't really need it for this so I just went with what I thought was the simpler approach to just spawn the next one when the current one finishes.
The issue I ran into using observers is that I end up with "infinitely" creating many VolumeNodes and/or SpatialBasicNodes (I tried a few different things and I either saw many many VolumeNodes or many many SpatialBasicNodes). The samples are really short (it's birds), so the entities pile up quickly.
I managed to fix the issue by spawning a "marker" entity in the observer and instead using a separate Update system listening to Added<Marker> to spawn the next sample. This stops the "infinite" entity spam, but
a) This sounds like a bug somewhere (I don't have a repro for this observer case sorry)
b) Using the Update approach (see https://gist.github.com/mgi388/2e57cb3959824be1971daed48f4e639e), we don't see infinite entities anymore, but we do see them continually spawning and despawning. This can be seen looking at the following logs and noticing that the entity version keeps going up. Now, from the code, I do expect that we're continually spawning new SpatialBasicNodes, but my question is whether or not we're meant to be doing this or if we should be reusing a previously spawned node or something?
The last entity in the list is the one that is changing 60v0 in the first line, then 62v254 in the last line 9 minutes later.
2026-01-18T04:05:13.326579Z INFO spatial_basic: Audio nodes - Volume: 22, SpatialBasic: 11 ([98v0, 97v0, 96v0, 95v0, 94v0, 93v0, 92v0, 91v0, 66v0, 64v0, 60v0])
2026-01-18T04:05:14.316919Z INFO spatial_basic: Audio nodes - Volume: 22, SpatialBasic: 11 ([98v0, 97v0, 96v0, 95v0, 94v0, 93v0, 92v0, 91v0, 66v0, 64v0, 60v0])
2026-01-18T04:05:14.491803Z INFO spatial_basic: Player despawned, spawning trigger entity
2026-01-18T04:05:14.508683Z INFO spatial_basic: Trigger detected, spawning new SamplePlayer entity
2026-01-18T04:05:15.310274Z INFO spatial_basic: Audio nodes - Volume: 22, SpatialBasic: 11 ([98v0, 97v0, 96v0, 95v0, 94v0, 93v0, 92v0, 91v0, 66v0, 60v0, 99v1])
2026-01-18T04:05:16.315320Z INFO spatial_basic: Audio nodes - Volume: 22, SpatialBasic: 11 ([98v0, 97v0, 96v0, 95v0, 94v0, 93v0, 92v0, 91v0, 66v0, 60v0, 99v1])
...
2026-01-18T04:14:33.311654Z INFO spatial_basic: Audio nodes - Volume: 22, SpatialBasic: 11 ([98v0, 97v0, 96v0, 95v0, 94v0, 93v0, 92v0, 91v0, 66v0, 60v0, 62v254])
2026-01-18T04:14:34.315147Z INFO spatial_basic: Audio nodes - Volume: 22, SpatialBasic: 11 ([98v0, 97v0, 96v0, 95v0, 94v0, 93v0, 92v0, 91v0, 66v0, 60v0, 62v254])
So is the problem that there are many of these nodes in the world all at once, or that they're continually being created and despawned?
Or is something else going on?
There are many. They keep being created but not cleaned up. And frame rate eventually tanks.
With your deferred approach?
With the observer approach.
Right, but do you have any issue with how it is now (like in the gist) except that it's awkward?
Or are there more issues.
With the differed / Update system approach (the one from the gist) it works and thereās no frame rate drop. But Iām wondering if despawning and respawning a new spatial node is a problem.
Which is why I wondered if I was meant to create the node once or something and reuse it somehow.
No, that's quite cheap -- nodes that are spawned attached to sample players are not themselves added to the audio graph. We simply diff them to send updates to the audio graph. But I think I see how your original approach could spiral out of control.
Is it something to do with some of the observers in bevy_seedling not being fired if I use observers on my side and insert the new components / spawn the new entities and some stuff on seedling side gets skipped or doesnāt see a diff?
Sample players can be despawned for three reasons.
- The sample completed playback
- The sample was booted from the sampler pool because another sample with equal or higher priority was queued and the pool is full
- The sample was queued with a lower priority than everything else in the pool and didn't find a slot for 250ms (or whatever its individual queue lifetime is set to)
I'm trying to think about if 2 or 3 could cause a cascade of sample players that just don't fit in the pool.
One of the changes I'll be pushing this weekend is to actually give the reason for sample completion in the PlaybackCompletion event, so you'll be able to determine why a sample "reached completion."
Ack re nodes being cheap here and Iām fine with the āuglyā deferred approach. Itās a shame I guess that I canāt do it inside observers but maybe thereās some weird state happening where something is not seeing a state / data change (like we need a sync point to run). But Iām kind of just making these words up, no evidence this is the reason.
I think it ought to work. I don't see how the number of entities could explode if they're only spawned when the current one is despawned.
If you have the time, a minimal repro could help get to the bottom of that. I should be able to address it this weekend along with everything else.
Yep I should be able to make a minimal repro š¤ canāt do right now will do later.
Separately, I made gizmos for spatial audio for bevy_seedling. Should I PR it or do you want to do it yourself?
(Or not at all which is fine)
Oh, really? Hm, I think we'd want to add a new feature for it since I imagine it'll bring in some rendering crates.
Yea I added a gizmo feature
Looks something like this. My camera is broken right now so hard to get a better angle.
Hard coded for now. This is a āremakeā of an old game if you didnāt know (so not my assets, etc). I havenāt worked out what values to use for the distances. Iāll probably put it in some asset config eventually and just make up the values myself.
That said, each of these spatial effects is an āinstanceā and they have data attached like the sound effect packet and the sound effect ID within the packet, so itās surprising they didnāt also attach the distance. Butā¦hard coding is easier and old games š
Oh you meant on the gizmo lol
Well, in other words; how do you calculate the radius of the gizmo -- is it based on the spatial parameters or just hard-coded?
Also, how many concentric gizmos do you think are too many?
One thing we could do is render successively fainter gizmo spheres around a single sound. We could draw them every -6dB for example. That would show you both how far the sound goes before it fades to nothing and exactly how the falloff settings influence the decay.
Might be too much visual noise though. We could also just do two, where one is perceptually "halfway" and the other is around -30 to -50 dB, where it's effectively "silent."
Sorry went afk so donāt have the code right now. But itās not hard coded. It uses the spatial node config.
The above all sound like good ideas. Could always have each as a setting. Allowing the dev to choose different views essentially.
We could also get sweaty with it 𤣠we could analyze the sounds live, get their RMS dB over the last 100-500ms, and then mutliply that by the falloff to get the live, apparent "reach" of sounds. That would be cute.
That's for another time though.
Sounds like an audio engine engineerās problem
You could take a look at the render debug displays Aevyrie made this cycle, and use color in a similar way
Just override the color of all of the materials to show volume, with some way to control which emitters you want to show
Imo this is a niche and noisy enough visualization that trying to do it as an overlay is a mistake: it'll either be distracting and irrelevant or hard to focus on
yeah probably so
Minimal repro as promised. Let me know if you aren't able to run it, but it should work https://github.com/mgi388/bevy_seedling_observer_repro
And bevy_seedling tracking issue to investigate: https://github.com/CorvusPrudens/bevy_seedling/issues/81
Alright, I've diagnosed the problem. Thanks for the repro! The OnComplete::Remove behavior was removing the SampleEffects relationship, but that orphans the effects. They then become expensive, because when they're orphaned, they're added to the audio graph (they're no longer merely POD used for an effect).
Clearly, the entities related by SampleEffect should be despawned with OnComplete::Remove. This indeed fixes the issue.
If you're wondering why the SamplePlayer entities have any effects at all -- the default pool has a single VolumeNode effect in the default configuration. You can see that here. This allows you to easily dynamically control the volume of individual sounds if you want.
The overhead in terms of audio processing is almost nonexistent if you don't use the volume (Firewheel allows nodes to be skipped if they know they'll produce no effect). But if you don't want the default pool to have an effect, you can choose a more minimal configuration.
I'm not sure why this same behavior wasn't triggered by the other deferred approach, but maybe that wasn't using OnComplete::Remove.
Nice work, and nice turnaround time thank you! I probably wonāt get a chance to look until tomorrow.
Just wanted to say that I confirmed it fixed the problem, thanks again.
@ionic sedge should the spatial_basic example (and other spatial examples) spawn their player with SpatialPool (currently they don't)? Or do you think it's enough that if a user is dealing with spatial sounds and pools, it's advanced enough that there's no point having it in the examples and they'll work out what they need?
I figured people looking to the examples would want the absolutel minimum viable path to achieve what they want.
Not providing a pool is one less thing to think about. That of course means a dynamic pool will be created, which experienced users may not want. My assumption is that experienced users will realize this and be more principled about where they play their samples if they care about that (and in fact may remove the dynamic bus entirely to prevent accidentally creating new pools).
My broader assumption is that people will need to take time to build up a mental model of how bevy_seedling works, either because they're not familiar with audio, the ECS, or simply the API surface of the crate.
My hope is that dynamic pools make it very easy to get started -- they mostly "just work" after all.
Yep that's all good, and makes sense. I thought I'd just check if it was an oversight in the example or not.
And yeah, building up a mental model of pools and nodes is still something I'm missing and trying to grow. Not to say it's because seedling is lacking, just because it's something that takes time to read and absorb all the docs, etc.
yeah it took me about three days before i really understood what was going on
at least
@ionic sedge when using volume.fade_to(Volume::SILENT, ...) is there a recommended way to check when the fade is done apart from checking volume.volume == Volume::SILENT? Do you know if it would be safe to rely on this check or do we need some sort of function on it like volume.is_basically_silent() (name obvs to be bikeshedded)?
Yes, that's a distinct limitation of the current animation system. We could (quite easily) implement executing arbitrary systems within the AudioEvents. Maybe that's a good idea, since a nice ECS-first animation system may be a long time coming.
We do have "near zero" methods though on volume.
You could do something like volume.amp_clamped(0.01) == 0.0
@ionic sedge I've managed to migrate my Bevy 0.17 app off the kiras and bevy_audio fully to bevy_seedling. The only main outstanding "blocker" is the (apparent) bug in bevy_seedling about not being able to stitch music samples together seamlessly (https://github.com/CorvusPrudens/bevy_seedling/pull/77).
It's a pleasure to use the crate. The docs are extensive which is great. So thanks! There's still a lot to learn and a lot I don't know. And I suspect some of my usage/requirements are relatively basic so I am probably not exercising that much.
Other minor but notable improvements that would help / have helped are some missing entity Names (maybe, it's debatable if they should be added to sample effect things) and Reflects.
The entity names are useful to see all the bevy_seedling related entities in the inspector rather than 100s of Entity (56).
The reflects are important for debugging when using an inspector and I found it harder to navigate the relationships/graph to understand what was linked to what. After I patched these locally, this helped.
Yeah you've made issues for both those, right?
I think they're both reasonable, and a feature for adding names is probably reasonable, although they shouldn't contribute too much to binary bloat either way.
@ionic sedge my solution for music fade out was this. Spawn a node and link it to my Music entity.
commands.spawn((
Name::new("Music fade out volume node"),
VolumeNode::default(),
FadeOutVolumeNodeOf(music_entity),
));
When spawning a new sample, link the fade out node to it:
let sample_entity = commands
.spawn((
MusicSample { scheduled_start },
MusicPool,
events,
SamplePlayer::new(sample.clone()).with_volume(MUSIC_SAMPLE_VOLUME),
settings,
))
.id();
// Samples are children of the music entity.
commands
.entity(music_entity)
.add_child(sample_entity);
commands
.entity(fade_out_volume_node.0)
.insert(EffectOf(sample_entity));
This means I can leave the MusicPool volume node for controlling the music volume separately to any fade out happening.
And it's required to have a separate node to the samples because the samples are short-lived (I'm stitching them together).
If this is enough context to make sense and it is how you imagined it, great, this is here for others (there was at least one other question a few months back about this). If it's "wrong", feel free to say.
This is kind of obvious but: Having an inspector was really helpful when doing a PoC on this music fade out node. I could just drag the volume of the fade out node in the inspector and see that it was able to control the volume of all the samples within my music entity, but then separately also drag the volume of the MusicPool to show that that could also be controlled separately.
Oh yeah I've mused a few times now that being able to visualize the audio graph (i.e. in a node graph editor) would make the crate so much easier to understand! I eagerly anticipate the official Bevy editor.
I suppose this runs the risk of having the fade out node get despawned when the sample player completes (unless you've adjusted the OnComplete settings).
It doesn't seem to be getting despawned and I am using with_on_complete(OnComplete::Despawn).
Are you looping the music?
Nope
Each sample is like ~6s or something and it plays to completion, then the next sample starts, and so on.
š¤·
Don't expect you to look at it, but https://gist.github.com/mgi388/e26081c236d34d353fe167eb8dce94e6 is the code if it's clearer.
Hm, well either way I'd probably write it like this myself:
let sample_entity = commands
.spawn((
MusicSample { scheduled_start },
MusicPool,
events,
SamplePlayer::new(sample.clone()).with_volume(MUSIC_SAMPLE_VOLUME),
settings,
sample_effects![(
Name::new("Music fade out volume node"),
VolumeNode::default(),
FadeOutVolumeNodeOf(music_entity),
)],
))
.id();
// Samples are children of the music entity.
commands
.entity(music_entity)
.add_child(sample_entity);
In other words just making it a bit more declarative.
Though you could end up with more than one FadeOutVolumeNodeOf I suppose.
Oh it's a 1-to-1 so it should be fine.
Yeah 1-1
(Correct me if I am wrong) but your suggestion doesn't work because spawning that new sample gets a new VolumeNode, and so if you are fading out over 10s but the samples are only 6s, the next sample will no longer be fading out because it's volume is 1.0 (because of VolumeNode::default()).
Oh I see you're maintaining the fade over different samples. Hm, and is this equivalent to fading out the whole music pool? In other words, are other samples being played in the same pool at the same time that should not be fading out?
- Yep you need to maintain fade out over different samples within the same
Musicentity. - There could technically be multiple
Musicentities (unlikely, but let's say there is). - So you need a fade out volume node per
Musicentity.
In other words, are other samples being played in the same pool at the same time that should not be fading out?
So yep.
Ah that's too bad. If that were not the case, you could just add a single volume node in series with the whole pool, which is nice.
But yeah breaking up the fade would be annoying otherwise. Frankly, I'm surprised that works, but I guess I can't complain haha.
For completeness, I could edit the MusicPool like this:
commands
.spawn((
SamplerPool(MusicPool),
Name::new("Music Sampler Pool"),
sample_effects![VolumeNode::default()],
))
.chain_node((
Name::new("Music Pool Fade Out Volume Node"),
VolumeNode {
volume: Volume::Decibels(-6.0),
..Default::default()
},
));
But because I need to support multiple Music entities at once, this didn't work.
just add a single volume node in series with the whole pool, which is nice.
My code above is this, yes?
ya
Frankly, I'm surprised that works, but I guess I can't complain haha.
Ha, I did sort of worry about this. Maybe one day it won't.
Once I broke out of the sample_effects![] macro and edited EffectOf directly I got a bit worried.
To some extent that's the nature of ECS APIs. You can often use them in ways the original author doesn't intend or anticipate.
If I was prudent I would add a unit test for this use case so it's protected.
If you want to do it "correctly," you could also transplant the AudioEffects. The fade is basically "rasterized" in there.
In other words, take it out of the old entity and insert it into the new one. But that's a bit annoying.
You can also mitigate the potential removal in other ways. You should be able to unrelate it in the case of any PlaybackCompleteEvents firing.
Before it's despawned.
Yeah glad you mentioned that. I started down a similar but more hacky path where I was going to track the current node's volume and then when spawning the next node I was going to insert the volume node of that one with the volume value from the prev sample.
In other words, take it out of the old entity and insert it into the new one. But that's a bit annoying.
Recall I also use scheduling to make it seamless and so the next sample entity is spawned before the current sample finishes and so I'd need to reach into the current entity before it's actually finished playing.
Well, a gap in the fade will manifest either way. It's likely that you just won't hear it.
The reason is that using the same entity in this way doesn't literally mean it's the same node in the graph. To support this would require re-compiling the audio graph every time a new sample is played. (This isn't that expensive, but ideally it can be avoided).
So what this is really doing is just bringing over those audio events to a new node. That means we should expect to see a very short flat spot in the fade. But this will likely be completely imperceptible in all circumstances.
Well, actually the flat spot may be avoided. But this method of using the same entity isn't strictly necessary to achieve that. It's functionally the same as taking the AudioEvents and moving them to a new entity with the same volume.
We might need to consider an API that communicates this difference more clearly.
// This spawns a `VolumeNode`. At the end of the frame, barring
// any other changes to the entity, `VolumeNode`'s
// audio processor will be inserted into the audio graph, with
// an automatic connection to the main bus.
commands.spawn(VolumeNode::default());
// This spawns a `VolumeNode` related by `EffectOf`. That means
// it will not be inserted directly into the audio graph. Rather,
// we simply use its value to update some other `VolumeNode` entity
// in the ECS. That other one, which exists in the pool, is what
// represents the actual processor in the graph.
commands.spawn((
SamplePlayer::new(server.load("my_sample.wav")),
sample_effects![VolumeNode::default()],
));
I suspect this is too implicit however.
The former is relatively rare. It's not often you need to spawn nodes like this on their own. So we could require an explicit component like ProcessorHandle or something that indicates this entity represents an audio processor.
Anyway, all of this trouble comes from the fact that while you can schedule samples precisely relative to each other (once we fix the issue anyway), it would be convenient if you could target the same sampler. That way, the fade out doesn't need to jump around or anything.
Queuing up samples like this is very common, so maybe the best path forward for this use case is to explicitly support this.
Yeah an explicit API that does the right thing here makes sense to me
Just to confirm. The precise scheduling issue is not actually related to your suggestion that it would be nice to be able to target the same sampler, right?
For example, a composite sound effect made up of 5 samples that play one after another may not need precise scheduling, but one could still want to have a fade out that ācoversā the whole sound effect (just as my music fade out ācoversā the whole music) hence just checking whether precise sampling is related to ātarget the same samplerā?
a composite sound effect made up of 5 samples that play one after another may not need precise scheduling
Technically this is true if the audio processor supported it. But Firewheel's sampler is already quite complicated and it supports one sample at a time, so ideally we wouldn't need to modify it. Maybe we'll revisit this though.
In other words, my first approach would be to set up an API in bevy_seedling that uses precise scheduling internally to queue up samples.
This of course requires the precise scheduling to be precise, so we'll need to fix that.
Doing a fade simultaneously would be supported with this, and would be much more convenient because the sampler wouldn't change..
(Hopefully this addresses your question!)
hiya. just checking when bevy 0.18 support should roll out for seedling?
The main blocker right now is how we'll handle the backend changes to Firewheel. There's a short path forward which isn't as ideal but could be done in just an hour or two, and there's a longer path that may take a few more days.
I suppose we should just take the shortcut for now, so I'll see if I can coordinate with BillyDM.
cool.would be great. thanks much.
either way i think it would be really good to have a 0.18 release for the jam ready since i imagine most will use seedling so the web builds have good audio 
word
Oh for sure! It'll absolutely be ready well before the jam.
whens the jam start?
Feb 15
bevy game jam?
Yep, see #jam š
fun
when 0.18
Soonā¢
Likely the next day or two.
i think the delay node in firewheel is correct outside of that weird egui specific issue. (though could use a review). is that something that would be good to get in the version change or does it not really matter (im guessing this is mostly to keep up with the bevy release)
it would definitely be great! im pretty sure just adding a new module and types isn't breaking semver though, so it doesn't necessarily need to be added with the next breaking publish
If egui is getting mutable access to the notify, that may be enough to trigger the update on that
although it doesn't look like that's happening
yeah sorry to keep bringing that bit up lol. ive spent like hours trying to figure out what is going on specifically with that example and have resolved that its simply cursed
i would love to get this in though
i might able to take a look later today to see if i can spot anything
oh one thing i'd say is can we give the EchoNode a default CHANNELS?
i.e. pub struct EchoNode<const CHANNELS: usize = 2>
It would be a bit tedious to have to explicitly provide them when the vast majority of the time it'll be two channels
ah yes totally forgot you can default const generics. will update that when i rebase
Oh yeah, I forgot you can do that too. I should do that for Firewheel's built-in nodes.
oh hm so i guess its limited in use since you still need to specify types if you wanna fill in with ..Default::default() afaict, but is still useful to have the default when constructing like SomeNode { .. }. I like how you added the type aliases as well, so adding that to the echo node.
One way to address this is to only implement Default for stereo only, or alternatively provide a new for stereo only.
I tried the former but im unsure if its worth the tradeoff of making non-stereo unable to use the ..Default pattern which I assume will be pretty common when setting up nodes
I like the new idea
i wonder if even just mono and stereo make sense
presumably you'd still have a Default implementation that's generic over the channels
yeah, that makes sense to me, i can try that out on echo and see if i run into anything that seems weird. in terms of getting people set up easily, i also really like providing a new(mono/stereo) fn that exposes all the settings that you'd probably want to adjust anyways. might feel a bit less daunting than filling in a whole struct
@next otter, I have a branch up that includes the cpal version tick you requested without a Bevy 0.18 migration here: https://github.com/CorvusPrudens/bevy_seedling/tree/firewheel-0.10.
This will serve as the starting point for the 0.18 update, which I'm hoping to complete today. There will likely be small breaking changes as I resolve some of the remaining issues.
The major changes included in this branch are described in the change log.
if we go the mono/stereo fn route, curious to know what the preferred fn would look like. i feel like calling out each channel is a bit easier to understand since not everyone knows L/R matches 0/1, but also its pretty verbose
impl EchoNodeMono {
pub fn mono(delay_seconds: f32) -> Self {}
}
impl EchoNodeStereo {
pub fn stereo(delay_seconds_left: f32, delay_seconds_right: f32) -> Self {}
}
impl EchoNodeStereo {
pub fn stereo(delay_seconds: [f32; 2]) -> Self {}
}
ok actually i convinced myself that the array is a bad idea due to L/R being 0/1 being implicit knowledge
ya i do think a different arg for each channel is probably better
hm ok @ionic sedge not to bikeshed too much but im wondering if something like new and new_mono might be easier to find than stereo and mono and represent default use better (though I like the consistent pattern of the latter a little more). but im kinda on the fence.
though also thinking, nodes that don't have channels could also use the new pattern as well which would be more consistent, since stereo/mono might not make sense in every node context.
i was combing through the nodes to see if that pattern makes sense, and i feel like in general it could be nice to have a somewhat consistent "new" fn (whatever it is named) for nodes. e.g., FastBandpassNode has the fn from_cutoff_hz which is accurate, but also it is the main way I think the node will be constructed since most users wont bother with smooth seconds or the coeff update factor. It may be more discoverable to apply naming there too ("stereo"/"mono"/"new"/whichever).
@ionic sedge Just a heads up on a breaking change I just made. In regards to this issue https://github.com/BillyDM/Firewheel/issues/100, I realized it would make more sense to have load_audio_file take an Option<NonZeroU32> for the sample rate instead of a NonZeroU32.
I think they mean they want to also know what that sample rate is. That is, the function should return the sample rate or give some other information about the original rate!
For example, the SampleResourceInfo trait could provide the original sample rate, and possibly the resampled sample rate if itās different.
The only other way to get this information would be to know ahead of time (not very practical for large projects) or duplicate work in bevy_seedling with symphonia probing.
Oh, I see. I'll work on that then here soon.
@ionic sedge is right. I use the original sample rate in a calculation for random pitch changing / varying.
Separately I do know bevy_seedling has its own support for that, but the game assets Iām using rely on the original sample rate to calculate this.
Ok. I realized I need to actually change this in my Symphonium library. That's ok, because I was planning on adding another breaking change to Symphonium anyway.
Ok, now it's done! https://github.com/BillyDM/Firewheel/commit/54a595a8fa530a6117d158a0ff205fd28c99b8eb
I also made it to where you can probe metadata of an audio file before decoding it. https://docs.rs/symphonium/latest/symphonium/struct.SymphoniumLoader.html#method.probe_from_file
any plans for 0.18
I'm in the final stages of publishing a new version. Should be up today.
cool
@ionic sedge food for thought re calling the āentity namesā feature ādevā. I personally donāt love the idea of a āgrab bagā of less granular features all in a ādevā feature. One may want to enable entity names in release but not every dev feature. That said I know thereās many ways to skin a cat and everyone has preference on level of granularity so just my initial reaction.
I think the feature collection design makes a lot of sense for this
I figured it aligned with bevy's dev feature, which among other things enables (internal) names for components. I also decided to feature-gate only the automatic audio node names, leaving the rest untouched since their cost is effectively zero.
I think more granularity is reasonable, although I'm not sure I want to break it out quite yet. I'd probably set up something like this:
dev = ["node_names", "other_dev_stuff"]
node_names = []
other_dev_stuff = []
Since node_names is maybe a little non-obvious and dev seems to be getting more popular in the ecosystem (see bevy_new_2d).
Although I suppose it would be a less disruptive change in the future if I break it out now.
By that you mean, for example, the ādevā feature collection enables ānode_namesā, ādebugā and āreflectā features?
Yep!
Yeah maybe I guess Iād do whatever would be closer to bevy proper? One less thing to change in any great upstreaming.
Yeah, doing it now will make integration slightly easier
Ah, well I suppose it also makes sense to insert names on sample players too for this feature. I may go with entity_names in that case.
sorry if im spamming ya just wanted to make sure it wasnt lost @ionic sedge š did you happen to have any opinions on this?
To be honest, it seems a little tricky and nothing came to mind as obvious.
I admit new_mono and new_stereo would probably be more idiomatic than just mono and stereo. It would also be more cohesive with the rest of the nodes if they had simple new methods.
But should we pursue that design? Maybe. It's not that uncommon to see
BigStruct {
item_one: 10.0,
..Default::default()
}
in Bevy code, though. Bevy structs (and many in the ecosystem) also frequently have builder-style consuming methods for setting fields. Those require a mild but annoying amount of boilerplate to add.
yeah that is rather common. in this case it would need to be EchoMonoNode/EchoStereoNode and doing the above wouldn't work on the generic version obv, if that is fine.
though arguably people may likely reach for the generic EchoNode and construct it in that way & need to specify a const
Can you not do:
EchoNode {
some_field: 10.0,
..EchoNode::stereo()
}
is that dubious?
I suppose it ends up a little awkward if you want to provide the delay amounts.
EchoNode {
delay_seconds: [0.5, 0.5],
..EchoNode::stereo()
}
I don't mind that - I kind of like the notion of making stereo or mono default impls instead of new fns, that usage is a bit more intuitive. another option i was thinking is to not expose the raw echonode in the prelude, only the aliases
oh yeah hmm
yeah actually creating an echonode in general could be confusing from that arrangement since it relies on array order even outside of the stereo/mono builder convo. though i suppose that can always be called out in doc comments
it's tricky innit
This is my main problem with nodes that use generics for their channel count. It becomes a huge UX pain.
It would be nice if we could erase the Node and return a monomorphized channel count in construct_processor. That would require double-boxing with the current API though, and you would need a fallback dynamic processor for channel counts that you don't specifically handle.
i.e.
fn construct_processor(
&self,
config: &Self::Configuration,
cx: ConstructProcessorContext,
) -> impl AudioNodeProcessor {
match self.confic.channel_count {
1 => Box::new(Processor::<1> { /* */ }), // mono
2 => Box::new(Processor::<2> { /* */ }), // stereo
4 => Box::new(Processor::<4> { /* */ }), // quad
8 => Box::new(Processor::<8> { /* */ }), // 7.1
_ => Box::new(DynmaicProcessor { /* */ }),
}
}
neat, that looks like a better experience
Then again, you don't necessarily get guaranteed maximum performance in the processor if it's not among the bespoke arrangements.
(I honestly don't think it's too big of a deal but
.)
Of course, you'd still need something like mono and stereo constructors, so it wouldn't remove that aspect.
it would also be nice not to need a separate DynamicProcessor though I imagine i can reuse basically everything and its just the shape of the processor that changes a bit
Given the multiple channel-dependent fields in EchoNode, this sort of erasure may not even be much of an ergonomic win. An erased node could have incorrect collection lengths.
I do think it would be a win for simpler nodes though. Certainly for bevy_seedling at least, where every type has to be registered.
yeah registering every generic is not ideal
maybe this is too radical... but what if echonode was just stereo, and there's a separate echomononode of a separate type, and channels above stereo are not supported?
im completely unsure if this sort of thing would realistically be used above stereo in the real world. perhaps the current impl is trading robustness for ergonomics in a way that isn't actually that valuable
Personally I'd be very opposed -- I don't think it's worth denying other channel configurations, at least not yet.
In principle we could support arbitrary channels with combinations of mono and stereo nodes. There would be a bit of overhead compared to a single node, scaling with the number of channels.
cool, so it sounds like the erased nodes work for simple nodes that dont have channel num specfic params and then otherwise we'll probs need some fn like mono/stereo. and those fns can either take parameters, or act as a default for that channel configuration.
That seems reasonable for the time being!
@ionic sedge latest from github is working as expected with bevy 0.18.0. thanks
There's also a new version up on crates.io just in case you're not aware.
@ionic sedge Finally got around to reading the seedling docs and examples! (And I actually understand Bevy more this time to know what is going on.) Pretty sweet! It's definitely neat seeing a higher level API on top of Firewheel's API!
The dynamic pool feature is pretty clever! I might consider making an optional abstraction like that at some point which other game engines could use.
Oh nice!
Yeah I've gone back and forth on whether dynamic pools are a good idea even just pedagogically. You could probably argue it makes things "just work" a little too much haha.
But I bet it'll really help a lot of people just get their ideas off the ground. I wouldn't want people to get bogged down in the details of how pools work.
It would also be nice to have an optional API that is more "traditional" with busses and some prebuilt configurations to choose from that would accommodate most games. Though that could also be a separate crate.
Hm, kinda like this? https://docs.rs/bevy_seedling/latest/bevy_seedling/configuration/enum.GraphConfiguration.html
Or like, all the way in terms of API?
I wonder if you could build such an API on top of bevy_seedling instead of completely separately 
Though on the other hand, I have learned from creating Firewheel that creating a nice-to-use and well-documented API that you expect other people to use is a lot of work. š So I should probably hold off on that, or just let someone else create one.
I'm sure we've also talked about building a traditional bussy GUI on top. That's certainly very achievable once the editor rolls around.
Yeah, prebuilt configurations kind of like that
Is that a typo, or do I just not know what a "bussy GUI" is? 
you know.... like a GUI with busses and tracks xD
oh no it crashed gimp
i wanted to zoom in on the kid's face
mfw they rope me into a promo with la bussi
he probably had to smile a lot that day š
And of course there is another meaning to the word which came to mind first. 
okay probably not the best word to describe a gui with buses
gooey bussy
(Sorry not sorry)
oh no, BillyDM! The blasphemy
@ionic sedge is firewheel-web-audio still the way to init seedling in web or is just enabling bevy_seedling/web_audio enough now?
I'm getting weird panic on web
Cannot allocate an Asset Handle of type 'bevy_seedling::sample::assets::AudioSample' because the asset type has not been initialized. Make sure you have called `app.init_asset::<bevy_seedling::sample::assets::AudioSample>()`
The web_audio feature uses firewheel-web-audio internally, but you do still need to call SeedlingPlugin::new_web_audio() or otherwise provide the web audio backend.
I generally do this with a feature for my projects like:
#[cfg(not(feature = "web"))]
app.add_plugins(SeedlingPlugin::default());
#[cfg(feature = "web")]
app.add_plugins(SeedlingPlugin::new_web_audio());
I have some plans that should remove the need to do this in the future.
Oh, I guess my run commands messes up the setup: bevy run web has no way of providing features feature flag should be before web - hence feature web is not used
hmm, I'm getting seedling error on init in browser:
āØāØ```
Failed to initialize Web Audio backend: "Error: creating audio worklet instance: JsValue(InvalidStateError: AudioWorkletNode constructor: Unknown AudioWorklet name 'WasmProcessor'
...backtrace...
log.target = "firewheel_web_audio::backend";
log.module_path = "firewheel_web_audio::backend";
log.file = "/home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/firewheel-web-audio-0.4.0/src/backend.rs";
log.line = 306;
versions
bevy = 0.18
bevy_seedling = { version = "0.7", features = ["hrtf", "web_audio"] }
What's your rustc and wasm-bindgen version?
but I only see it on itch, let me check what versions I see there
āØ```
nightly-2026-01-20-x86_64-unknown-linux-gnu
wasm-bindgen-cli v0.2.108
hm, those look good
and you checked the āwasm sharedarraybufferā checkbox on itch? i assume so of course since this follows an update, but im just checking!
sorry to disturb, it seems there are more (including wgpu panic), will debug further
Are you aware of the linker flags you need for shared memory (wasm multi-threading) as well in rustc 1.94+?
these ones I presume - yeah I have them
āØ```
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"]
it's changed a bit
see this PR in the cli: https://github.com/TheBevyFlock/bevy_cli/pull/728
# .cargo/config.toml
[target.wasm32-unknown-unknown]
rustflags = [
"-Ctarget-feature=+atomics",
"-Clink-arg=--shared-memory",
"-Clink-arg=--max-memory=1073741824",
"-Clink-arg=--import-memory",
"-Clink-arg=--export=__wasm_init_tls",
"-Clink-arg=--export=__tls_size",
"-Clink-arg=--export=__tls_align",
"-Clink-arg=--export=__tls_base",
]
Although you should see a clearer error than what you reported if this is the problem.
yeah these used to be automatically enabled when +atomics was enabled
@ionic sedge just checking in regarding the "seamless" bug. I assume you've just been busy and haven't had a chance to look at it yet? I'm hoping the repro is useful and ultimately it doesn't come down to user error š¬
Yes, sorry for the delay on it! I know it's a big problem, and I suspect it may not be user-error. Certainly we want to get it fixed soon -- it's really important for the audio working group as a whole that it works properly!
I can't say when I'll be able to dive into it though. I should have time before the bevy jam, in particular since I want to (selfishly haha) have it working for my team.
No need to apologize! I really appreciate all your efforts and the entire crate. Just thought I'd check in.
Hello. Trying to figure out if bevy_seedling would be better than kira for what i'm trying to achieve with spatial audio. I do text-to-speech on sometimes large text, tts is done on each chunk. I want to play each chunk in sequence, and have a keybinding to cycle through the list of audios.
The spatial examples are really cool.
Looks like i'd need to overwrite the entity's SamplePlayer when i want to change which chunk of audio must be played.
No you can move the playhaed if you want. https://docs.rs/bevy_seedling/latest/bevy_seedling/sample/struct.PlaybackSettings.html#structfield.play_from
Sample parameters that can change during playback.
something like:
*settings.play = true;
settings.play_from = PlayFrom::Seconds(0.5);
Ok but that would be helpful if there was just one audio sample that i want to seek.
I have n audio samples attached to the entity, and i want to cycle between them.
Sort of like a playlist
Yeah if you insert a new SamplePlayer it'll start playing the new one.
Ok, great.
Thanks.
bevy_seedling looks great
Really impressed by the hrtf example
There's no way to set a radius for the audio source though, is there ?
Searched for "radius" in the docs.
There's spin_radius in the examples
Ok it moves the entity in a circle i suppose
There are quite a few options for adjusting how the basic spatialization works. The simplest is the SpatialScale component: https://docs.rs/bevy_seedling/latest/bevy_seedling/spatial/struct.SpatialScale.html
Cool
You can adjust a bunch of parameters directly in the SpatialBasicNode https://docs.rs/bevy_seedling/latest/bevy_seedling/prelude/struct.SpatialBasicNode.html
Though note that bevy_seedling automatically writes to the offset field, so manual changes to that will be overwritten.
One more thing i wanted to ask, can you have both the bevy_kira_audio and bevy_seedling plugins loaded at the same time ?
99% sure you can. I was doing this while migrating off BKA.
Cool, thanks.
Some more praise for bevy_seedling and firewheel -- I had been maintaining a custom audio manager on top of a custom bevy_kira_audio as well. This crate is sooo much easier to use!
Some issues though (to follow)...
Some feedback on the spatial audio support for 3D. Even with the 2D example, it was a bit confusing to set up in a working way when adapting it to a "real world" scenario.
The examples and docs make it look like one would spawn a SamplePlayer, Transform, and the SpatialBasicNode effects together in order to make each sample player behave spatially.
But in a more complex scenario, the pattern doesn't hold. I had started from Jan's foxtrot conversion to bevy_seedling PR for reference and dutifully created several pools, one of which is an Sfx pool:
commands.spawn((
Name::new("SFX"),
SamplerPool(Sfx),
sample_effects![(
SpatialBasicNode::default(), // I forgot this was here
)],
// ...
));
and, on top, I was spawning effects like
commands.spawn((
Sfx,
xfrm,
sample_effects![SpatialBasicNode::default()], // WRONG!
SamplePlayer::new(fx.myeffect.clone()),
// ...
));
So in essence there were two SpatialBasicNodes in play, one from the effect and one from the pool.
The end result is that the Sfx samples played at the same volume no matter what. I'm not sure why.
So, the fix, of course, was to remove the per-sample SpatialBasicNode.
(I do see that when I invert the scenario -- removing SpatialBasicNode from the Sfx pool, but keeping it on the sample -- I get a somewhat more useful message:bevy_seedling::pool::queue: Queued sample "sounds/..." with effects in an effect-less pool. )
Another bit of feedback is, my project has several SamplerPools and I want to selectively be able to mute and un-mute them while maintaining the user-configured volume in between such changes.
I didn't find an obvious way to do this, but of course, I am a beginner and may have missed something. As it is, one has to clear VolumeNode::volume to do muting, then loses the original setting when un-muting. I worked around it like so:
/// This drives the volume from the user config point of view.
///
/// Our [apply_volumes] system ensures that a corresponding VolumeNode matches
/// the volume and muted state.
#[derive(Component)]
#[require(VolumeNode{ volume: Volume::SILENT, ..default() })]
pub(crate) struct UserVolume {
pub volume: Volume,
pub muted: bool,
}
///
app.add_systems(PostUpdate, apply_volumes)
/// Apply mute-able UserVolume to VolumeNodes.
pub(crate) fn apply_volumes(
mut vol_q: Query<(&UserVolume, &mut VolumeNode), Changed<UserVolume>>,
) {
for (user, mut vol) in vol_q.iter_mut() {
vol.volume = if user.muted { Volume::SILENT } else { user.volume };
}
}
VolumeNode could arguably have a muted bool to make this work more easily for end users.
adding muted to VolumeNode seems reasonable. in the absence of that, you could chain VolumeNodes and use one for muting and the other for user-configured volume
Yeah to expand on what @sick copper said, you can add nodes wherever you want! So an easy way to mute a pool without adjusting its primary volume would look like:
commands
.spawn(SamplerPool(Sfx))
.chain_node((
VolumeNode::default(),
SfxMute
));
fn mute_sfx(mut muter: Single<&mut VolumeNode, With<SfxMute>>) {
muter.volume = Volume::SILENT;
}
Not sure what you ran into with the spatial audio -- I don't think there's quite enough context to determine the root cause. Did you make sure the sample player had a transform?
There should be no difference whether you provide an effect in the pool's template or directly when spawning a sound.
To be clear, the error situation seemed to be having multiple SpatialBasicNodes -- one on the SamplerPool and ones on each an independently launched entity with SamplePlayer (yes, with Transform) and also with a SpatialBasicNode targeting that pool.
That's the intended usage. If you don't provide one when spawning a SamplePlayer, it'll be cloned from the pool's template instead.
That is, the two should be equivalent.
The end result in both cases should be the same:
// given a pool like this
commands.spawn((
SamplerPool(Sfx),
sample_effects![SpatialBasicNode::default()],
));
// this
commands.spawn((
Sfx,
SamplePlayer::new(server.load("my_sound.wav")),
));
// and this
commands.spawn((
Sfx,
SamplePlayer::new(server.load("my_sound.wav")),
sample_effects![SpatialBasicNode::default()],
));
// should result in en entity like this
(
Sfx
SamplePlayer
SampleEffects [
SpatialBasic
]
)
Of course it's no good if that's what you did and it didn't work! Feel free to provide more context to see if we can get to the bottom of it.
I'm trying to narrow down the situation without a consistent answer, but more clues.
To explain the test case, I have a Camera3D with a SpatialListener3D that starts very far away from a noisy entity that is rapidly playing these short (0.35 second) samples, many times a second. (Think, in-world sound effects.) At the given distance (> 300 m), I shouldn't be able to hear anything.
When I tried playing a much longer sample (a 15 second full-amplitude sine wave), then for some of the SamplePlayer instances, the sound starts loud for a few ticks then quiets down (per attenuation) soon after. When I try a one-second wave, I hear only a few of them.
ā
This situation also occurs when there is only the one SpatialBasicNode in the SamplerPool rather than on each entity. So that confirms your assertion that there should be no difference.
While watching the bevy_inspector_egui view with the bevy_seedling/dev enabled, it seems I would hear the incorrectly attenuated sample when a new SamplerNode was created. (Perhaps a full pool mitigates the issue?)
A random guess about the cause (which I ran into in my own 3D audio prototype) was the fact that the sample might start playing before its effects and spatialized attenuation have been fully computed? I.e. it seems as if spatial::update_3d_emitters_effects might be running a frame too late? I was spawning stuff in PostUpdate. If I move to PreUpdate, I hear the incorrect volume less often, but it still occurs.
I'll work on a repro case.
It might be related to the smoothing we do. Feel free to make an issue on the repo! I think it's likely this is something we need to address.
The error will seem to disappear if all the nodes in the pool already start at the target distance, so that's why you'd see it go away after a moment.
Yup, noted that in the report š
hmm.... adding a sample player with PlaybackSettings::default().paused() should start with things not playing, right? is it still compatible if the audio is looping? tryna figure out why mine is playing immediately
This works for me I believe:
// Start the audio paused.
PlaybackSettings::default()
.with_playback(false)
.with_on_complete(OnComplete::Remove),
thanks! cool yeah i must have something funky goin on
double check that youāre not using bevy_audioās PlaybackSettings! We donāt have a paused method.
Are there any known bugs with index-out-of-bounds?
Two of my playtesters got these and I remember seedlings had something with index out of bounds a couple of months back (it can't be from my own code because I never access indexes unsafely):
The giant number is -1 of u64
parry isn't in the seedling tree
i can't imagine that's related to our code, at least directly
@heady robin Parry is physics, most likely you depend on it through avian/rapier (they currently both use it)
Oh sorry
where did you see that it's parry
ohhhhh
Sorry to bother then, off to avian I go
Thanks for the help
I've made this for having a looping player, probably could be better (note, complete bevy noob here trying to meet jam deadline). Any hints are welcome!
Got some helpers like
#[derive(Asset, TypePath, Deserialize, Debug, Clone)]
pub struct LoopData {
loop_start_seconds: f64,
loop_end_seconds: f64,
}
#[derive(Component)]
struct ScheduledLoopStart {
last_start: InstantSeconds,
loop_data: Handle<LoopData>,
}
then I can start like
#[derive(Event)]
pub struct StartLoopedSampler(pub Name);
fn start(
target: On<StartLoopedSampler>,
time: Res<Time<Audio>>,
mut commands: Commands,
loops: Res<Assets<LoopData>>,
samples: Res<Samples>,
) {
let Some((sample, d)) = samples.0.get(&target.0) else {
return;
};
let Some(data) = loops.get(d) else {
return;
};
let loop_start_time = time.delay(DurationSeconds(data.loop_end_seconds));
let mut events = AudioEvents::new(&time);
let settings = PlaybackSettings::default();
settings.play_at(None, time.now(), &mut events);
settings.play_at(
Some(PlayFrom::Seconds(data.loop_start_seconds)),
loop_start_time,
&mut events,
);
commands.spawn((
target.0.clone(),
events,
settings,
SamplePlayer::new(sample.clone()).with_volume(Volume::Decibels(0.0)),
ScheduledLoopStart {
last_start: loop_start_time,
loop_data: d.clone(),
},
));
}
Oh that's funny I was moments away from posting a gist
repeat the loop
fn repeat_loop(
mut commands: Commands,
query: Query<(
Entity,
&mut ScheduledLoopStart,
&PlaybackSettings,
&mut AudioEvents,
)>,
time: Res<Time<Audio>>,
loops: Res<Assets<LoopData>>,
) {
let now = time.now();
for (player, mut scheduled_start, settings, mut events) in query {
let Some(data) = loops.get(&scheduled_start.loop_data) else {
continue;
};
if now >= scheduled_start.last_start {
let loop_duration = data.loop_end_seconds - data.loop_start_seconds;
let next_start = scheduled_start.last_start + DurationSeconds::new(loop_duration);
settings.play_at(
Some(PlayFrom::Seconds(data.loop_start_seconds)),
next_start,
&mut events,
);
scheduled_start.last_start = next_start;
commands.entity(player);
}
}
}
and stop
#[derive(Event)]
pub struct StopLoopedSampler(pub Name);
fn stop(
target: On<StopLoopedSampler>,
mut commands: Commands,
mut query: Query<(Entity, &Name, &SampleEffects), With<ScheduledLoopStart>>,
mut volume: Query<(&VolumeNode, &mut AudioEvents), Without<SampleEffects>>,
) {
let Some((entity, _, effects)) = query.iter_mut().find(|(_, name, _)| **name == target.0)
else {
return;
};
let Ok((volume, mut volume_events)) = volume.get_effect_mut(effects) else {
return;
};
volume.fade_to(Volume::SILENT, DurationSeconds(1.0), &mut volume_events);
commands.entity(entity).remove::<ScheduledLoopStart>();
}
hehe, I am sure there is stuff to improve on this so I am still curious about that gist
at least this helped me start getting some understanding about the crate
this is essentially what i wrote, though i didn't include any start and stop ceremony
https://gist.github.com/CorvusPrudens/c27695704c9a8a21615db372450455d3
nice, checking the playhead instead of the absolute time is neat
Mine also doesn't start at the beginning of the loop point.
Both of these implementations technically have a limitation; loops that are shorter than the frametime won't be reliably observed. But that can be solved by just scheduling more in advance with slightly more robust code. If your loops are always longer than a few frames it should be fine.
Using the absolute time is also probably a better idea 
makes sense! I guess ideally the looping would be implemented on a lower level instead of in ECS? luckily right now my loops are longer than the frame delta
thanks for the help btw!
Yeah to guarantee it always works, it needs to happen in the actual audio thread.
The sampler is written by BillyDM (Firewheel maintainer), and it's already rather complicated, so progress on loop regions has been a little slow. I think we should be able to implement a simple loop region feature for it though.
while this runs fine natively, I am getting this error (and no sound playing) when testing my game on wasm
ERROR /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy_seedling-0.7.0/src/node/mod.rs:792 graph error: MsgChannelFull
any pointers?
is there something special I need to do to work with wasm?
my deps are like
bevy_seedling = { version = "0.7.0", default-features = false, features = [
"reflect",
"ogg",
"rand",
"web_audio",
] }
and I've put
const audioCtx = new AudioContext();
function resumeAudio() {
if (audioCtx.state === "suspended") {
console.log("audio context resumed")
audioCtx.resume();
}
window.removeEventListener("click", resumeAudio);
window.removeEventListener("keydown", resumeAudio);
}
window.addEventListener("click", resumeAudio);
window.addEventListener("keydown", resumeAudio);
into my index.html, just in case (not sure if this is needed at all)
You only need the web_audio feature if you want multi-threaded audio (which I highly recommend!)
You don't need to mess with the audio context (and either way the audio context in that snippet wouldn't be accessible to the Wasm), but you do need to make sure the SeedlingPlugin is using the WebAudio backend. You can do so with the new_web_audio method.
You're using Bevy CLI, right?
I don't but I'll try
building with new_web_audio gets rid of the error but the game produces no sound
i am on rustc 1.95.0-nightly
ya it's a pre-requisite -- the real requirement is atomics on Wasm, which the multi-threading flag does in the CLI
It's a pain to set up manually -- without the CLI you need:
# .cargo/config.toml
[target.wasm32-unknown-unknown]
rustflags = [
"-Ctarget-feature=+atomics",
"-Clink-arg=--shared-memory",
"-Clink-arg=--max-memory=1073741824",
"-Clink-arg=--import-memory",
"-Clink-arg=--export=__wasm_init_tls",
"-Clink-arg=--export=__tls_size",
"-Clink-arg=--export=__tls_align",
"-Clink-arg=--export=__tls_base",
]
If you don't want to bother with all of that, the Bevy CLI also has a default index.html that likely resolves the error you were seeing.
It uses some clever JS to start the audio context properly.
(bevy_seedling's web audio feature doesn't need this JS, but the default setup does)
no luck with bevy cli either
I'll try more later because it is painful to wait and see my laptop build bevy for wasm and go like this
it seems that the issue was with the http server I was using, running the build files on itch works fine
thanks for the assist!
hey! im running into some weird behavior and wanted to confirm im doing things right.
#[derive(NodeLabel, PartialEq, Eq, Debug, Hash, Clone)]
pub(crate) struct MusicBus;
#[derive(PoolLabel, PartialEq, Eq, Debug, Hash, Clone)]
pub(crate) struct MusicPool;
....
cmd.spawn(SamplerPool(mixer::MusicPool))
.connect(mixer::MusicBus);
....
fn start_playing_bgm(
mut cmd: Commands,
game_assets: Res<GameAssets>,
music_volume: Single<&mut VolumeNode, With<mixer::MusicBus>>,
) {
let mut sampler = SamplePlayer::new(game_assets.music_a.clone());
sampler.repeat_mode = RepeatMode::RepeatEndlessly;
cmd.spawn((sampler, MusicPool));
dbg!(music_volume.volume);
}
So, I get a value of 0.2 for the volume (manually set) and it doesn't seem to affect the playback volume of the clip, which is always full volume 
(i also have like 3 other busses & a spacial one, if that is relevant)
oh, strange. Even if I don't specify a pool and directly add a volume node it still has no effect. no clue what i'm doing wrong there
what's the music bus look like
oh also be careful about shadowed pool and bus labels
If you create a label with the same name as one from bevy_seedling, it can be easy to accidentally use the wrong one.
This even happened to my own jam team! So I might have to do some adjusting. Seems like people frequently create their own labels with identical names.
hm, is there some other setup? its just the cmd spawn above
ah ill check that. is music bus one that exists?
actually i can just check when i get back!
well, a node label like "MusicBus" is just a node label, meaning there needs to be a node there like a VolumeNode that acts as the bus
i'd also expect it to log an error tbh
and if it doesn't it probably should
yes and the fact that the system qualifies MusicBus but not MusicPool has me worried! shouldn't it also be mixer::MusicBus?
If you've specifically imported it then it'll use it, but if not it may be falling back to bevy_seedling's * import
yeah okay that was at least part of the problem
is there a reason why there is a default musicpool but not a default musicbus?
yeah i just figured it didn't need a bus since there's only one related pool
the sound effects bus has two pools connected to it by default
oh so youd just query for the musicpool and adj the volume there?
ya
okay that makes sense, ty
any idea on why wasm-bindgen could be failing here?
if I run bevy run web the game builds/runs fine but without audio
but when I try bevy run web -U multi-threading to enable the thing it fails with
thread 'main' (3205289) panicked at /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-cli-support-0.2.106/src/interpreter/mod.rs:209:17:
__rustc[dde6fad95d619c59]::rust_begin_unwind: Condition failed: `address % width == 0` (4 vs 0)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: failed to run wasm-bindgen, trying to find automatic fix...
error: command `wasm-bindgen --no-typescript --out-name bjam --out-dir /home/user/bjam/target/wasm32-unknown-unknown/web --target web /home/user/bjam/target/wasm32-unknown-unknown/web/bjam.wasm` exited with status code exit status: 101
iād recommend posting in the working group thread: https://discord.com/channels/691052431525675048/1278871953721262090
iāve never seen this error
If I had to guess, Iād say that something is expecting the code to be compiled in 64-bit mode
but itās in 32-bit
Since it says address % width is 4
I would be surprised if this issue has anything to do with seedling, although itās not impossible. More likely itās an issue with the way the build and/or wasm runtime is configured
Maybe multi-threading is only supported by your browser in wasm64 mode (for some reason) but itās still being compiled with the wasm32 target? Maybe try another browser or explicitly set the target to wasm64-unknown-unknown?
itās wasm bindgen failing, not the binary
Oh my bad, well that makes it even less likely to be related to seedling but explicitly using wasm64 might still fix it
thanks, I'll do that once the jam ends
Hi all, I migrated to Seedling + Firewheel for the jam, and it was relatively painless! I was able to delete a lot of custom audio code stacked on top of a fork of bevy_kira_audio. That said, I ran into two difficulties along the way and filed reports on those (#87 and #91).
What's left is a general concern about the developer experience related to SamplerPools and effects. I think this concern has come up before, and the docs explain how it works reasonably well, but IMHO it's not quite intuitive how the pool's audio nodes are replicated into SamplePlayer entities. Indeed, it feels almost the reverse of what I'd expect.
I'll make a thread for this... (oh, apparently I can't?)
I'll explain what happened to give an idea of how a new user might (and did!) misunderstand things.
For my specific case, I had set up a Music pool like so:
commands.spawn((
Name::new("Music"),
SamplerPool(Music),
VolumeNode::default(),
));
The VolumeNode allows me to provide user-configurable volume for all music. It worked fine.
I started out playing music in that pool as usual, with commands.spawn(Music, SamplePlayer::....). That was fine. Its volume was driven from the pool volume as expected.
The issue comes from later trying to add a fade-in effect. (I did not use VolumeFade due to #91.)
To do this, on the client side, I "simply" added VolumeNode::default() to the SamplePlayer's entity and added a system to drive that. I.e.:
commands.spawn((
Music,
SamplePlayer::new(music_sample).looping(),
VolumeNode::from_linear(0.) // I know it's not in sample_effects![]
));
then I had a system that (tried to) drive the VolumeNode::volume over time. The volume was not affected through I confirmed the changes were being made to that node on that entity. There were no warnings in the log.
I know this is wrong, now, but given the symmetry with how the SamplerPool is defined, it seemed obvious, i.e. "To override the volume for a player, add a VolumeNode to the entity, like it's done for the pool."
I soon found from docs and usage that I needed to use sample_effects![] to add the VolumeNode. Fine. But then that led to no audible change, but now at least I saw the logged warning WARN bevy_seedling::pool::queue: Queued sample "music/song0.ogg" contains one or more effects that the pool does not.
That seemed odd -- I already had a VolumeNode on the pool. "How was it working for the pool then?" But I muddled through and I was able to do the fade-in through queries indirected through SampleEffects.
-->
So in the end, to make it all work, I needed:
commands.spawn((
SamplerPool(Music),
VolumeNode::default(),
sample_effects![
VolumeNode::default(),
],
));
and
commands.spawn((
Music,
SamplePlayer::new(music_sample).looping(),
sample_effects![
VolumeNode::from_linear(0.)
],
));
etc. etc.
This is where the developer experience question comes in. I can get behind the idea of sample_effects! and wrapping effects in a relation. (Though maybe a warning about adding effects as siblings of SamplePlayers would be useful.)
But the thornier question is, as asked by a less experienced seedling user, is, why does the pool need to have the same effects the sample player might want? Do I have to list every possible anticipated effect in the common code I use to set up a pool? It seems backwards. I would expect to be able to freely add effects on a sample-by-sample basis, such that the subgraph for the sample has its own effects overriding and augmenting those in the pool.
Otherwise it seems one would need to generate a pool per set of potential effects, which might lead to a lot of pools with slightly different setups and potentially overallocated PoolSizes across the world, negating the benefits of pooling in the first place.
This may just come down to naming. For me, "pool" sounds like an allocation abstraction, not a "template", as it seems to be in practice. Thoughts?
(as a Bevy maintainer I am extremely grateful for this testing and feedback)
I agree that there are a number of papercuts in the API right now. I should put more work into communicating incorrect usage, such as inserting a node on a SamplePlayer entity, or trying to route between something other than nodes.
Anything more starts to become questionable in my opinion. For example, we could have a pool builder type that prevents you from setting it up incorrectly. But this erects barriers in understanding the shape of a pool in the ECS and gives people more than one way to construct them.
This is part a broader problem in the space of ECS-first APIs which I don't have a great solution for. Tooling would help a lot in my opinion -- the ability to inspect the shape of things like pools at runtime could build understanding much faster than attempting to abstractly reason about it.
With better correctness enfocement, you'd likely arrive at your big question much more quickly.
why does the pool need to have the same effects the sample player might want?
Actually, if you want this functionality, you can get it right now! Just omit the pool component when you spawn SamplePlayers and see what happens.
That is, we do have dynamic pools. They come with some trade offs, but I designed them to make it easy to slap things down without needing to be careful about pool management.
One of those trade offs is performance.
A big reason we do sample pooling is because it allows you to dynamically play sounds without recompiling the audio graph. This is part of how we achieve a ~7x tighter performance distribution than rodio and kira. They are indeed a tool for managing allocation, and their role as a template is essential for this pre-allocation.
Even if you had a lot of different pools, they would succeed in preventing recompiles as designed.
In practice, I don't expect many permutations to show up for most projects. At least, I haven't run into any sort of explosion yet. For one, many effects can be disabled or bypassed, or simply set to values that don't affect the sound by default. Due to Firewheel's design, these disabled effects are essentially free.
(For example, here's the enabled field of the SvfNode: https://docs.rs/firewheel-nodes/latest/firewheel_nodes/svf/struct.SvfNode.html#structfield.enabled)
Right, I understand pools being used as allocation limiters, and that's something I appreciate since I had to hand-code it in the past. What threw me is the (apparent) fact of needing to pre-plan and pre-allocate for future potential effect nodes on the pool itself. I see the effects as "part of" the sample player. Like in my large pool of "Sfx" I would expect to be able to arbitrarily make a sampler entity with a volume effect, a sampler entity with a reverb effect, something with doppler, etc.
I.e. I think it would be fine (maybe with caveats or warnings that could be disabled with other components) to create a new firewheel subgraph based on the pool and a given entity sampler's unique combination of effects, as long as the pooling aspect were maintained.
I'm sure I will dive more into this and learn more. I'm trying to provide "new user" feedback for cases which others are likely to run into š
Oh, I see what you mean by allocation in this context. That is, there's a limited number of voices in the pool, meaning you can only play so many samples at once.
I was using the term in a broader sense. Changing the shape of the graph, such as for a unique set of effects for a particular sampler, is also a kind of allocation.
I see the concerns, but is there really that much expense to rebuilding the graph in user time vs. the ECS work that the client code already needed to invoke to spawn entities in the first place? I.e. if a sound effect is spawned a few times a second?
Rebuilding the graph incurs a penalty in the audio thread, which we have to be much more careful about.
We can do a lot of work in the ECS though -- that's no problem!
Sure, I understand and agree about that. Does rebuilding the graph need to traverse the entire world of AudioNodes rather than e.g. grafting on a new subgraph for a new entity?
The core of the recompilation happens outside the audio thread, but when the compiled graph is sent to the audio thread, the processor has to update buffers and move things around (or drop things).
I don't have a precise intuition on the work required -- @static quest wrote all the graph code. He might have some insight.
That is, we do have dynamic pools. They come with some trade offs, but I designed them to make it easy to slap things down without needing to be careful about pool management.
Thanks for reminding me to look. I looked into this originally and avoided it for one major reason -- I am using the pools for two major reasons: (1) limiting the total number of active sounds and (2) unified volume / muting control. It seems (1) may be covered, but if all such sounds go to DynamicBus, it would involve a little extra effort to propagate user volume/muting settings to that bus instead of to a pool (as for music/UI sounds).
I suppose a deeper understanding of the graph structure would provide a solution for that. I'm imagining e.g. some entity with VolumeNode that acts as the sfx volume controller, to which these dynamic samplers link, before/instead of the dynamic bus? But it seems the usage would be less symmetric to the other pools.
Yeah, you'd have to fall back to volume nodes per-sample, maybe with a marker component. Certainly possible, but not as nice.
A suggestion (again with 5% understanding of the API surface area):
Perhaps a friendly way to support dynamic effects with sampler pools (like how I'd want) is some DynamicEffects marker on a SamplerPool which lets the user say "I accept responsibility for the potential expense of rebuilding the audio graph in exchange for being able to easily have custom effects on my samplers that incorporate those in the pool"? Then seedling would do the expensive but user-directed work like pool::dynamic does.
Then, later, when/if performance optimization is needed, users know what component to look for and can refactor the effects down into the pools. (And conversely if users don't have this marker and make an error in usage by attempting to add effects, you can recommend adding this marker to the pool as a convenience.)
(I'd like to maintain common mostly-immutable code for an audio backbone -- pools, volume/muting UI, etc. -- without moving unexpected/asymmetrical parts of it per game -- the effects pool here -- or doing the manual VolumeNode routing as we discussed above.)
What is their question about specifically?
What is the cost of graph recompilation on the audio thread? Does it scale depending on the size of the changes?
But yeah, the pool API was specifically made because graph recompilation is both expensive, and because there is no way to schedule graph updates to be sample-accurate.
I imagine the actual compilation part algorithm isn't that expensive (we are only working with graphs with a few dozen nodes). The main expense is allocating the audio buffers (and deallocating the old ones). Though I suppose I could try and add logic to try and reuse the old allocation if the number of buffers hasn't grown. I just haven't bothered since realtime graph updates was never an intention anyway.
But yeah, another thing is that the graph itself is not part of the event scheduling system, so there is no way to schedule "graph updates".
And if the nodes you are adding/removing also need to allocate/deallocate data, then that will also factor in to the performance.
If you want to dynamically apply effects, then the intended way is to just enable/disable the effects you want in the sampler pool.
It helps to think of it more in terms of a "DAW" audio engine (which is what Firewheel was inspired on after all). In a DAW you don't add/remove plugins for every single sound you play. You set up the graph beforehand and handle everything with events.
But yeah, I understand that the "Sampler Pool" concept isn't the most intuitive, especially if you are coming from Rodio.
@ionic sedge The most optimal way might be if bevy_seedling had a way to let users to choose all of the effects they would want beforehand, and then bevy_seedling creates a sampler pool with all of those effects. Then bevy_seedling would dynamically enable/disable nodes for the given worker depending on what is added to the ECS thing.
That might require a common "bypass" parameter that is common to all nodes. But that can be implemented with the AudioNode trait or something.
That makes sense (re: performance and allocations and proper use of the pool), and I think we're on the same page, so I'll just close off with a view that makes it easier to be adopted by new users.
I'd argue that if this project becomes the default audio system for Bevy, only a handful of users will be interested in or aiming for the sample-accurate workflows when learning it. It's great that it's possible to be sample-accurate but I'd prefer that be the (slightly) more difficult path, while allowing new users a more flexible playground where they can mess around.
I.e. today I just care about spawning sound effects in response to certain user events. Even in the "slow" kira world, I never noticed any significant delay for something to start playing since it was all in user time š
Since seedling and firewheel are two projects, I think it presents a good way to split the responsibilities between frontend and backend. bevy_seedling as a "user/frontend" API could allow for flexible use of the ECS and take pragmatic steps to make sense of a haphazard mash of pools and samplers and dynamic effects. It can warn the user or provide markers like I mentioned to silence any performance warnings during prototyping/testing. But its job would be to present firewheel these graph changes at the right time, without sacrificing performance guarantees for programs that take care to properly preallocate effects and pools.
I see the ideal situation akin to how I can toss a ton of Mesh3D/MeshMaterial3D into a scene, and the rendering plugins take care of sorting and culling and all the rest. I don't need to adhere to a certain allocation order or material usage count or anything. If I go overboard, FPS tanks, but it didn't prevent me from getting there. As a Bevy user, I don't want to / need to care about the lower level data structures or the performance in extreme cases until my project gets complicated enough to care -- until then I just want something that works that can tuned later.
Thanks for hearing me out! I look forward to future work š
Or actually, bypassing nodes could be handled by the Firewheel processor itself, with no extra work needed on the node author's part.
I'm not necessarily opposed to opt-in performance. To some extent, bevy_seedling is already designed like this. If you want a perfectly predictable graph with no runtime changes, you can opt into it by giving pools an empty range for their size (like 32..=32). By default, they start smaller and grow if they reach capacity.
What is the default pool size that bevy seedling uses?
The default range is 4..=32, meaning pools start with four voices.
They grow quadratically, so recompilations should be infrequent.
Ok, cool. I just wanted to make sure that you weren't spawning pools of size 32 by default. That would be a lot of unused nodes for the majority of use cases.
ya that was my thinking
If we had in-place processing, then that would greatly reduce the processing overhead of having a lot of unused nodes. That would take some changes to the graph compilation algorithm though.
(Although Firewheel does quite well with a ton of nodes :3)
If even a node is bypasses/not doing anything, the graph processor still has to copy all the input buffers to the output buffers. If we had in-place processing, then we wouldn't need to do this copy operation.
i do really like the API as-is though
oh unless it doesn't change the processor api
in which case i have no opinion
We could make in-place processing opt-in for nodes. Still, it wouldn't be the most trivial task to implement. If I find the time and inclination I might look into it.
True. And modern CPUs are pretty fast at copying small-ish buffers. (1024samples * 4bytes_per_sample * 2channels = 8192 bytes copied per stereo in/out node).
I guess if you were trying to run Firewheel on more esoteric hardware it might be more of a problem.
Like how a 386 SX-20 couldn't play 4-channel mod files š
(Though you probably aren't running complicated audio effect setups on esoteric hardware anyway.)
One reservation I have with a more dynamic API (aside from performance as we've discussed) is disrupting people's mental model.
Evidently, it already takes some time to build a mental model for bevy_seedling. I'd argue a pool's SampleEffects functioning as a template for its players is fairly simple. But if they instead functioned as a kind of default set of effects that can be arbitrarily reconfigured, what sample_effects! is doing becomes even more opaque.
It's possible of course that this is not an accurate assessment of how newcomers would feel about such a feature. Maybe it's already enough magic that this wouldn't make much of a difference.
FWIW, part of my confusion came from a meaning of "template" as used in e.g. IDE template projects, being "something you start with then expand. A "prototype" might be a different take but still not quite the same as e.g. Javascript where you can indeed add stuff.
Also, a stumbling block in the understanding is that the examples are a bit too simple to build on, and the docs for https://docs.rs/bevy_seedling/latest/bevy_seedling/pool/struct.SamplerPool.html make it seem like it's "just" a way to limit sounds and provide effects. E.g. Pools with effects will automatically insert SampleEffects into queued SamplePlayers.. I added in my mind (which wasn't true) "... along with other effects you may have". So maybe saying explicitly "Each SamplePlayer targeting a SamplerPool may only override the effects already present in its pool. Otherwise use the shared dynamic pool ."
A component for building sampler pools.
oh i see
I was trying to update seedling to 0.19 tonight as a test to see how feasible it was to maintain a 0.19.0-dev fork and made it pretty far wrt updating firewheel, rtgc, etc. but firewheel-ircam-hrtf is having an issue with the glam31 flags I think, where Diff and Patch are not implemented for glam::Vec3.
Is this something y'all have run into before with the collection of glam flags? (output here if you want to look: https://gist.github.com/ChristopherBiscardi/34db99240d54a6e2bd65016cde9b471d ). I've checked for the usual things like duplicate glam versions, etc but there's a decent amount of crates so it is possible I just missed something.
Hm, has glam received another update?
Ah, and we're using 0.32 on main?
Yeah this is really annoying. Orphan rule. We figured it would be fine to feature flag the minor versions, but that's also annoying.
If you're updating Firewheel itself, I suppose you could add another flag. glam has more frequent breaking changes than I thought it would, and it's quickly becoming rather unwieldy.
we're only on 0.31 if my local copy is correct
yeah, bevy_render is still on 0.31 on main afaict: https://github.com/bevyengine/bevy/blob/08a3f24deca5f07b699c674ddf1633a25b4853af/crates/bevy_render/Cargo.toml#L110
so I believe that the current flags should suffice. Maybe I have something misflagged... but if I did I think cargo would warn me
I'll take another crack at it tomorrow. I think I'm most of the way done so just need to figure out where the gap in glam31 is
That's definitely a bit odd.
cargo tree gives me all three versions, but only 0.31 is actually installed. the other two are empty because the flags aren't enabled afaik
Ah, looks like glam-31 is actually just lacking an impl
oh sweet haha, that's an easy fix
thank you š
cool, I'm through into actual seedling 0.19 updates now. thanks again š
doesn't look like anything interesting so far, just the reflect re-org causing churn, Replace -> Discard, and one missing component annotation that might end up being related to the resources as entities pr
awesome, thanks for getting it going! feel free to PR the changes
although the sprawling nature of the crates is kind of annoying š
sure I'll throw it up for you after I get it all working. Probably un-mergable though since I'm targeting main
I'm happy to just put the draft PRs up in the various places though
Hey jsyk I put up a PR to fix a bug I was hitting in my game, itās a small fix so if youāre planning a minor release soon it might be worth including https://github.com/CorvusPrudens/bevy_seedling/pull/92
@static quest I have branches for the chain of crates (firewheel, rtgc, etc) related to: https://github.com/CorvusPrudens/bevy_seedling/pull/93
Do you also want draft PRs like this one for visibility? They're against bevy/main right now, so wouldn't be mergable until later, and I don't want to bother you if you don't want them sitting there during the 0.19-dev cycle.
Yeah, sending some draft PRs make sense.
ok will do
I take it back, 0.32 merged today š
Sorry lol!
I'm trying to get bevy_seedling to work with asio (for jackrouter only available with asio on windows I believe) anyone have tips on getting it to work with asio instead of wasapi?
do i have to change from cpal to rtaudio?
got it working
stream_config: CpalConfig {
output: CpalOutputConfig {
host: Some(HostId::Asio),
device_id: Some(DeviceId(HostId::Asio, "JackRouter".to_string())),
..Default::default()
},
input: None,
},
..SeedlingPlugin::default()
}```
and cpal = { version = "0.17", features = ["asio"] }
I just noticed when playing button sound effects sometimes the sound plays quietly, sometimes it plays at normal volume. Is there anything that stands out that would cause this that would be user error?
Even if I allow a sensible duration between when I click the button it happens (I think it does happen more if I click really fast but can't quite tell). But normal button clicking seems to play them really quietly sometimes.
I checked my sounds have the same playback settings (i.e., it's not like I'm randomly setting a different volume or playback speed).
I am using the game config and I haven't put these sound effects on any particular pool.
Hmm hard to tell. That issue talks a lot about spatial. These are just UI button SFX without any spatial on them specifically (though I do have spatial running in this scene).
It does talk about smoothing though, so maybe related/same issue.
True, but I suspect this issue may be due to smoothing in general. If the sound effects are very short, it may end up playing largely with some previous level.
Yeah the sample is super short. It's a button click SFX and the duration is like...00:00 š
Hm, what if you set smoothing to zero on all the volume nodes in that pool? Obviously we don't want that for a long-term solution, but it might help confirm the issue.
Give me a sec, I'll try it. smooth_seconds -> 0.
Didn't seem to help. Local change I made follows. Proved the log ran:
2026-02-25T03:07:03.412413Z INFO bevy_seedling::configuration: Configuring the graph using volume node: VolumeNode { volume: Linear(1.0), smooth_seconds: 0.0, min_gain: 1e-5 }
Started work on my project again recently and in case I haven't already said this, I wanted to say that you've all done a fantastic job with seedling/firewheel and it's become even better since I last touched it. Hope to see it upstreamed at some point.
Hmm, that is definitely odd. What happens if you set SamplerConfig::num_declickers to 0?
@dark sonnet In regards to #87, what happens if you change this line to if true { ? https://github.com/BillyDM/Firewheel/blob/cee51f897a9806bd3c27f3184a84f642832e00db/crates/firewheel-nodes/src/spatial_basic.rs#L319
@ionic sedge Are the UI elements using SpatialBasic or VolumePan?
If they are using VolumePan, then what happens if you set this line to if true { ? https://github.com/BillyDM/Firewheel/blob/cee51f897a9806bd3c27f3184a84f642832e00db/crates/firewheel-nodes/src/volume_pan.rs#L233
Hmm, that change just causes all spatial audio to be forever silent, whether or not I manually adjust the SpatialBasicNode::offset (which seems expected given the original test)
Just VolumeNode I believe, if youāre referring to mgiās issue.
It didn't help, no change (maybe worse, hard to say). I proved that the setting was 0 using this:
fn print_sampler_config(q: Query<&bevy_seedling::prelude::SamplerConfig>) {
for config in q.iter() {
error!(
"Current SamplerConfig.num_declickers: {:?}",
config.num_declickers
);
}
}
Logs showed: ERROR engine: Current SamplerConfig.num_declickers: 0.
I did this test still with Corvy's suggested change in BTW:
let mut vn = VolumeNode::default();
vn.smooth_seconds = 0.0;
Hmm, that is strange.
Just trying to repro it in the bevy_seedling settings example using this sample.
let source = server.load("846145_6512859-lq.wav");ājust drop this inside the play_sfx function as the source.
cargo run --example settings_menu
I can repro it using this sound effect. It's more subtle than my own sample, but nonetheless I can still hear it go quieter on occasion. This was still with the two tests in (smooth_seconds 0 and num_declickers 0).
Ok, I'll look into it when I'm finished with this other thing.
Alright, I finally got iced_baseview working! Now I can focus on Firewheel. https://codeberg.org/BillyDM/iced_baseview
Which branch of bevy_seedling, the master branch?
Yep this is on master / same as bevy_seedling 0.7.0.
Ok, yeah, I am able to reproduce it. Time for every programmer's favorite activity, debugging! š
Phew, I'm glad you can reproduce it though. You got this!
Oh, I'm already cluing in on what's happening. It appears that for some reason, the playhead isn't always at 0 when playing a sample.
@ionic sedge Are you doing anything on your end that might be causing that? Like are you scheduling the events with a specific timestamp when they should be played immediately?
All events are currently scheduled to the time at the beginning of the frame. I think this is a good idea personally, but another decision has turned out not good by default; scheduling playback for sounds to occur as if they started when initially spawned.
That is, if loading a sample delays playback, it'll be scheduled such that the playhead matches where it should be had it been ready. The idea being that this would guarantee correct timings in all cases.
However, most people don't need precisely timed sample playback, and it's actually quite bad when asset loading takes a while, such as in Wasm. You can see this in our jam game where a lot of dialog gets cut off at the start because it has to first stream the file. (This effect may be less noticeable if you have quite fast internet.)
However, I'm not sure why that would affect the volume.
Oh ok, yeah then I suspect what's happening is that you are scheduling the events too early, and by the time the sampler gets the event, that timestamp has already passed. Don't forget there is about a 20ms worse-case latency for audio buffers of length 1024. For samples that should just "play immediately" at the lowest latency possible, you definitely just want to send an unscheduled event.
It effects the volume because in order to start playing a sample from a playhead other than 0, the volume has to fade in to declick (if you didn't do that, then there can be a very audible click).
Scheduling is more for scheduling a sequence ahead of time.
You should always prefer non-scheduled events for the lowest latency, unless the game has a specific use case for scheduling like cutscenes or musically-synced stuff.
Or say, playing "rapid fire" effects like playing a bunch of gunshot samples at a very specific interval.
Though does that mess with bevy seedling's design too much?
No, I went out of my way a fair bit to make it so. I had some more reasons, but don't have time to go through them atm. I believe I was worried in part about general correctness. In any case, I can investigate removing the scheduling by default. If that resolves some of these issues, that would be very convenient.
You could also try disabling the "play in the past" feature (if there is an option to disable that, I can't remember.) Still, for the best latency, non scheduled is preferred.
I would say scheduling makes sense if you are automating a parameter.
@ionic sedge Hi, is it possible to play quite literally nothing on the audio channel on purpose?
ya like silence?
I have a WASM build, but as you are probably aware, its super strict about audio
it requires user input and all that stuff
yeah that part I'll preserve with the somewhat basic automation curves we have currently in bevy_seedling
And when you first play a sound with seedling, i assume it creates the audio pipe right there and then. However, it creates an ugly glitch sound in the first half a second, probably trying to catch up. Can we maybe intentionaly output "nothing" on the sound channel at startup, so that the pipe on WASM initializes before we push actual audio?
Yeah I can try to play wav with silence in it, but I think there might be a better solution to this that this hack
Ah, well the root cause here could be a few things. Worst case scenario, I think a better hack would be to simply ramp up the volume on the main bus over 250ms or so at startup.
If you need a solution now, you might not have too much trouble writing a node that does this yourself!
However, I'm sure we'll want to get to the bottom of this a bit more properly. It may have to do with the compensation I'm doing as I mentioned above, which we'll shortly axe.
FYI, https://github.com/bevyengine/bevy/pull/23126 got merged š
Awesome!
yea i think i have had weird issues forgetting a specific bevy feature almost every time so this is huge for me
There's a follow-up ready to split off UI too if you wanna take it š
I don't know enough about opting out of bevy_ui to know what to put where, sorry 
I mean, I can do a best guess effort, but I feel like someone who actually uses Bevy with disabled bevy_ui will know better than me what's actually needed
KK, I can grab this tomorrow
This is awesome! Iām wondering if Iām doing things wrong though, currently Iām disabling Bevy's default audio by removing it from the default plugins (and using all the default features). Is there a reason to disable it with features other than compile times?
Conflicting imports in the prelude
also it'll be slightly slower to compile, but bevy audio is pretty small
Ah yeah that makes sense, I don't use crate preludes anyway so that explains why that didn't occur to me (I prefer explicit imports and don't mind relying on the LSP to insert them)
I ended up being busy with other things this weekend. I'll get to this either today or tomorrow!
And binary size
cpal is failing here, but I couldnāt say why 
Darn, I haven't been feeling well these past couple of days. Hopefully I can get back to working on Firewheel soon!
Been a while since Iāve been active here, is there a good way to communicate back with the main world yet? Worst-case I can just use mpsc, but if thereās a better way that has a proper API (and doesnāt involve adding an extra point of potential cache contention between main and audio worlds) that would be nice if itās available
Specifically I want synchronisation info, in case thereās an API for that but not a generic communication system
I donāt mind just using mpsc, I mostly donāt want to feel like an idiot when I find a better way down the line š
I had an EGD procedure today, and it went fine! The doctor didn't find any serious issues, so he suspects is probably just IBS or less likely a bacterial infection. I was already kind if trying a low Fodmap diet, but the doctor said to try and follow it more strictly, and to also try staying away from dairy altogether.
Oh yeah, and he said to try smaller more frequent meals.
If you are talking about sending data from the audio thread to the main thread, the proper way to did it is either with atomics, and lock free spsc buffer (like the ringbuf crate), or a triple buffer (like the triplebuffer crate).
Yeah Iām pretty familiar with lockless programming for audio (itās what I do as a job), itās more about whether thereās some prebuilt API to do it that's already integrated with the ECS etc. Actually funnily enough, literally just last week I wrote a lock-free ring buffer to communicate between an audio and main thread for a programming interview
Ok, I'm feeling well enough again to work on Firewheel!
Hi! Is it possible to change the audio output of the game at runtime ?
Yes, check out the select_outputs example.
oh nice, sorry I did not see it before asking. That's exactly what I needed
Thanks
what you can just query OutputDeviceInfo ? 𤯠That's insanely convenient
I was expecting a world of suffering
It is a push-style setup though, so if a new device may have been connected, you'll have to trigger a FetchAudioIoEvent before it'll be up-to-date.
i see. Still, it's super cool to have these things in the ECS world directly
It's a little barebones now, but we expect to significantly improve it in the near future.
Okay nvm making this menu will not be that simple haha. I must find a way to filter things out š
wow that's a lotta devices
yeah....
uh
I have to check with windows, maybe it's a linux thing
I don't think that apps in windows can access each output
might also be your particular backend, i believe mine was a bit cleaner
(though im driving macos at the moment)
I use pipewire with alsa
almost all the outputs given are actually the alsa output themselves
edit: all of them
I believe these devices are coming straight from cpal in this example, so I believe you'd need to filter them out one way or another.
aw man, sticking to default audio is not that bad in the end
cpal's direct API may give you better means to filter, it's been a minute since I've used it myself.
I also use pipewire with alsa but mine certainly do not look like this. AMD thought, so maybe here lies the rub
I have a soundcard with a bunch of outputs and inputs, and different modes, as well as a motherboard with lots of outputs..
Looks like itās really listing everything. Like each individual HDMI port (5 total.. š )
I think they can with asio4all but not with the default driver but that might have changed, last time I did RT audio on windows was like 8-10 years ago
@ionic sedge Alright, here is the new audio backend API! Let me know what you think! https://github.com/BillyDM/Firewheel/pull/107
oh awesome! i should be able to dig into this properly tomorrow at the latest
I'm looking for a way to send sounds to individual sound card outputs, seems like seedling doesn't have this built-in and I would need to look more towards firewheel?
got it working!
config: FirewheelConfig {
// Expose all 32 ASIO channels on AudioGraphOutput so we can route
// to any output pair (e.g. channels 3-4 = ports 2-3).
num_graph_outputs: ChannelCount::new(32).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 bypasses auto-connect to MainBus since connect_with inserts PendingConnections.
commands
.spawn((
SamplerPool(Channels34Pool),
))
.connect_with(AudioGraphOutput, &[(0, 2), (1, 3)]);
// Play the sample in the custom pool by adding the pool label component.
commands.spawn((
Channels34Pool,
SamplePlayer::new(server.load("sample.wav")).looping(),
));```
@ionic sedge Derp, my hard clip algorithm was wrong. š https://github.com/BillyDM/Firewheel/commit/5eafe56a9f1f6d3d435fa0d9593205bdb45c70f5
