#bevy_seedling

1 messages Ā· Page 6 of 1

static quest
#

I would expect it to not work without those imports in no_std.

#

Nevermind, now it is failing to compile. This is weird.

static quest
#

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.

short fossil
#

@hoary gust may I get bevy-steam-audio now? šŸ™‚

#

need an owner invite on crates-io

static quest
#

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

lapis stone
static quest
lapis stone
#

ahh

static quest
#

Ok, Firewheel 0.9.1 is published!

#

Oh yeah, we need to get the issue with clack sorted out.

lapis stone
hoary gust
#

might be a bit buried, idk if they expire?

short fossil
#

Think it may have

short fossil
#

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

ionic sedge
#

this is like the ruby takeover

hoary gust
#

I'm gonna steal it back

#

Setting up a bot to make another with mixed - and _

lapis stone
#

@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.

lapis stone
heady robin
#

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?

lapis stone
ionic sedge
#

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.)

heady robin
#

Luckily, this is Bevy, so I can just query for them

#

Man, I love this engine

static quest
ionic sedge
lapis stone
#

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

ionic sedge
#

libm is the standard here (which is what num traits uses in no_std contexts) and i believe it's perfectly stable across platforms

lapis stone
static quest
#

(But you would need to create a libm features that enables the libm feature in num_traits)

heady robin
lapis stone
# static quest Yeah, either method would work.

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.

mystic epoch
#

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.

mystic epoch
#
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 }],
    ));
ionic sedge
#

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.

mystic epoch
#

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 🤷

ionic sedge
#

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!

mystic epoch
#

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

ionic sedge
#

i do wonder why children! never seemed to trigger this

mystic epoch
#

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

ionic sedge
#

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).

mystic epoch
#

tbh this does kinda feel like clippy being a bit too trigger happy

autumn crypt
#

Is there an easy way to change the pitch of an audio clip?

ionic sedge
#

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).

ionic sedge
autumn crypt
near dagger
#

is there docs for this project yet?

near dagger
#

has oneone done a tutorial on spatial sounds?

ionic sedge
near dagger
ionic sedge
#

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.

near dagger
#

is the steam audio using seedling or working with it?

ionic sedge
#

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!)

near dagger
#

rad.

#

thanks. yeah i will start with seedling

near dagger
#

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?

near dagger
#

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
ionic sedge
near dagger
#

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);
        }
    }
}
ionic sedge
#

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.

near dagger
#

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.?

ionic sedge
#

Yeah that should avoid it entirely.

near dagger
#

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`
ionic sedge
#

Oh yeah that's just Rust stuff -- try

mut volume_nodes: Query<&mut VolumeNode>,
near dagger
#

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);

ionic sedge
#

Okay @near dagger feel free to cargo update bevy_seedling -- 0.6.1 should resolve the issue you ran into.

cedar bison
#

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)?

ionic sedge
cedar bison
#

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.

ionic sedge
#

Yeah it won’t be triggered on looping samples unless you stop them.

cedar bison
#

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).

ionic sedge
cedar bison
#

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.

ionic sedge
#

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.

frosty folio
#

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)

ionic sedge
#

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 blobthink.

#

As for the cracklies, is this running with optimizations?

frosty folio
#

yeah

#

if i just read from the channel directly (to eg. draw a bar in my terminal) with no bevy involved, everything's fine

ionic sedge
#

Is the channel fixed capacity?

frosty folio
#

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

ionic sedge
#

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.

frosty folio
#

oh yeah. realtime spsc channel that auto-resamples sounds real nice

static quest
#

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 ^

ionic sedge
#

As in the stream node?

static quest
# ionic sedge 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.

ionic sedge
ionic sedge
#

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.

obsidian tusk
#

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

static quest
#

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.

obsidian tusk
#

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

obsidian tusk
obsidian tusk
obsidian tusk
static quest
#

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.

obsidian tusk
#

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

trim belfry
stoic igloo
#

It’s Non Send

#

It might need more
maturing though.

obsidian tusk
tribal garden
#

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 šŸ™‚

ionic sedge
# tribal garden Hi everyone, I am looking for advice on implementing a rhythm game with Bevy/Rus...

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.

tribal garden
#

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.

ionic sedge
#

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.

tribal garden
#

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

viscid plank
#

But all the magic is in firewheel, so also yes

ionic sedge
#

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.

obsidian tusk
#

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 šŸ˜…

tribal garden
#

Thank you all so much, this all helps a ton. Gives me plenty to look into

tribal garden
#

I would say something guitar hero-ish is a stepping stone, my aim is something much more complicated

obsidian tusk
#

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

tribal garden
#

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 šŸ˜…

short fossil
slim pulsar
stoic igloo
#

These are ideas only.

  1. bevy_time used by bevy_seedling
  2. scaling to 80 m stop playing sound instead of -24 dB
  3. maybe add tight room for 2D top down etc to change sound decay parameters
  4. 2D effects corvy had TODO: I might have a sketch
  5. 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

mental rover
#

how can i allow SfxBus to have a dynamically changable volume? using commands.spawn(SamplerPool(SfxBus)) doesn't work

ionic sedge
# mental rover how can i allow `SfxBus` to have a dynamically changable volume? using `commands...

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

errant hornet
#

how can i implement a proximity voice ? when player is distanced to the transform volume is same

obsidian tusk
#

You need to use the spatial node + spatial listener system, there’s an example in the repo one sec

errant hornet
#

oh yes i forget to add spatial basic node

#

thank you so much

obsidian tusk
#

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)

obsidian tusk
errant hornet
#

can we feed raw pcm data to bevy_seedling ?

#

i'm thinking to make a voice chat

obsidian tusk
#

I can't remember how you add a source node to seedling, maybe Corvus has an idea

#

but it should be possible

errant hornet
#

there is stream node

#

but i dont now how to use it

obsidian tusk
#

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

stoic igloo
#

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.

stoic igloo
#

Also, they are not asset sounds. They are mic inputs that require an output and cpal is not duplex like that.

stoic igloo
#

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.

obsidian tusk
stoic igloo
#

Yes, however that bus has to remain active so that it’s available

obsidian tusk
#

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

stoic igloo
#

There are many ways. I just want it active all the time.

ionic sedge
#

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!

obsidian tusk
#

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

stoic igloo
#

Great starting point!

stoic igloo
#

Well, besides being bevy 0.12

obsidian tusk
#

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)

stoic igloo
#

@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.

static quest
#

@ionic sedge ^

stoic igloo
#

His cat emoji ooh’ed!

#

I just want it in Firewheel if that’s ok.

#

I built it in Firewheel.

static quest
#

(Corvus is in charge of the bevy demo, not me)

stoic igloo
obsidian tusk
#

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.

static quest
#

You can just add a delay compensation node like you would any other node.

stoic igloo
#

Custom

#

I did that.

obsidian tusk
#

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

stoic igloo
#

Corvy is interested so I wanted to add to Firewheel first.

static quest
static quest
stoic igloo
#

It’s based on your spatial_basic. Same ui same system.

obsidian tusk
#

Ja I’ll have a go, the nodes already have latency metadata so I have the info I need to do it

stoic igloo
#

I have to clean it up. Thanks a lot.

static quest
#

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.

ionic sedge
#

oh nice

static quest
#

Ok, I've added a triple buffer node and published Firewheel 0.9.2!

cedar bison
static quest
mental rover
#

how can i make audio go to a different audio output?

cedar bison
#

is there a replay gain node?

cedar bison
ionic sedge
sick copper
#

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?

ionic sedge
#

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.

sick copper
#

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)

obsidian tusk
#

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

ionic sedge
#

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.

sick copper
ionic sedge
#

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.

sick copper
#

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?

ionic sedge
sick copper
#

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?

ionic sedge
#

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.

sick copper
#

ok great. i'll have a look at all this tomorrow. thanks for your help!

sick copper
#

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?

ionic sedge
# sick copper ok i think my first question is about the focus on samples in `bevy_seedling` an...

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.

sick copper
#

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

cedar bison
#

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?

static quest
#

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.

trim belfry
#

does it make sense to compare two firewheel::Volume by comparing their .linear()? or is there a better way

ionic sedge
static quest
#

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.

trim belfry
#

probably! I was surprized to fing out I cannot compare Volumes tbh, maybe it makes sense to derive PartialOrd?

static quest
#

Oh, does Volume not already derive PartialOrd? That's an oversight if that's the case.

trim belfry
#

yep, thats why I was asking in the first place šŸ™‚
you can't do node1.volume >= node2.volume

near dagger
ionic sedge
near dagger
#

ok šŸ‘ like more latency than in a daw with a slowish builtin soundcard?

ionic sedge
#

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.

near dagger
ionic sedge
#

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
static quest
#

There is also a convolutional reverb.

ionic sedge
#

Ah I didn't realize the reverb for convolution was already merged.

static quest
#

I think it could be useful to have a limiter as part of the Firewheel repo.

near dagger
#

yes a limiter is crucial. i thought i saw one somewhere

ionic sedge
#

The default setup includes one as the final node before the output in bevy_seedling.

near dagger
ionic sedge
#

You can see how firewheel-ircam-hrtf does it for example. It's a third-party Firewheel node that's bevy_seedling agnostic.

obsidian tusk
ionic sedge
obsidian tusk
#

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

static quest
#

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.

obsidian tusk
static quest
#

Rtaudio is MIT, so you are good there.

obsidian tusk
sick copper
#

i got my web audio working using the firewheel web audio backend. works well!

next otter
#

@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.

ionic sedge
next otter
ionic sedge
#

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.

next otter
#

Does "bus" imply plural? I thought it did.

ionic sedge
#

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.

next otter
#

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 šŸ™‚

next otter
ionic sedge
#

Do you work with audio software much @next otter ? This isn't a challenge, I'm just curious.

next otter
ionic sedge
#

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.

next otter
ionic sedge
next otter
#

@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).

ionic sedge
#

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.

next otter
#

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.

ionic sedge
#

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.

next otter
#

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.

ionic sedge
#

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!

next otter
# ionic sedge All three at once? Like in three different projects?

No lol all the same project.

  • bevy_kira_audio was first, because at the time that had the closest chance to being upstreamed and generally seemed favored over bevy_audio.
  • Then I introduced bevy-kira-components because it looked like it could be close to being the "way forward", but then was deprecated.
  • I then thought going to bevy_audio would be a safer choice, but as I started the integration I ran into a weird bug, which I believe was a rodio one. Can't quite recall, but something to do with rodio doing an incorrect sample rate conversion so the sound played weird (but kira didn't have the bug!).
  • Been delaying the migration, but the latest kira changes meant it was annoying for me to upgrade my fork of bevy-kira-components, so I decided to bit the bullet and go to bevy_seedling now. No pressure!

Early on in the above journey I introduced features to control the crate's audio lib, and that has helped a bit.

ionic sedge
#

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).

ionic sedge
next otter
next otter
#

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:

  1. 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?
  2. 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 construct with_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.

ionic sedge
#

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.

ionic sedge
#

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.

next otter
# ionic sedge There are two main things happening here, I think. The first is that `play_at` ...

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).

next otter
# ionic sedge Hm, seems a bit odd it would take _that_ long (unless you aren't applying any op...

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.

ionic sedge
#

Oh, yes so it's just taking a while to decode the audio (and seemingly to also resample for the airpods).

next otter
#

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.

ionic sedge
next otter
# ionic sedge 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...

ionic sedge
#

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.

next otter
ionic sedge
# ionic sedge Hm, and how fast were your frames compared to the processing rate? Maybe that's...

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.

ionic sedge
#

(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.

next otter
#

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ā€

next otter
next otter
ionic sedge
next otter
#

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.

ionic sedge
next otter
ionic sedge
#

I wonder if we could make it a little less touchy blobthink although I'd probably consider precise scheduling of this sort to be moderately advanced usage.

next otter
ionic sedge
#

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.

next otter
ionic sedge
#

ya you could do that

next otter
#

Obviously name was a placeholder. Needs to be pluralized šŸ™ƒ

ionic sedge
#

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.

next otter
next otter
next otter
#

@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

GitHub

I found a segfault when running in release mode with opt-level = &quot;s&quot;. PR created so you can the repro.
āÆ rustc --version
rustc 1.92.0 (ded5c06cf 2025-12-08)
āÆ RUST_LOG=trace cargo...

ionic sedge
#

Hm, I wonder if that's cpal itself.

ionic sedge
#

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.

next otter
next otter
ionic sedge
#

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.

next otter
ionic sedge
#

oki then that should be doable since Firewheel has a version on git with the new cpal and old bevy

next otter
ionic sedge
#

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.

next otter
# next otter <@164224139316428800> added a seamless example repro showing the issue https://g...

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.

  1. 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.

  2. 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).

ionic sedge
#

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.

next otter
ionic sedge
#

Ah, but to be clear, we resample the assets when the sample rate changes, so the time shouldn't meaningfully change.

next otter
#

Ah ok cool.

ionic sedge
#

There may be a very small amount of imprecision though.

stoic igloo
#

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.

ionic sedge
#

Can you post the message verbatim?

stoic igloo
#

Error::bevy_seedling::pool::dynamic Expected audio node in ā€˜SampleEffects’ relationship. Sorry

#

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.

ionic sedge
#

I assume you're talking about a custom node you made here? Did you remember to register it?

#

if it doesn't do diffing

stoic igloo
#

I forgot that. I added it now. Thanks. I’ll read amazing docs.

heady robin
#

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)?

ionic sedge
#

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.

heady robin
#

Interesting
Do you know if there's a... "system hook/event" I could listen to?

ionic sedge
#

I'm not sure. I don't believe cpal provides anything.

heady robin
ionic sedge
#

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.

heady robin
#

I see
(I have no idea how to do that, but I'll do the research tomorrow)
Thanks

ionic sedge
#

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.

next otter
heady robin
heady robin
next otter
next otter
#

@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])
ionic sedge
#

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?

next otter
#

There are many. They keep being created but not cleaned up. And frame rate eventually tanks.

ionic sedge
#

With your deferred approach?

next otter
#

With the observer approach.

ionic sedge
#

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.

next otter
#

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.

ionic sedge
#

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.

next otter
ionic sedge
#

Sample players can be despawned for three reasons.

  1. The sample completed playback
  2. The sample was booted from the sampler pool because another sample with equal or higher priority was queued and the pool is full
  3. 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."

next otter
#

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.

ionic sedge
#

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.

next otter
#

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)

ionic sedge
#

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.

next otter
#

Yea I added a gizmo feature

ionic sedge
#

Are they like a circle or sphere around the emitter?

#

ya a PR would be great

next otter
ionic sedge
#

oh you have a lot going on there! that looks great

#

how do you calculate the radius?

next otter
#

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 šŸ™‚

next otter
ionic sedge
#

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."

next otter
#

The above all sound like good ideas. Could always have each as a setting. Allowing the dev to choose different views essentially.

ionic sedge
#

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.

next otter
#

Sounds like an audio engine engineer’s problem

viscid plank
#

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

ionic sedge
#

yeah probably so

next otter
ionic sedge
# next otter And `bevy_seedling` tracking issue to investigate: https://github.com/CorvusPrud...

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.

next otter
next otter
#

@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?

ionic sedge
#

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.

next otter
#

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.

sick copper
#

yeah it took me about three days before i really understood what was going on

#

at least

next otter
#

@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)?

ionic sedge
#

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

next otter
# next otter BTW I'm currently migrating off `bevy_audio`, `bevy_kira_audio` _and_ `bevy-kira...

@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.

ionic sedge
#

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.

next otter
# ionic sedge One simple way to sidestep this fade-cancelling issue is to just add another vol...

@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.

ionic sedge
#

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.

ionic sedge
next otter
ionic sedge
#

Are you looping the music?

next otter
#

Nope

#

Each sample is like ~6s or something and it plays to completion, then the next sample starts, and so on.

#

🤷

ionic sedge
#

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.

next otter
#

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()).

ionic sedge
#

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?

next otter
ionic sedge
#

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.

next otter
#

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?

ionic sedge
#

ya

next otter
#

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.

ionic sedge
#

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.

next otter
#

If I was prudent I would add a unit test for this use case so it's protected.

ionic sedge
#

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.

next otter
#

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.

ionic sedge
#

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.

ionic sedge
# next otter > In other words, take it out of the old entity and insert it into the new one. ...

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.

viscid plank
next otter
# ionic sedge Anyway, all of this trouble comes from the fact that while you _can_ schedule sa...

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ā€?

ionic sedge
#

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!)

near dagger
#

hiya. just checking when bevy 0.18 support should roll out for seedling?

ionic sedge
#

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.

near dagger
#

cool.would be great. thanks much.

rapid garnet
#

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 ratjam3

near dagger
#

word

ionic sedge
#

Oh for sure! It'll absolutely be ready well before the jam.

near dagger
#

whens the jam start?

ionic sedge
#

two or three weeks i think

#

the 14th right?

rapid garnet
#

Feb 15

near dagger
#

bevy game jam?

viscid plank
near dagger
#

fun

errant hornet
#

when 0.18

ionic sedge
lapis stone
#

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)

ionic sedge
#

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

lapis stone
#

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

ionic sedge
#

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

lapis stone
#

ah yes totally forgot you can default const generics. will update that when i rebase

static quest
#

Oh yeah, I forgot you can do that too. I should do that for Firewheel's built-in nodes.

lapis stone
#

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.

ionic sedge
#

One way to address this is to only implement Default for stereo only, or alternatively provide a new for stereo only.

lapis stone
#

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

ionic sedge
#

i wonder if even just mono and stereo make sense

lapis stone
#

ohhh

#

hmmm

ionic sedge
#

presumably you'd still have a Default implementation that's generic over the channels

lapis stone
#

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

ionic sedge
#

@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.

ionic sedge
#

The major changes included in this branch are described in the change log.

lapis stone
#

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

ionic sedge
#

ya i do think a different arg for each channel is probably better

lapis stone
#

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).

static quest
#

@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.

ionic sedge
#

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.

static quest
#

Oh, I see. I'll work on that then here soon.

next otter
# static quest 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.

static quest
#

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.

static quest
empty oyster
#

any plans for 0.18

ionic sedge
empty oyster
#

cool

next otter
#

@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.

viscid plank
ionic sedge
#

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.

next otter
next otter
viscid plank
#

Yeah, doing it now will make integration slightly easier

ionic sedge
#

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.

lapis stone
ionic sedge
#

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.

lapis stone
#

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

ionic sedge
#

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()
}
lapis stone
#

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

ionic sedge
#

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 { /* */ }),
    }
}
lapis stone
#

neat, that looks like a better experience

ionic sedge
#

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 blobshrug.)

#

Of course, you'd still need something like mono and stereo constructors, so it wouldn't remove that aspect.

lapis stone
#

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

ionic sedge
#

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.

lapis stone
#

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

ionic sedge
#

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.

lapis stone
#

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.

ionic sedge
#

That seems reasonable for the time being!

near dagger
static quest
#

@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.

ionic sedge
#

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.

static quest
#

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.

ionic sedge
static quest
#

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.

ionic sedge
#

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.

static quest
static quest
ionic sedge
#

you know.... like a GUI with busses and tracks xD

static quest
#

OH, right. Lol

#

The internet has fried my brain bavy

ionic sedge
#

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

static quest
ionic sedge
#

he probably had to smile a lot that day šŸ˜…

static quest
#

And of course there is another meaning to the word which came to mind first. super_bavy

ionic sedge
#

okay probably not the best word to describe a gui with buses

static quest
#

(Sorry not sorry)

stoic igloo
#

oh no, BillyDM! The blasphemy

trim belfry
#

@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>()`

ionic sedge
#

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.

trim belfry
#

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

trim belfry
#

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"] }

ionic sedge
#

What's your rustc and wasm-bindgen version?

trim belfry
#

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

ionic sedge
#

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!

trim belfry
#

sorry to disturb, it seems there are more (including wgpu panic), will debug further

ionic sedge
#

Are you aware of the linker flags you need for shared memory (wasm multi-threading) as well in rustc 1.94+?

trim belfry
#

these ones I presume - yeah I have them
⁨```
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"]

ionic sedge
#

it's changed a bit

#
# .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.

trim belfry
#

jesus christ

#

thank you very much!

ionic sedge
#

yeah these used to be automatically enabled when +atomics was enabled

next otter
#

@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 😬

GitHub

GitHub is where people build software. More than 150 million people use GitHub to discover, fork, and contribute to over 420 million projects.

ionic sedge
#

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.

next otter
#

No need to apologize! I really appreciate all your efforts and the entire crate. Just thought I'd check in.

chilly pewter
#

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.

ionic sedge
#

something like:

*settings.play = true;
settings.play_from = PlayFrom::Seconds(0.5);
chilly pewter
#

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

ionic sedge
#

Yeah if you insert a new SamplePlayer it'll start playing the new one.

chilly pewter
#

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

ionic sedge
chilly pewter
#

Cool

ionic sedge
#

Though note that bevy_seedling automatically writes to the offset field, so manual changes to that will be overwritten.

chilly pewter
#

One more thing i wanted to ask, can you have both the bevy_kira_audio and bevy_seedling plugins loaded at the same time ?

next otter
chilly pewter
dark sonnet
#

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)...

dark sonnet
#

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.

sick copper
#

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

ionic sedge
#

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.

dark sonnet
ionic sedge
#

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.

dark sonnet
# ionic sedge That's the intended usage. If you don't provide one when spawning a `SamplePlaye...

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.

ionic sedge
#

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.

ionic sedge
#

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.

dark sonnet
lapis stone
#

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

next otter
lapis stone
#

thanks! cool yeah i must have something funky goin on

ionic sedge
lapis stone
#

whoops that'll do it. thought i disabled the feature

#

5am programming moment

heady robin
#

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

ionic sedge
#

parry isn't in the seedling tree

#

i can't imagine that's related to our code, at least directly

cedar bison
#

@heady robin Parry is physics, most likely you depend on it through avian/rapier (they currently both use it)

heady robin
#

ohhhhh

#

Sorry to bother then, off to avian I go
Thanks for the help

fickle notch
#

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(),
        },
    ));
}
ionic sedge
#

Oh that's funny I was moments away from posting a gist

fickle notch
#

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>();
}
fickle notch
#

at least this helped me start getting some understanding about the crate

ionic sedge
fickle notch
#

nice, checking the playhead instead of the absolute time is neat

ionic sedge
#

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 blobthink

fickle notch
#

thanks for the help btw!

ionic sedge
fickle notch
#

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)

ionic sedge
#

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?

fickle notch
#

building with new_web_audio gets rid of the error but the game produces no sound

ionic sedge
#

ya you need the nightly compiler

#
bevy run web -U multi-threading
fickle notch
#

i am on rustc 1.95.0-nightly

ionic sedge
#

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)

fickle notch
#

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!

lapis stone
#

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 Think

#

(i also have like 3 other busses & a spacial one, if that is relevant)

lapis stone
#

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

ionic sedge
ionic sedge
#

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.

lapis stone
lapis stone
#

actually i can just check when i get back!

ionic sedge
lapis stone
#

oh yeah thats probably it then lmao

#

querying a component that doesnt exist

ionic sedge
#

i'd also expect it to log an error tbh blobthink and if it doesn't it probably should

ionic sedge
#

If you've specifically imported it then it'll use it, but if not it may be falling back to bevy_seedling's * import

lapis stone
#

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?

ionic sedge
#

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

lapis stone
#

oh so youd just query for the musicpool and adj the volume there?

ionic sedge
#

ya

lapis stone
#

okay that makes sense, ty

fickle notch
#

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
ionic sedge
#

i’ve never seen this error

obsidian tusk
#

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?

ionic sedge
#

it’s wasm bindgen failing, not the binary

obsidian tusk
#

Oh my bad, well that makes it even less likely to be related to seedling but explicitly using wasm64 might still fix it

fickle notch
dark sonnet
#

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?

viscid plank
#

(as a Bevy maintainer I am extremely grateful for this testing and feedback)

ionic sedge
#

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.

dark sonnet
#

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 šŸ™‚

ionic sedge
#

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.

dark sonnet
#

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?

ionic sedge
#

We can do a lot of work in the ECS though -- that's no problem!

dark sonnet
ionic sedge
#

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.

dark sonnet
#

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.

ionic sedge
#

Yeah, you'd have to fall back to volume nodes per-sample, maybe with a marker component. Certainly possible, but not as nice.

dark sonnet
#

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.)

static quest
ionic sedge
#

What is the cost of graph recompilation on the audio thread? Does it scale depending on the size of the changes?

static quest
#

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.

static quest
dark sonnet
#

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 šŸ™‚

static quest
ionic sedge
#

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.

static quest
#

What is the default pool size that bevy seedling uses?

ionic sedge
#

The default range is 4..=32, meaning pools start with four voices.

#

They grow quadratically, so recompilations should be infrequent.

static quest
#

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.

ionic sedge
#

ya that was my thinking

static quest
#

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.

ionic sedge
#

(Although Firewheel does quite well with a ton of nodes :3)

static quest
ionic sedge
#

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

static quest
#

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.

ionic sedge
#

ya no worries on that

#

i think it's still (almost surprisingly) fast as-is

static quest
#

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.

dark sonnet
static quest
ionic sedge
#

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.

dark sonnet
#

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 ."

ionic sedge
#

oh i see

viscid plank
#

Important analysis / discussion here

brazen monolith
#

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.

ionic sedge
#

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.

brazen monolith
#

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

ionic sedge
#

That's definitely a bit odd.

brazen monolith
#

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

ionic sedge
#

Ah, looks like glam-31 is actually just lacking an impl

brazen monolith
#

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

ionic sedge
#

awesome, thanks for getting it going! feel free to PR the changes

#

although the sprawling nature of the crates is kind of annoying šŸ˜…

brazen monolith
#

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

obsidian tusk
brazen monolith
#

@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.

static quest
brazen monolith
#

ok will do

brazen monolith
viscid plank
solid vine
#

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?

solid vine
#

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"] }

next otter
#

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.

ionic sedge
#

It's possible this is related to #87

next otter
#

It does talk about smoothing though, so maybe related/same issue.

ionic sedge
next otter
ionic sedge
#

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.

next otter
next otter
obsidian tusk
#

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.

static quest
#

@ionic sedge Are the UI elements using SpatialBasic or VolumePan?

dark sonnet
ionic sedge
next otter
# static quest Hmm, that is definitely odd. What happens if you set `SamplerConfig::num_declick...

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;
static quest
#

Hmm, that is strange.

next otter
next otter
# static quest Hmm, that is strange.

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).

static quest
#

Ok, I'll look into it when I'm finished with this other thing.

static quest
static quest
next otter
static quest
#

Ok, yeah, I am able to reproduce it. Time for every programmer's favorite activity, debugging! šŸ›

next otter
static quest
#

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?

ionic sedge
#

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.

static quest
#

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.

static quest
#

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?

ionic sedge
# static quest 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.

static quest
#

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.

obsidian agate
#

@ionic sedge Hi, is it possible to play quite literally nothing on the audio channel on purpose?

obsidian agate
#

I have a WASM build, but as you are probably aware, its super strict about audio

#

it requires user input and all that stuff

ionic sedge
obsidian agate
# ionic sedge ya like silence?

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

ionic sedge
#

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.

short fossil
ionic sedge
#

Awesome!

lapis stone
#

yea i think i have had weird issues forgetting a specific bevy feature almost every time so this is huge for me

viscid plank
short fossil
#

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

viscid plank
obsidian tusk
ionic sedge
#

also it'll be slightly slower to compile, but bevy audio is pretty small

obsidian tusk
short fossil
#

any idea where this might be coming from?

static quest
ionic sedge
static quest
obsidian tusk
#

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 šŸ˜…

static quest
#

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.

static quest
obsidian tusk
#

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

static quest
#

Ok, I'm feeling well enough again to work on Firewheel!

quaint ermine
#

Hi! Is it possible to change the audio output of the game at runtime ?

quaint ermine
#

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

ionic sedge
#

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.

quaint ermine
#

i see. Still, it's super cool to have these things in the ECS world directly

ionic sedge
#

It's a little barebones now, but we expect to significantly improve it in the near future.

quaint ermine
#

Okay nvm making this menu will not be that simple haha. I must find a way to filter things out šŸ˜…

ionic sedge
#

wow that's a lotta devices

quaint ermine
#

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

ionic sedge
#

might also be your particular backend, i believe mine was a bit cleaner

#

(though im driving macos at the moment)

quaint ermine
#

I use pipewire with alsa

#

almost all the outputs given are actually the alsa output themselves
edit: all of them

ionic sedge
#

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.

quaint ermine
#

aw man, sticking to default audio is not that bad in the end

ionic sedge
#

cpal's direct API may give you better means to filter, it's been a minute since I've used it myself.

trim belfry
quaint ermine
#

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.. 😭 )

obsidian tusk
static quest
ionic sedge
#

oh awesome! i should be able to dig into this properly tomorrow at the latest

solid vine
#

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?

solid vine
#

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(),
    ));```
static quest