#bevy_seedling

6985 messages · Page 7 of 7 (latest)

static quest
#

I forget, does bevy seedling have hard clipping on by default?

#

FirewheelConfig has it disabled by default.

ionic sedge
#

Awesome! So glad to see this working for people with channel needs other than stereo BongoBlob

solid vine
ionic sedge
#

Oh that's so cool blobawe

static quest
obsidian tusk
# static quest The OS and/or sound card probably does its own hard clipping anyway.

I think it should be hard-clipped both at the point that it’s emitted from the app and at the point where the final OS-mixed output gets sent to the sound card, yeah, although every time it gets resampled to another sample rate you need to clip again because interpolation can create samples that are larger than any sample in the input. I think it’s better to warn the user if they’re emitting clipping samples but not to clip them in software by default, because then they might get clipped twice when the OS resamples to driver rate. If they want to remove the limiter they should figure out their own solution to handling clipping. I’d only force the output to be software-clipped if some platform misbehaves if you emit samples outside of -1..1

static quest
obsidian tusk
#

It’s a pic from a promo shoot I did

ionic sedge
#

oh i see it now, wow that’s a good shot

static quest
#

@obsidian tusk OOOOH, now I see it!!! I was seeing some kind of weird bird thing with its head tilted sideways like this. 🤣

obsidian tusk
#

Omfg ahahaha thank you for the diagram 😅😅😅😅 I’m going to think about that when I see this pic now

static quest
#

You're welcome! 😎

obsidian tusk
static quest
#

Here's the glorious SVG file if you ever want to make that your profile pic. 😛

obsidian tusk
#

Aha amazing, thank you

static quest
#

(Made in Inkscape)

static quest
#

@ionic sedge Oh yeah, what do you think of the new audio backend API? I'd like to get moving on getting Firewheel ready to be upstreamed.

ionic sedge
#

ya im sorry, i haven't given it an examination yet in context with bevy_seedling

#

i really should get to it today

#

i don't want to hold things up

ionic sedge
# static quest <@164224139316428800> Oh yeah, what do you think of the new audio backend API? I...

Okay after a bit of my own tidying (forgive the delay), I've just started taking a look. I'm liking it! This will make profiling and testing so much easier (among other things).

I'm sure I could determine why soon enough, but can you elaborate on this constraint?

The only small catch is that the user must make sure that the CpalStream is dropped before the FirewheelContext is dropped. But this is a worthwhile tradeoff IMO.
I'll start evaluating how I can incorporate this into bevy_seedling tomorrow.

static quest
# ionic sedge Okay after a bit of my own tidying (forgive the delay), I've just started taking...

The Firewheel context waits for the Firewheel processor to be cleanly dropped before dropping. (This is needed for CLAP plugins if and when we add that feature).

If this context is dropped before the stream, then the processor will never get dropped.

Though now that I say this, I just rubber-ducked myself into figuring out a solution, which is to just wrap the stream handle in an Arc<Mutex<Option<Box<dyn Any>>>> that is shared between both the context and the stream handle context.

#

Or actually, a cleaner solution would be to have a Box<dyn StreamHandle> trait that has a stop_stream method.

static quest
#

Oh wait a minute, there's even a better way to do this. I'm already using a wrapper for the FirewheelProcessor, so I can just add an atomic flag that makes it drop its contents if it still exists on the audio thread.

static quest
viscid plank
ionic sedge
#

Oh nice, the horrendous trait facade I had for the context can go away with the new API too. Very nice.

solid vine
#

I'm getting familiar with seedling and see things are very centered around the sample player as a sound source, I come more from a synthesis/procedural sound background and wanted to know what your thoughts are on how procedural sound sources fit into the architecture?

ionic sedge
#

What kinds of procedural sounds are you thinking of creating? What would you like it to look like?

solid vine
#

yes! I'm looking to do synthesis with a bunch of techniques(that ive been developing in supercollider and other similar environments), usually its chaining nodes together, for example starting from a sine/saw wave oscillator, adding filters and effects, modulating with envelopes and automating with values from the game world

#

for context for now im working off of your fundsp radio example

#

but am interested in developing native nodes for seedling

ionic sedge
#

Oh nice! So I'm curious what your thoughts on it are right now.

how procedural sound sources fit into the architecture
I guess from my perspective, it seems pretty straightforward at first glance.

For example (imagine these nodes existed), you could set up a voice like this:

#[derive(NodeLabel, Debug, Clone, PartialEq, Eq, Hash)]
struct SawGroup;

fn saw_voice(mut commnads: Commands) {
    let saw_node = commands.spawn(SawNode::default()).id();

    // you could relate these nodes however you like!
    // but as children, they'll be easy to find
    commands.chain_node((AdsrNode::default(), ChildOf(saw_node)))
        .chain_node((SvfNode::default(), ChildOf(saw_node)))
        .connect(SawGroup);
}

So now, you could query over all SawNodes with children, modulating their values however you like.

#

You could spawn these voices all upfront, or over time (at the cost of some graph recompilation).

#

If you wanted to get fancy with it, then you could construct a lightweight pooling or queuing mechanism for these voices.

#

The above example expresses things in small nodes, but you could also write all of it into a single node like the fundsp example. Either style is fine -- and of course you could freely mix them.

#

I'm not totally sure this addresses your concerns -- feel free to correct me.

solid vine
#

Absolutely, I'm still getting familiar with the architecture, so my questions are still a bit uninformed. Would the sawnode be similar to the custom node example ? Or would it have to be similar to sampleplayer to be a source (with no input)?

ionic sedge
#

As a generator, there's no need to take any input. It just writes to the output.

solid vine
#

awesome, looking into it!

sick copper
#

i am doing strictly synthesis in my use of seedling and have found that i prefer to write monolithic synth nodes that i connect to the higher level nodes provided by seedling/firewheel. as opposed to connecting very many synthesis primitive nodes together in the firewheel audio graph as you would in supercollider

#

so e.g. i have a modal synth node that takes a half dozen parameters that i use for collision sounds, which responds to strike events by exciting the set of active modes and outputs the full waveform of the collision

#

i think chaining smaller primitives is neither as ergonomic nor performant

ionic sedge
#

though i will note that smaller nodes may be more performant than you might think

#

we should be able to prove this out soon with the new backend changes (it’s much easier to set up a benchmarking harness)

solid vine
#

cool, that brings another thing I was wondering about, what is the state of profiling for bevy seedling ? should i go for tracy or is there dedicated audio profiling ?

ionic sedge
#

We don’t have tracing set up ourselves. Tracy might also be a touch problematic in the audio thread due to syscalls.

This is something we’ve been thinking about. It would be great if the Firewheel processor could be made to profile the execution time of each node.

static quest
static quest
ionic sedge
#

Yeah it depends on the OS for sure, but at the very least it's monotonic, so it should be okay. Per-node + total time would be perfect (and not just for installations or anything, it's super important for real game studios).

static quest
#

Ok, that should be easy enough to add! Once I'm done updating fixed_resample to use the new rubato I'll work on that.

static quest
#

Man, this ended up being a lot more work than I thought! I should have it done by tomorrow though.

ionic sedge
#

Oh nice!

static quest
#

(The main problem was that rubato completely changed their API to use this new audioadapter crate for the user's input/output buffers. Oh man was that a pain to integrate into fixed_resample and symphonium!)

queue xkcd comic about competing standards

#

Though on the plus side it does mean that rubato now natively supports both interleaved and deinterleaved buffers. So that should slightly improve sample loading performance!

ionic sedge
#

Funnily enough, we might want to make the firewheel processor an audio adapter if I'm reading the crate right 😅
seems like it would make creating a platform abstraction for bevy (such as what charlotte wants) a bit easier

static quest
#

Oh, you mean for the input/output buffers for FirewheelProcessor::process_interleaved?

#

Yeah, that could be done.

ionic sedge
#

Maybe, although I don't know if it actually makes sense.

#

But if would be nice if we could plop anything in the "processing position" of such an abstraction -- Firewheel included.

#

Anyway, might be interesting to think about as the upstreaming progresses.

static quest
#

Things are taking even longer because I realized I could rework the API to support non-integer sample rates. bavy

ionic sedge
#

oh but that sounds cool

static quest
#

Also family is over, so I don't have that much time to work on it.

ionic sedge
#

oh that's okay, i think things will really get interesting for the upstreaming once 0.19 is out the door anyway

west zephyr
#

note that im still hot on a more incremental approach not a big PR doing everything at once

ionic sedge
cedar bison
ionic sedge
#

oh my godddddd no way

#

sintel???

#

sorry, they're only one of my favorite musicians ever

#

so cool to see them contributing

brazen monolith
#

oh right no threading in theads rip

brazen monolith
#

sorry for the double ping lol

ionic sedge
#

Code Parade did a fractal render over one of their tracks: https://www.youtube.com/watch?v=EkZsPcsV7yE

This video took almost 40 hours to render in 4K resolution, but it can run realtime at lower resolution and quality settings. Everything was made with with the PySpace library. It's not really user-friendly, but if you're an experienced Python programmer that's interested in rendering your own fractals, you can check it out here:
https://git...

▶ Play video
#

this was how i found them -- cool collab

#

sintel seems to have niche appeal but ho boy is it made for me!

lapis stone
#

roderickvd is sintel?

ionic sedge
#

nu but sintel is the latest commit to cpal

lapis stone
#

oh

#

lmao thats cool

frail flax
#

oh lol when i loaded this topic it only had one post

lapis stone
#

i havent used kira in a bit, but I also think seedling does a good job of leveraging the ECS in a really nice way instead of as incidental and is generally more expressive and enjoyable to use (at the sacrifice of maybe a bit more complexity?)

frail flax
ionic sedge
#

Yeah, if your needs are relatively simple, they may already be met by bevy_audio!

frail flax
#

so if i'm using seedling, do i disable the bevy_audio default plugin?

ionic sedge
#

My preferred approach is to completely remove bevy_audio with feature flags. The getting started section goes over this. You'll end up with a toml looking something like:

[dependencies]
bevy_seedling = "0.7.0"
bevy = { version = "0.18.0", default-features = false, features = [
  # 2d
  "2d_bevy_render",
  "default_app",
  "picking",
  "scene",

  # 3d
  "3d_bevy_render",

  # ui
  "ui_api",
  "ui_bevy_render",

  # default_platform
  "android-game-activity",
  "bevy_gilrs",
  "bevy_winit",
  "default_font",
  "multi_threaded",
  "std",
  "sysinfo_plugin",
  "wayland",
  "webgl2",
  "x11",
] }
frail flax
#

ah right yes you'd have to disable the feature

#

you'd include both x11 and wayland?

ionic sedge
#

You can enable both -- they don't conflict. The above should represent the exact set of default features (minus audio-related ones).

frail flax
#

aight

#

i think i need to use a different font system anyway lol

#

maybe not .. i wanted to set kerning

ionic sedge
#

For simple, direct panning controls, you can use the VolumePanNode. Or you can just use the SpatialBasicNode, either way.

frail flax
#

the sounds play and not play kind of at random?? i'm doing e.g.

commands.spawn((
    SamplePlayer::new(assets.load(spec.soundData)),
    PlaybackSettings::default()
        .with_play_from(PlayFrom::BEGINNING)
));```
#

shouldn't that play on the default pool? i can't tell when sounds play and when they don't but it feels like random whether they do or not

#

i mean, i can tell when they should be playing, but i can't discern any pattern as to when it plays and when it doesn't

#

it shouldn't matter if i'm doing that spawn in an observer. . . . . ?

frail flax
#

this is with GraphConfiguration::Game and On<PlaybackCompletion> is firing with PlaybackComplete every time

lapis stone
#

im assuming your error is sounds either play late or not at all. that sounds like the audio is trying to be played before it is loaded to me 🤔 but i actually dont know how the sample player handles unloaded assets.

#

maybe see if adding assets.load(spec.soundData) to a Startup system makes it work by hackily preloading (assuming this sample isn't played the first frame of the game. the second time the asset is loaded in your spawner code it should just return the already-loaded handle). if it does, you might want some states to preload all of your assets.

ionic sedge
lapis stone
#

What does happen if the asset is not finished loading but is added to the sample player? My expectation is that it would be played once the asset is loaded but I'm not sure 🤔

ionic sedge
#

I made an API choice that is clearly a mistake in hindsight (and will be fixed soon). When a sample is loading, the entity keeps track of how long it’s taken. Upon loading, the entity is queued and the playhead skips to where it should be had it loaded immediately.

So without optimizations and with short sounds, they can be skipped entirely.

#

The idea was to guarantee sounds are always strictly in time.

lapis stone
#

ahh okay makes sense. yeah that feels more like behavior id want in special situations

ionic sedge
#

oki what the heck im gonna go fix it now and release a patch

#

i wanna definitely make sure moon doesn’t hit this papercut

#

oh but yes to be clear, preloading the assets will fix this!

#

in the meantime

obsidian tusk
obsidian tusk
ionic sedge
#

I think I'll move the behavior to a marker component.

obsidian tusk
#

It shouldn’t be on playback settings?

ionic sedge
#

Well, that would be a breaking change. Of course, I could make a new minor release.

#

But I'm roughly 50/50 on whether one is better than the other.

obsidian tusk
#

Maybe playback settings should be non_exhaustive so you can add stuff like that

lapis stone
#

hmm

#

i feel like the behavior is uncommon enough to where a marker component is fine, but also not super opinionated

ionic sedge
#

Yeah, plus it may be useful to query over these precisely timed samples.

lapis stone
#

oh yeah

#

smart crow

frail flax
frail flax
#

i can tell it's doing the preloading at least to some degree because there's a lot of ```
2026-03-15T11:35:13.648236Z INFO symphonia_format_riff::common: ignoring unknown chunk: tag=smpl, len=60.

in the logs during startup, and not during actual attempted sound playback
ionic sedge
#

Ah, yes that looks correct!

frail flax
#

so yeah i'm not sure what else would lead to playback being erratic in the way i described

ionic sedge
#

Is this with volume modulation going on, or merely playback?

frail flax
ionic sedge
#

Hm, would you mind sharing your code? Feel free to make a gist if it's a little big!

frail flax
#

it's very big :( i can try to make a minimal reproducing example, but when i tried running the examples in the bevy_seedling repo everything seemed fine

ionic sedge
frail flax
#

is it possible it could be because i'm implementing AssetPath?

ionic sedge
#

It should be

DefaultPool,
sample_effects![VolumeNode::default()]
#

SamplerPool(SomeMarker) creates a new pool, or replaces the old one if it exists

frail flax
#

in assets.load(spec.soundData) that soundData thing is my own struct

ionic sedge
#

So that might be why it's so intermittent.

frail flax
#

ok but i also only added that after i saw these issues

#

i'll remove it, but let's see

frail flax
ionic sedge
#

I think it should log an error if it fails.

#

Yes, the asset server will log an error.

frail flax
#

but, the thing that's even more confusing is that sometimes a sound will play, and then trying to play the same sound again later won't play, and then later still it will play again

#

because, shouldn't preloading it once be enough?

ionic sedge
#

Oh and how many sounds are playing at once?

#

Is it a lot?

frail flax
#

3–4 at most

ionic sedge
#

Oh okay that should be fine.

#

The default voice limit is 24 simultaneous sounds per pool.

frail flax
#

yeah i'm not anywhere near that

#

this all feels sufficiently weird i guess i need to try making a test case

#

sigh

ionic sedge
#

Sorry for the trouble! Hopefully it's something simple.

frail flax
#

and, like, spawning sounds from an observer system shouldn't be different in any way?

ionic sedge
#

Yes that should be totally fine.

frail flax
#

as a last-ditch effort, if i turned up logging inside bevy_seedling, might it spit something out?

ionic sedge
#

Hm, possibly, although there aren't too many logs below info.

frail flax
#

can confirm haha

#

i know it's using other crates which might be doing their own logging

ionic sedge
#

Yes that's true.

frail flax
#

well, symphonia isn't logging anything either

#

aight yeah i'm going to eat some breakfast then attempt to reproduce this minimally

ionic sedge
#

Okay, thanks! I'd love to get to the bottom of this.

frail flax
#

mhm

frail flax
#

@ionic sedge wild-ass guess but as i'm putting this example together .. the ordering of add_plugins calls shouldn't matter, right?

ionic sedge
#

Correct it shouldn't change anything.

viscid plank
#

But you should try and write your plugins such that it doesn't

frail flax
#

@ionic sedge ok well i have something. github gist of just this one source file? bc there's also like the Cargo.toml and the assets. i can probably make a self-contained tarball and post it here

ionic sedge
frail flax
#

i'm curious what will happen if i put a wav file in a gist, but we'll see

#

yeah no

remote: Gist does not support directories.
remote: These are the directories that are causing problems:
remote: src, teraknorn
frail flax
#

i can try minimizing this more, but i was surprised at this behavior from this code, so

#

i don't know if there's something obvious to you first

#

minimizing it more and it isn't playing still :T i even set it to play looping and it thinks it's doing something but i don't hear any audio

#

i've been wondering if this is a macos thing

#

i can also get on vc if that would help lol

ionic sedge
#

thanks! i shjould be able to get to this in a bit

ionic sedge
#

Hm, oddly enough I'm not getting any sound at all. My setup might be messed up though. I'll investigate more tomorrow.

frail flax
#

that's why it's so puzzling

dark sonnet
#

Regarding "no sound"... maybe a completely separate issue, but I am running into complete silence, on Linux, but only in release builds. (bevy 0.18, firewheel 0.10, bevy_seedling 0.7.0). I doubt it's a system issue; a previous build of my game jam game using the same dependencies does generate sound in release, though a fresh release build does not.

The default log messages don't differ between debug and release. But rebuilding with firewheel/log shows consistent differences in debug and release. Perhaps there's some unexpected node ordering issue that breaks things? I don't know how to interpret these logs yet.

See https://gist.github.com/eswartz/c62b87518312f336fc741640a5059969 . I am able to use bevy-inspector-egui in release if there are ways to analyze this at runtime.

ionic sedge
#

Hm, a semver-compatible update in some dependency may have broken something, just yesterday even!

dark sonnet
dark sonnet
#

(FWIW if anyone is testing my git repo in the gist, you only need to visit the startup menu to hear/not hear sounds)

ionic sedge
#

@static quest is it possible that the clamp somehow..... made everything silent? Is the hardclip change the only thing that made it into 0.10.1?

static quest
#

Hmm, it shouldn't.

ionic sedge
#

I know right, haha. Although I noticed things suddenly going inexplicably quiet in my own refactoring last night too.

static quest
#

Hmm, strange. Is this on the current crates.io version, or the master branch?

ionic sedge
#

I figured it was just a mistake I made somewhere, but it would be quite the coincidence now that others are reporting it in the published crates!

dark sonnet
ionic sedge
#

I suppose it could also be spurious, and maybe coincidentally triggered by 0.10.1. Maybe some of that UB is finally causing problems?

#

Only being silent in release supports this possiblity.

static quest
#

Have you tried running cargo update?

dark sonnet
ionic sedge
#

I can also report hearing sound in dev, and not in release.

static quest
#

And then cargo clean?

dark sonnet
static quest
#

So does bevy_seedling add the nodes in the same order every time, or does it add nodes asynchrounously?

#

Anyway, I'm almost done with the fixed-resample update, so I'll look into it when I'm done.

ionic sedge
#

It depends on the order of entities in their tables, so it may not be deterministic.

#

Although I think it should be effectively deterministic in practice.

dark sonnet
#

As a note, the ordering of the nodes does not seem to be an indicator of anything. I got the same log differences in debug vs. release builds wrt logging (before and after the firewheel-graph version change), so I think that was a spurious difference. And yes, the order was consistent between runs.

frail flax
ionic sedge
#

The current lack of sound may be upstream of bevy_seedling. Looks like we'll have to investigate for a little bit. I haven't seen this happen before!

frail flax
ionic sedge
#

Can you try pinning firewheel-graph to exactly 0.10.0 like Ex-Sorbitol?

ionic sedge
#

(Note that when running without --release, the repro seems to work as expected for me. Not sure if that's what you're seeing.)

frail flax
#

i'll try without

frail flax
frail flax
dark sonnet
#

I'm seeing the same.

static quest
#

@ionic sedge So does it work if you disable hard clip in the settings?

ionic sedge
#

i'll have to do a quick test in a bit

static quest
frail flax
#

also, when i turned off default-features for bevy to disable bevy_audio, some of my graphical rendering stopped displaying. is there a way to diff enabled features? i'm not sure what feature it was using

ionic sedge
#

feature management can be tricky! there's not really a built-in way to do this kind of feature diffing

#

you could probably compare lockfiles though

frail flax
ionic sedge
#

ya

#

unless they omit default features, im not totally sure

frail flax
# ionic sedge ya

i don't see any plausible key in Cargo.lock that contains features

ionic sedge
#

Ah, sorry. It's purely dependencies.

static quest
#

Ok, the new version of fixed-resample and symphonium is finally finished!

frail flax
#

is there a way to anti-pin a version? in python you could do dep = "0.11.*,!=0.11.1"
i don't see cargo.toml having the same

ionic sedge
#

I don't believe so, unfortunately

static quest
#

It would probably be a good idea to add a debug feature that checks if any output sample is not a finite number.

How would you prefer that feature be implemented, as a feature flag, or as a boolean in FirewheelConfig? And is it ok if it panics if a non-finite number is detected, or do you think it should be an atomic flag that the logging system can poll?

#

It would also certainly be helpful to have a tool that can turn the debug output of the Firewheel schedule into a visual graph. But I'm not sure where I would even start with that.

ionic sedge
static quest
#

Though I would be kind of suprised if there was a mistake in the compiler algorithm. It's pretty much just Kahn's algorithm with a reference-counted buffer allocator.

ionic sedge
#

Panicking is pretty bad in my opinion -- can the processor reasonably recover from nans?

static quest
#

Yeah, the OS might just output silence if an application gives it invalid samples, which is why I'm suspecting it.

#

And I suppose it could be the UB in the schedule module (making an &mut from an &).

dark sonnet
#

Just wondering, since you seem open about knowingly doing fishy operations :), have you seen runs of cargo miri for analyzing UB?

static quest
#

Though we did fix the UB in a recent PR, so it might not be it.

ionic sedge
#

It would be quite funny if it finally triggered now, of all times.

static quest
#

I probably should add the fix to 0.10 though.

ionic sedge
#

Hm, that should be included in my refactoring work I'm doing, so it looks like none of that resolved it.

ionic sedge
static quest
ionic sedge
ionic sedge
#

Ah, sorry it looks like I actually was behind still on my local branch. Pulling the latest changes resolved the silence issue. Of course, since it was spurious, I can't exactly be sure these commits truly fixed it.

frail flax
#

would PlaybackSettings::despawn_at make sense to have? if i'm fading some audio out, i want to despawn the player when it's done

trim belfry
frail flax
trim belfry
#

if volume <= Volume::SILENT ?

frail flax
ionic sedge
static quest
static quest
#

@ionic sedge Ok, I patched the UB in version 0.10.2 (and yanked 0.10.0 and 0.10.1). Hopefully that fixes the issue!

ionic sedge
#

Thanks! I'll give it another test later today to see. I suspect the code changes would make it disappear incidentally anyway, since we only managed to trigger it just now!

obsidian tusk
#

Do any of you (BillyDM perhaps?) happen to know how DAWs estimate the latency of the output audio device?

static quest
#

Usually the OS's audio API tells you. I'm not sure if CPAL does though.

#

But if you know the block size, then you know that it is at least sample_rate/block_size seconds.

#

Oh wait, no, it's block_size/sample_rate seconds.

obsidian tusk
#

Yeah I guess that’s better than nothing. I could also have a calibration screen like in guitar hero

ionic sedge
#

Isn't there also hardware estimates? The web audio API actually provides this.

static quest
#

Yeah, rhythm games typically feature calibration settings.

#

I know RtAudio provides the latency. I'm not sure about CPAL.

obsidian tusk
#

Maybe I’ll just use the audio engine latency for now since that’ll make sure I actually have some kind of latency handling and do something smarter later

obsidian tusk
static quest
obsidian tusk
#

Oh that’s really interesting, well the main thing I want it for (synchronising inputs and animations with audio) requires sending info from audio to main thread anyway so it’ll do

#

Any idea how I’d access that from seedling?

static quest
#

Ok yeah, that makes sense.

#

Yeah, Firewheel currently doesn't expose that. It might be possible with a custom CPAL backend.

obsidian tusk
#

Oof, well that’s a bit out of scope for now unfortunately

static quest
#

Though I suppose it wouldn't hurt to add an "playback_instant" field to ProcInfo.

obsidian tusk
#

Yeah that would be nice, I can’t be the only person who cares about that. I actually just saw someone online complaining about audio latency for non-rhythm competitive games since human reaction time to audio is much faster than for visuals

static quest
frail flax
static quest
#

Oh actually, no it can be used. You can subtract the callback timestamp from the playback timestamp to get the amount of delay between the process function and when the data will be delivered to the DAC.

frail flax
#

also, whilst i’m thinking about it, is there a pattern for “play after loading” other than checking the AssetServer before a spawn?

#

i guess i can have a unified schedule system of my own that starts playback after load and despawns after fadeout

ionic sedge
frail flax
ionic sedge
#

not in 0.7.1 ;)

frail flax
#

oh ok

#

is that released?

ionic sedge
#

yep!

frail flax
#

ok

ionic sedge
#

let me know if it results in any improvements to your project

frail flax
static quest
#

Ok, I added a playback_delay field to ProcInfo!

obsidian tusk
#

Do you know if CPAL includes the delay from the buffer size in that? If not, it might be worth calling it output_delay or something to make it a bit clearer that it isn’t the entirety of the latency. Maybe worth having a simple helper method for the buffer delay and for the sum of the two if you’re in the area anyway, that’s easy to implement in user code though ofc

#

Anyway, the info's there now so I can dig into it when I get to figuring out the audio subsystem

static quest
obsidian tusk
#

Ok cool, yeah that makes sense

static quest
#

I could call it process_to_playback_delay to make it clearer.

obsidian tusk
#

I think that playback_delay is fine if the only other delay is between the main and audio thread, I don’t think it would be expected that firework would (or could) handle that

static quest
#

I kind of like the clarity, so I think I'll go ahead and change it.

obsidian tusk
#

So long as the delay is starting from the start of the process call and not the end I think the naming is fine

obsidian tusk
static quest
#

Yeah, it's the start of the process call. (The OS would have no way of knowing how long you code will take to process in the audio thread. 🙂 )

ionic sedge
#

lemme take another look right now

static quest
#

Oh looks like I messed up the formatting when fixing merge conflicts. 💀

#

Oh, I messed up the imports too.

static quest
#

Ok, fixed them!

#

I want to give one final thought to what value we should use for the DEFAULT_DB_EPSILON (as in how quite must a signal be to be considered "silent").

Right now it's at -100dB, but that might actually be a bit excessive. Apparently ffmpeg uses a default of -60dB: https://ffmpeg.org/ffmpeg-filters.html#silencedetect

ionic sedge
#

ya i see -60 pretty often

static quest
#

We could split the difference and go for -80dB. Though if -60 is commonly used, then maybe we should use it too. I'll do some more research to see if I can find anything.

#

Hmm, apparently the KFR DSP library uses -100dB as the default.

#

Hmm, JUCE uses -100dB as the default as well.

ionic sedge
#

That's probably fine then, to be honest.

static quest
#

Yeah, I'll ask in the audio programmer discord to see what they think.

static quest
#

Oh sweet! Well, that's the first time that using unsafe actually came to bite me in the butt later. I have now earned the right of passage as a Rust developer! 🦀

obsidian tusk
#

Both of those are expected to work with audio input though, you can probably use a much lower value for an all-digital system

obsidian tusk
obsidian tusk
#

Like, 100dB dynamic range covers a whisper on the other side of the room to instant hearing loss

#

I went to go check my numbers on that and got this incredible AI summary that classifies rustling leaves as "mild hearing loss"

obsidian tusk
heady robin
#

I'm setting up audio buses and pools this way:

for bus_id in AudioBusId::iter() {
    commands.spawn((VolumeNode::default(), EffectsBus(bus_id)));
    commands
        .spawn(SamplerPool(EffectsPool(bus_id)))
        .connect(EffectsBus(bus_id));
}

Attaching sounds to pools this way:

fn attach_pools_to_new_sample_players(
    newborn_sample_players: Query<(Entity, Has<MusicTrackPlayer>), Added<SamplePlayer>>,
    mut commands: Commands,
) {
    for (sample_player_entity, music_track) in &newborn_sample_players {
        commands
            .entity(sample_player_entity)
            .try_insert(EffectsPool(if music_track {
                AudioBusId::Music
            } else {
                AudioBusId::SoundEffects
            }));
    }
}

Yet, I'm gettiing the following error:
Queued sample "audio/sound_effects/card/card_ui/card_hovered/cardHovered2.ogg" with effects in an effect-less pool.

What could be the cause?

lapis stone
heady robin
#

I like having enum-based markers so that it's easy to make more

#

Also I can just tell something to go like:
Are you a sound effect? Route yourself to AudioBusId::SoundEffects (which I know if Pool(SoundEffects) routed to Bus(SoundEffects))

lapis stone
#

well, i think that seedling might be operating on those at a component level, so i'm unsure if an enum of the same type would work (?) i am very unsure about that though

#

idk why it would result in that warning either way

lapis stone
#

if that doesn't work i'm sure corvus has some better advice

heady robin
lapis stone
#

did you by chance create your own type called MusicPool

heady robin
frail flax
#

i'm trying to figure out how to have an effect that i can tune how 'dry v. wet' it is. like if i have a LowPassNode and by default 0% of sound goes through it (totally 'dry') but sometimes i want to hear 80% that and 20% not-that (80% 'wet')
it seems like SendNode would be involved in some way, but i can't quite put it together in my head

#

like, i guess i can have a SendNode chained to a VolumeNode, but then i have to change the parameter in two places to make sure i don't have >100% of the sound. unless that's the best i can do

obsidian tusk
# frail flax i'm trying to figure out how to have an effect that i can tune how 'dry v. wet' ...

Dry/wet for filters is notoriously finicky because filters shift the phase of the input audio, so mixing it with the dry signal will cause interference. If you just want to get it working though, the way you’d do it is to basically have a send node with a linear volume of your wet amount and then have a volume node after the send node with a linear volume of your dry amount, and then have both the return and the dry signal go to the same mixer node

obsidian tusk
frail flax
#

i understand if the filter changing the phase might make it sound off tho

obsidian tusk
#

It’ll be alright, it’s something you’ll notice if you’re a music producer but it’s an effect that people use deliberately sometimes

frail flax
#

heh

ionic sedge
obsidian tusk
frail flax
#

i haven't tried it yet but i expected i could make the generalization extend to schedule_tween too

ionic sedge
#

Both a node label and bus label will insert a type-erased interned trait object, which is what I actually query for.

heady robin
#

(What should I do then)

ionic sedge
#

When does attach_pools_to_new_sample_players run?

heady robin
ionic sedge
#

I wonder if it's getting to them after one cycle.

heady robin
#

I also tried putting it in an observer, which changed the result but it was funny still

ionic sedge
#

At which point they may have already been assigned to the DefaultPool

heady robin
#

(Thinking)

ionic sedge
#

Can you try putting it in PostUpdate ordered before

#

just a sec

heady robin
#

Ok so I'll start by putting it in an observer again

#

oh ok

ionic sedge
#

Okay to guarantee it, you can put this in Last and order it before SeedlingSystems::Acquire. Or PostUpdate should be fine if you only spawn stuff in Update.

heady robin
#

I'll PostUpdate it

ionic sedge
heady robin
#

(It'll take a few minutes to compile since my project is already dozens of thousands of lines and it's down below in the systems crate)

ionic sedge
#

PostUpdate will work though.

heady robin
#

I'm closing a few last things, mainly settings, where I want to allow users to control the music and sfx settings and that's where the problem arises

ionic sedge
#

An observer should work well too, btw. I wonder what issue you ran into there blobthink

heady robin
#

I don't remember exactly, but it's currently staring-at-screen time

heady robin
ionic sedge
#

yes yes mhm nodcathyper

heady robin
#

We'll see

heady robin
#

So um

#

If I put the sound effects bus on 1.0
All of the sound effects play at 1.0 (including, say, ones that should be entirely silent and are also very far away)

#

I'm not sure they play at 1.0 exactly but they're loud when they should be completely silent

ionic sedge
#

The nodes are multiplicative, so it can't be raising the volume of previous nodes! Seems like something odd may be happening.

heady robin
#

oh no

#

What it feels like is that it overrides their audio

#

Like, the volume of each player

#

let me check that my queries are absolutely correct

ionic sedge
#

Ah yeah I could totally see a rogue query messing them up.

heady robin
#
pub struct AudioBusUpdater;

impl Plugin for AudioBusUpdater {
    fn build(&self, app: &mut App) {
        app.add_systems(Update, update_audio_buses.run_if(resource_changed::<PlayerPreferences>));
    }
}

fn update_audio_buses(
    mut buses: Query<(&mut VolumeNode, &EffectsBus)>,
    player_preferences: Res<PlayerPreferences>
){
    for (option_tag, bus_id) in [
        (OptionTag::MusicVolume, AudioBusId::Music),
        (OptionTag::SfxVolume, AudioBusId::SoundEffects)
    ]{
        let value = player_preferences.get_or_default(&option_tag);
        for (mut volume_node, EffectsBus(bus_id_of_effect)) in &mut buses{
            if bus_id == *bus_id_of_effect{
                volume_node.volume = Volume::Linear(value);
                print_info(
                    format!(
                        "Setting {:?} audio bus volume to: {:?}",
                        bus_id, value
                    ),
                    vec![LogCategory::Audio],
                );
            }
        }
    }
}

That's exactly how buses should be updated, right?

#

I'm updating the buses, not the pools

ionic sedge
#

Yes this looks like it shouldn't do anything problematic. As long as the EffectsBus components are only on those bus nodes.

heady robin
#

I'm still getting these (the sounds listed here are some of the ones that should be completely silent but aren't)

#

looks like they aren't silent because they have no effects thus no modifiers

ionic sedge
#

I should probably report the pools and effects involved blobthink

heady robin
heady robin
#

But why would they disconnect?

ionic sedge
#

Hm, I'll have to take another look in a bit here.

heady robin
#

I doubt it's a problem in the crate if it works for everyone else

ionic sedge
#

ECS APIs can have some hard-to-anticipate bugs!

heady robin
#

Yea I learned that in the last couple of... years 🥹

#

Still, whenever I make a game in not-bevy I miss bevy

ionic sedge
#

I can't wait for archetype invariants BongoBlob

heady robin
#

(I don't know what that is so I guess I'll just have a fun surprise one day)

ionic sedge
#

The idea could encompass many tools, but the idea is that you can enforce invariants about archetypes. For example, A and B can never exist on the same entity.

heady robin
#

Reverse #require it is (sounds very useful)

heady robin
frail flax
#

"yes i know these two systems are ambiguous because no entity should ever have these two components simultaneously"

frail flax
#

so, here's a question.. what frequency should i use for a low-pass filter that's passing everything? i tried f32::INFINITY at first and that seems to uhhhh make lerp unhappy

#

i hardcoded 48_000 which is good enough i guess

ionic sedge
#

20k is probably fine, although technically there's some attenuation there

#

you could calculate it based on the filter response but it really doesn't matter much, maybe 24k if you want to be careful

frail flax
#

oh right nyquist frequency

#

48kHz is the sampling rate for a 24kHz signal

heady robin
heady robin
#

For the record, I just replaced them with empty labels (without enum field) and the warning persists

hot trellis
#
commands.spawn((
        SamplePlayer::new(asset_server.load("audio/sfx/surface_impact.wav")),
        Transform::default(),
        RandomPitch::new(0.15),
        PlaybackSettings::default().preserve()
    ))

 - - - in another system some time later - - - 
playback_settings.play();

is this not the correct way to play it?
It plays once at the beginning but then .play does nothing

also I'm using bevy 0.16 so bevy_seedling 0.5

heady robin
hot trellis
# heady robin I think they clear automatically I spawn a new sound every time

to my knowledge .preserve() makes it so they don't clear automatically
I was doing what you said before but there's this TINY amount of delay before the sound starts playing and it's just about noticeable so I wanted to know if it was caused by spawning a new entity and waiting for a command flush, or if it's just audio latency - and if the latter I'll probably be right back here asking how to reduce that
I think I need to reduce the buffer size or something but no clue how

heady robin
#

I think my next step would be to copy my setup to a clean project and see if it still happens

heady robin
#

I have no idea what the problem is
Is there a way not to use pools and still use the bus?
Maybe if I automatically .connect() all volume nodes?

ionic sedge
#

The delay may very well be loading the sample — are you preloading them at all?

ionic sedge
ionic sedge
hot trellis
#

to give some context I've been trying to solve this all day, didn't want to come in here and ask right away
and it just dawned on me I'm wearing bluetooth headphones aaand I'm pretty sure that's the reason
I record it with obs, 0 delay (which is actually strange given I was still wearing bluetooth headphones watching the obs clip...)
does the mp3 player know that there's audio latency and compensates, I've begun to question everything now omg

ionic sedge
#

Oh, yeah they introduce quite a bit of latency! Youtube and other players can compensate for this by momentarily holding back the video stream for a moment when it starts playing.

ionic sedge
#

Thanks a lot!

heady robin
#

I still didn't manage to replicate them but I did notice someting new. Audio players are time-scaled, so that different ones would change their speed based on the context in which they belong. However, they no longer respond to the time slowing down/pausing.
That's the query for slow down:

     (&TimeScalerSubscriber, &mut PlaybackSettings, &SampleEffects),
     Added<TimeScalerSubscriber>,
 >,```
Which means one of those (sample effects?? because of what it says in the warning) is not present
ionic sedge
#

Not all SamplePlayers will necessarily have SampleEffects. Maybe you already knew this -- if a pool has no effects then the player won't either.

heady robin
#

wh

#

Hold on, reading again

ionic sedge
#

It's just a Bevy relationship, so an empty set will mean the component doesn't exist.

heady robin
#

Are the effects of the pool overriding the effects of the player?

ionic sedge
heady robin
#

I see
It could be useful for performance (less entities)

#

But that doesn't explain why I'm getting the warning 🥹

#
commands.spawn((VolumeNode::default(), EffectsBus(bus_id)));
        commands
            .spawn(SamplerPool(EffectsPool(bus_id)))
            .connect(EffectsBus(bus_id));```

The pool DOES have no effects though
ionic sedge
#

right

#

Well if it doesn't, and you queue a sample with effects into it, then you'll get the warning.

heady robin
#

Ok so that explains it after all

#

um

#

So how should I approach having a music bus and a sound-effects bus?

ionic sedge
ionic sedge
heady robin
#

Oh

#

I want to be able to control all music volumes and all sfx volumes from a slider in the settings

heady robin
ionic sedge
#

Like individually?

#

Or as a group

heady robin
#

Not each sample

#

I'm spawning an sfx bus and a music bus
Then a pool for each of the buses
Then I add the "pool subscription" to each new sample player

ionic sedge
#

Well the pool routes into a single node already (by default a volume node), so you can query for that and change it directly!

heady robin
heady robin
ionic sedge
#

You don't necessarily need the buses if you just want to control the volume of the pool.

#

Yes.

#

For example

heady robin
#

So what I should do instead is spawn each pool with a sample_effects![VolumeNode ... ] then control their volume nodes directly?

ionic sedge
#
commands.spawn(SamplerPool(MusicPool));

fn adjust_all_music(music_volume: Single<&mut VolumeNode, With<SamplerPool<MusicPool>>>) {
    music_volume.volume = Volume::SILENT;
}
heady robin
#

Oh so not as a child even

ionic sedge
heady robin
#

I could put it right on the entity

ionic sedge
#

To be clear:


// this
commands.spawn(SamplerPool(MusicPool));

// is precisely equivalent to this
commands.spawn((
    SamplerPool(MusicPool),
    VolumeNode::default(),
));

// though you could put any node on the pool if you want
#

And all the samplers are routed to the SamplerPool<T> entity.

heady robin
ionic sedge
#

ya you can just check the value (.0)

heady robin
#

great

#

Ok, so that should solve it
I'll let you know how it goes
Thanks

#

That would not silence the warnings though I assume

#

Although the pool does have an "effect" that is the volume node

#

So the non-spatial sounds seem to be fine now
However
Does the pool of players with ItdNode and SpatialNode and such also need to have those?

#

And also the error where the music that should be silent plays is still there

heady robin
#

Currently the setup is:
Two pools (sfx, music), each is spawned defaultly (with a volume node on it)
Each player has some effects attached as children (ranging from just Volume to 5 different effects) and has a pool it subscribes to

ionic sedge
heady robin
#

Each player has its own effects

#

I could make different pools for each combination of effects (but, there are many combinations, so I'd rather not)

#

(For example, regular enemies have volume and spatial nodes. If an enemy is elite, their sounds also get lowpass and freeverb. So having the effects on the pools would mean having two pools for the enemies only, and having to route between them)

#

I could do that, but it would take time

#

Is there no other way to control all the volumes?

ionic sedge
#

I feel like we're continually talking past each other on a couple points. But I'll address this one first!

I could make different pools for each combination of effects (but, there are many combinations, so I'd rather not)

This has been brought up before in here actually. I would recommend adding all effects you might want and then having the default values be effectively disabled by default. For example, you might have a low-pass filter at 24kHz by default.

This is a massive advantage for performance. It allows you to not recompile the audio graph for every sound played, and Firewheel can skip a lot of effects that are disabled.

#

Maybe we can figure out a nice way to preserve performance without requiring this kind of planning, but it's a touch tricky. Note that dynamic pools give you the ability to describe sets of effects on the fly, but the routing is more tricky.

heady robin
ionic sedge
#

No worries -- if anything I take it as an admonishment against my APIs!

#

Ideally they should be easy to understand.

#

So I think they could use some work.

heady robin
#

Ok so let me see if I understand now
What you suggest is:
Two pools (sfx and music)
Each one has all possible effects
Sound players that don't want to use a specific effect should have it disabled, those which need an effect with different values than the one in the pool (say, something should have a more aggressive lowpass) should have that effect as a child(?)

#

How would that work with reverb for example?
Is there a "neutral" reverb?
What I mean is:
If we look at volumes, I could have the pool's volume be 1.0
This way, if an effect has 0.3 volume. or 0.0, or any volume, its final value stays the same
How can that be done with the others?

ionic sedge
#

Reverb should generally never be used per-node -- that is, you should have some reverb bus that you route to with, for example, a SendNode.

heady robin
#

Ok, let me lay out the plan and I'll start working on that first thing tomorrow:

  • Add all possible effects for the two pools in a default state
  • Instead of putting on nodes only the effects you want, put on them the effects you Don't want as well, but have them disabled
  • For reverbs, instead of specifying a reverb for that node, create nodes (pools?) for each possible reverb, then connect each one to its relevant pool (so sfx reverbs to sfx pool etc), and then whenever a sample needs reverb, connect it to the relevant reverb node using SendNode
#

@ionic sedge (is that a good plan?)

ionic sedge
#

Instead of putting on nodes only the effects you want, put on them the effects you Don't want as well, but have them disabled
If the default state (i.e. the values as provided in the pool entity) are disabled, you don't need to explicitly mention them every time you spawn a SamplePlayer! That is, you'd only have to mention the effects you care about.

ionic sedge
#

If you need more context on the reverb setup, let me know and I can show you in a bit more detail.

heady robin
#

and then also the reverb-send thing

heady robin
ionic sedge
heady robin
#

Ok so, let's revise my task list haha:

  • Add all possible effects for the two pools in a default state, just disabled
  • Instead of specifying a reverb effect for each node, create nodes (and tag them with ReverbConfig(ReverbId)) for each possible reverb, then connect each sample player to its relevant node using SendNode. Each one of the reverb config nodes still has to be routed to a pool so they can be controlled from the settings, which means I might want to have some with the same Id, just different ones for music and sfx.
#

Ok now

ionic sedge
#

Yes!

#

That sounds good

heady robin
#

Nice!
Thanks for the help

ionic sedge
heady robin
#

@ionic sedge um
How do I disable a node?

ionic sedge
#

depends on the node

#

we only have ad hoc methods frankly

#

maybe we should add a bypass

#

what kinds of effects are you arranging

heady robin
ionic sedge
#

Well freeverb is tricky, but you can disable a lowpass by setting the cutoff beyong the audible range!

#

Firewheel's lowpass also just has an enabled param

#

I need another node consolidation pass to remove seedling's old stuff.

heady robin
#

I see

#

Well, I guess the solution for now would be to

#

Also make a different pool for things with lowpass, and for freeverb

heady robin
#

Still getting warnings and all the bugs are still there (I assume, due to the effects thing still)

#

Currently spawning spatial sound pools with the spatial sounds nodes on default config:

fn spawn_audio_pools(mut commands: Commands) {
    for pool_id in AudioPoolId::iter() {
        let mut pool_commands = commands.spawn(SamplerPool(AudioPool(pool_id)));
        if pool_id.spatial() {
            if let AudioPoolId::SoundEffectsSpatialWithExtras = pool_id {
                ...
            } else {
                pool_commands.try_insert(sample_effects![
                    SpatialBasicNode::default(),
                    ItdNode::default(),
                    BlockedSoundNode::default(),
                ]);
            }
        }
    }
}```
heady robin
#

Thanks for all the help and patience

static quest
# ionic sedge maybe we should add a bypass

The main problem is declicking. If we add a bypass method in the firewheel engine, we will also need to add a declicker. Maybe it could be done, but we should figure out if it's really necessary or not.

heady robin
static quest
#

If we do, we would also need to add an on_bypassed trait method to nodes so that nodes can clear their buffers or whatever they need to do.

static quest
#

I'll look into it.

lapis stone
frail flax
#

i keep getting a crash when i try to pause a player:

Encountered an error in system `bevy_seedling::node::follower::param_follower<firewheel_nodes::volume::VolumeNode>`: Failed to apply audio patch to `firewheel_nodes::volume::VolumeNode`: InvalidData
   3: bevy_seedling::node::follower::param_follower
             at /Users/hab/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy_seedling-0.7.1/src/node/follower.rs:81:13
#

this is the pause code, and it still crashes if i remove the volume change too

playback.pause_at(time.delay(FAST_FADE) + DurationSeconds(0.01), &mut events);
volume.fade_to(Volume::SILENT, FAST_FADE, &mut events);
#

i didn't think i was changing the volume from another system, but i might be

ionic sedge
#

Ah, I have a suspicion. I'll investigate today and publish a patch!

frail flax
#

oh, that's a thing i've been meaning to fix, too—my bevy app on macos requests access to the microphone if i have bevy_seedling enabled, even if i don't seem to be requesting audio input. maybe this is a cpal thing? or is there some default i'm missing?

ionic sedge
#

I do believe it’s a longstanding cpal issue, yes.

frail flax
#

bah

ionic sedge
#

Make sure you’re using the events from the right entity!

frail flax
#

well, i already have an issue on my list for "make gilrs able to submit rumble to controllers on macos"

ionic sedge
#

The volume node and sample player mode have their own AudioEvents components

frail flax
#

i have been getting turned around a few times over in terms of when components are put on the same entity or not. i know it's something about filter chaining

#

but sometimes you can put a VolumeNode right on an entity

ionic sedge
#

Ah, to be clear, nodes (like VolumeNode and the special SamplePlayer) can never coexist on the same entity.

#

Though this isn’t strictly enforced by seedling.

frail flax
ionic sedge
#

Mhm! The SoundEffectsBus component is just a node label — it labels the VolumeNode in this case.

#

But, for example, putting a VolumeNode and SpatialBasicNode in the same entity will cause them to conflict.

frail flax
ionic sedge
#

Ya

frail flax
#

ok cool

ionic sedge
#

I might remove implicit routing.

#

It’s probably not necessary and adds confusion.

frail flax
#

still been thinking about making the generic effect split send thing mentioned the other day

#

need to focus on other parts of the project at the moment. just keep getting reminded that the 300Hz low-pass on the music sounds ok just probably more muffled than i want

#

changing the frequency doesn't have the effect that i want.. it feels like what i need is that low-pass filter to be only partially 'wet'

#

t @ionic sedge ok well here's the full system, just for context https://gist.github.com/habnabit/8ba3104314798eb0f25ea2ab5acbc48d
because of the get_effect_mut part taking extra queries, i was trying to consolidate some of the logic. i can try to fix this up myself; i just wondered if there would be some clearer idiom than what i can eke out

Gist

GitHub Gist: instantly share code, notes, and snippets.

#

that's re: the pause/fade thing

static quest
static quest
#

Ok, the convolution node is done!

ionic sedge
#

awesome!

#

oh i can finally provide some feedback on some of the prs today if you'd like

static quest
#

Oh wait, did you have feedback on the convolution node? (No pun intended)

ionic sedge
#

oh no probably not hehe

static quest
#

Ok cool, because I already went ahead and merged it.

ionic sedge
#

i do love love removing the channel generic

#

makes my life easier haha

static quest
#

Oh yeah, need to add the "noise gate" option to graph inputs.

static quest
ionic sedge
#

One thing I'd like to note is that the PR seems inverted from what I'd want from it. That is, the processor takes types that implement that trait, but I was maybe hoping we could make the processor itself implement it.

I don't know if that's possible or makes sense, so I'll take a deeper look and see if what I'm saying makes any sense. Sorry for being so vague!

static quest
#

Yeah, I'm not sure what you are getting at.

#

Oh yeah, I need to add the test to enforce the clap threading model. I'll do that next.

ionic sedge
# static quest Yeah, I'm not sure what you are getting at.

Oh okay it's fine actually. What I want is to basically support something like this:

struct TypeErasedProcessor(Box<dyn FnMut(&dyn Adapter, &mut dyn AdapterMut)>);

because then, with our platform abstraction in the ECS, we could slot anything into that position. Charlotte would appreciate this, as she sometimes has processing needs that would not call for Firewheel's graph. She's mentioned that she wants this many times.

#

And the way you've done it absolutely supports that.

static quest
#

Good thing I added a test for the CLAP drop ordering, turns out I missed a step in FirewheelContext's drop method that made it fail the test. 😅

static quest
static quest
#

Actually, I just thought of a better API. Instead of sending events to the node's process method, the events could be sent to a separate "on_events" method. That way bypassed plugins can still receive events when bypassed without compromising the API of the process method.

#

As a bonus, it would also help node authors separate their state update logic and their processing logic, instead of having it all in one big function.

#

I'll work on that tomorrow if I have the time.

frail flax
#

ooooohhhh i realized what i want might not be strictly a low pass filter? or does firewheel let you change the eq beyond cutoff frequency? i realized you can usually change q and gain too

#

parametric eq might give me what i want more readily

ionic sedge
#

The SvfNode gives you a lot of control like this.

#

You can choose between different filters

frail flax
#

or a high shelf

frail flax
#

so yeah if svf is already exposed then perfect

ionic sedge
#

You'll have to register it for the channels you want manually, though.

#

It's not registered by default.

frail flax
#

if i’m tweening/animating the gain, is updating the svf parameters ok? or does that mean it has to drop state

#

i haven’t seen the generalization of svf before

ionic sedge
frail flax
#

like, right now, you can tween/animate a volume node’s gain

ionic sedge
#

mhm

frail flax
#

can i animate an svf node without audio cutting out?

ionic sedge
#

Hm, like animate in what way?

frail flax
ionic sedge
#

Oh yeah it shouldn't be a problem

frail flax
#

okie

static quest
#

@ionic sedge As for managing the state of bypassing for nodes, how would you feel/would it be possible to use macro magic to automatically add a bypassed field to each node that derives AudioNode?

Or do you have a better idea on how to manage the bypassing state?

ionic sedge
#

I definitely wouldn't like to do that sort of thing. Could this be a property of the node that the firewheel processor keeps track of maybe?

static quest
#

Yeah, the Firewheel context can have get/set methods for the bypass state.

#

I just wasn't sure if that fit well with Bevy's ECS design.

ionic sedge
#

actually it would work great for the ECS -- we can just have a marker component that syncs state with the graph directly

#

should work great

static quest
#

Ok, cool!

#

I'll just do that then.

ionic sedge
static quest
#

Are you still happy with the Diff/Patch macros?

ionic sedge
#

Yep! I think they're actually quite lovely (not to glaze myself haha). They're quite minimal aside from creating the patch enum, but the enum is so convenient and powerful that I'm okay with it.

static quest
#

@ionic sedge Actually, are you able to just use Memo<bool> on your end to keep track of the bypass state?

#

Because I realized we would have the same state desyncrhonization issue with scheduled events if I tried storing the state in the Firewheel context.

#

@ionic sedge What do you think about this?

#[derive(Debug, Clone, Copy, Default)]
pub struct Memo<T> {
    value: T,
    baseline: T,

    bypassed: bool,
    baseline_bypassed: bool,
}

impl<T: Diff + Clone> Memo<T> {
    /// Construct a new [`Memo`].
    ///
    /// This clones the provided value to maintain
    /// a baseline for diffing.
    pub fn new(value: T, bypassed: bool,) -> Self {
        Self {
            baseline: value.clone(),
            value,
            bypassed,
            baseline_bypassed: bypassed,
        }
    }

    /// Generate events if the inner value has changed.
    ///
    /// This will also clone the inner value and assign it to the baseline.
    /// This may be inneficient if cloning is slow.
    pub fn update_memo<E: EventQueue>(&mut self, event_queue: &mut E) {
        self.value
            .diff(&self.baseline, PathBuilder::default(), event_queue);
        self.baseline = self.value.clone();

        if self.bypassed != self.baseline_bypassed {
            event_queue.push(NodeEventType::SetBypassed(self.bypassed));
            self.baseline_bypassed = self.bypassed;
        }
    }

    pub fn bypassed(&self) -> bool {
        self.bypassed
    }

    pub fn set_bypassed(&mut self, bypassed: bool) {
        self.bypassed = bypassed;
    }
}

impl<T> core::ops::Deref for Memo<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl<T> core::ops::DerefMut for Memo<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}
ionic sedge
#

Hm, and why does a memo need bypassed state?

static quest
ionic sedge
#

Hmm......

#

It seems worryingly invasive

#

The idea being that you wrap parameters in this type?

#

like VolumeNode

static quest
#

Yeah, isn't that what it's for?

ionic sedge
#

well ideallyit would be transparent

#

You can't query for VolumeNode if it's wrapped in Memo

#

can we accept bypass desync? what are the downsides

static quest
#

I mean, the downside is that some nodes could be bypassed/unbypassed when they aren't supposed to be.

ionic sedge
#

That is, it would (in my opinion, please interject if you feel differently) really downgrade the ergonomics and understandability of the ECS APIs.

#

ughh it's tough

ionic sedge
#

Well, different of course (this is due to smoothing I believe), but a similar idea -- when swapping out samples, if users are bypassing some nodes and not others, we need to make sure the bypassing state updates at the same time as the state changes of the parameters.

static quest
#

Another option could be to make bypass events non-scheduled only. Though that would limit the utility of it.

#

Hmm, another option could be to internally use an Arc<AtomicBool> to let the context keep track of the actual state the processor is in.

ionic sedge
#

Is it a bad idea to making bypassing completely invisible to the processor itself? This would mean there are no performance benefits to it, but I think it removes a lot of these API issues.

#

That is, the Firewheel processor could crossfade in and out when a node is enabled or disabled. Presumably.

static quest
#

It already requires no extra work on the node author's side.

ionic sedge
#

Oh doesn't it? I thought the buffers were an Option now

#

In any case, I'm so sorry -- can you restate the current issue with the API? It's not currently schedule-aware, right?

static quest
#

Actually, this is the better API I was talking about. I've implemented it, but currently there is no way to persist the bypass state reliably.

pub trait AudioNodeProcessor: 'static + Send {
    /// Called when there are new events for this node to process.
    ///
    /// Unless this node is bypassed, then [`AudioNodeProcessor::process`] will be
    /// called immediately after.
    fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, extra: &mut ProcExtra) {
        let _ = info;
        let _ = events;
        let _ = extra;
    }

    /// Called when the node has been fully bypassed/unbypassed.
    fn bypassed(&mut self, bypassed: bool) {
        let _ = bypassed;
    }

    fn process(
        &mut self,
        info: &ProcInfo,
        buffers: ProcBuffers,
        extra: &mut ProcExtra,
    ) -> ProcessStatus {
        let _ = info;
        let _ = buffers;
        let _ = extra;

        ProcessStatus::Bypass
    }

    fn stream_stopped(&mut self, context: &mut ProcStreamCtx) {
        let _ = context;
    }

    fn new_stream(&mut self, stream_info: &StreamInfo, context: &mut ProcStreamCtx) {
        let _ = stream_info;
        let _ = context;
    }
}
ionic sedge
#

Oh okay thank you

ionic sedge
#

Sorry, I hope I'm not being dense here.

static quest
#

@ionic sedge And this is what I was talking about with the "macro magic" thing:

/// Using this struct allows us to create a custom Diff/Patch that uses
/// a special "bypass" parameter event that the Firewheel processor can
/// detect and intercept.
pub struct Bypassed(pub bool);

impl Diff for Bypassed {
    // ...
}

pub struct VolumeNode {
    pub gain: Volume,
    
    // Automatically added by the macro.
    pub bypassed: Bypassed,
}

impl AudioNode for VolumeNode {
    /// Can be used to get the bypass state of a type-erased node (useful
    /// for pools).
    ///
    /// This also can be auto-generated by the macro.
    fn is_bypassed(&self) -> Bypassed {
        self.bypassed
    }
}
#

It would actually be a pretty simple macro.

ionic sedge
#

Yep! That part I followed. It

#

oop

#

It's not so much the simplicity of the macro that I'm worried about, but the adding of fields to a struct. Not only is that potentially a touch confusing to new users/contributors, it also requires an attribute macro -- it can't be done in the derive position.

#

I absolutely understand the simplicity of the implementation on your side within Firewheel though. That is very attractive.

#

Let me know if I'm off base @viscid plank, I'm sure you have opinions about this sort of thing.

static quest
#

@ionic sedge And of course another option is just to have bevy seedling keep track of the bypass state for each node. You treat it like any other parameter, and if the bypass state has changed (or if scheduled events have been cancelled), you can use this method added to ContextQueue:

impl<'a> ContextQueue<'a> {
    pub fn time(&self) -> Option<EventInstant> {
        self.time
    }

    /// Set the bypassed state of this node.
    pub fn set_bypassed(&mut self, bypassed: bool) {
        self.push(NodeEventType::SetBypassed(bypassed));
    }
}
viscid plank
ionic sedge
viscid plank
#

Yeah, if you want a real decision from me I'm going to ask you for a quick design doc :p

ionic sedge
ionic sedge
#

That is, this is something that we could trivially do I think.

static quest
#

Yeah, that would actually be even easier to implement on my end.

#

Ok cool. I'll just do that then!

ionic sedge
static quest
#

I suppose its essentially recreating the logic of Memo, just in a more manual way.

static quest
ionic sedge
#

hmmm i see, do you feel like splitting them up could be annoying or should it be okay?

#

i think we currently have a few variables on the stack gathered from the event draining in nodes, right?

#

frequently

static quest
#

Actually, I think it would lead to cleaner code, since it separates the state update logic from the process logic.

ionic sedge
#

oaky

static quest
ionic sedge
#

mm yes yes this seems like a good approach all around

static quest
#

I guess when I was experimenting with creating a custom GUI library in Rust a while ago, I used macros that automatically inserted fields very extensively to achieve a sort "inheritence" scheme. But yeah, that's a much different coding style than bevy.

ionic sedge
#

Yeah if it was common in Bevy, I'd be all for it! But I suspect cart would feel it's out of place and it could become a sticking point.

static quest
#

It ended up being a bit more work than I thought, but I think it's worth it. I need bypassing support in my DAW engine anyway.

#

Also, while editing all the nodes, I noticed that the freeverb node doesn't have a "wet/dry mix" parameter. I guess that makes sense if we only want to support using reverbs as send effects. I suppose if that's what we want to go for, then I could remove some complexity from the convolution node by removing the "wet/dry mix" parameter.

What do you think?

ionic sedge
#

oh boy haha

ionic sedge
#

rather than offering it for each node that we decide ahead of time should have it built in

ionic sedge
lapis stone
#

(i wanted wet dry when i used reverb on my jam game :))

ionic sedge
#

that makes sense yes yes

#

but i am greedy

#

i want ALLL nodes to have the option

lapis stone
#

yes

#

it should be like in the trait or smthn idk

ionic sedge
#

dw i'll come up with something cute

lapis stone
#

i saw the refactor earlier slightly

#

with params and it seems cool

west zephyr
#

whats the perf consideration of doing wet/dry in the graph vs in the node?

ionic sedge
#

Depends on the graph overhead I suppose, but Firewheel's volume nodes are quite efficient.

west zephyr
#

okay

ionic sedge
#

Though you could get away with a single additional node.

west zephyr
#

is there a fast path for 0/100% mix?

#

thats my main question ig

ionic sedge
#

Ya Firewheel's volume nodes bypass themselves in each case.

west zephyr
#

sweet

#

then we can make nodes not have dry/wet built in

#

and have seedling "extract" to multiple nodes

#

for dry/wet support

ionic sedge
#

Oh the refactor will allow me to benchmark this pretty easily to confirm.

west zephyr
#

im really excited to upstream all of this lmao

#

we should have a tracking doc of "are we upstream yet" in #1236113088793677888

ionic sedge
#

oh yes

west zephyr
#

theres a handful of things floating around that still need doing

static quest
#

Oh wait, the bypass declicking isn't actually declicking. Guess I need to do some more debugging. 🥲

ionic sedge
#

alas

static quest
#

Ok, fixed it!

static quest
ionic sedge
#

tastes good

west zephyr
#

it compiles faster

ionic sedge
#

it must grab people's attention haha, a few people ahve pointed it out

#

me and @astral gust are onto something

static quest
#

Hmm, I wonder why a bright red object would grab people's attention...

astral gust
#

🍎

ionic sedge
#

how did you even see that it's supposed to be silent

astral gust
#

i was just reading this thread

#

that was a lie

ionic sedge
#

oh
i thought we had something special

astral gust
#

we do, that's why i wanted to come clean

frail flax
ionic sedge
frail flax
#

oh ok

ionic sedge
#
app.register_node::<SvfNode<2>>();
frail flax
#

ok cool

frail flax
next otter
#

@static quest @ionic sedge did you get anywhere with the “sound sometimes plays quietly” issue (#87)? If it’s the same as the bevy_seedling issue it should still be tracked, but if it’s different maybe I need to create an issue so it doesn’t get lost in here.

ionic sedge
#

I haven't come up with any particular solution myself yet. I'd have to tinker with it a bit, which I really should get to.

static quest
#

Though I suppose another way it could be solved is to add a parameter to the Sampler node to control whether or not samples should be "played in the past".

static quest
#

@ionic sedge How would you feel about adding a boolean parameter to the sampler node that switches between "playing the sample in the past" and "playing the sample as soon as the event arrives"?

ionic sedge
#

Hm, If they’re scheduled in the past, wouldn’t it make sense to always play them in the past?

west zephyr
#

what does playing in the past mean?

#

like is that just skipping ahead in a sample? or

static quest
ionic sedge
#

That might just be something that past players have to deal with blobthink. I’m working on removing the scheduling-all-events-by-default currently, so it shouldn’t be difficult for people to avoid it going forward, at least I think.

#

For seedling anyway

#

Basically, I think we should get to the point in seedling where a scheduled event is very intentional, and thus there’s no need to carve out a special case for scheduled sample events. Does that make sense?

static quest
ionic sedge
#

It worries me a bit, but I'm not sure there's a perfect solution to achieve everything I want either way.

brazen monolith
frail flax
#

can i turn off the declicker to test?

ionic sedge
#

I should have something to look at in a couple hours if you don’t wanna mess with it. We’ll see though, I might have to finish it tomorrow.

ionic sedge
#

Ah, @frail flax, you should be able to disable timestamping globally right now with the ScheduleDiffing resource. (Just set it to false.)

frail flax
ionic sedge
frail flax
#

ahh ok

#

i'll try that now

ionic sedge
#

It disables all (normal diffing) scheduling though, so if you have some precisely timed sequences it might throw things off.

frail flax
#

it's all pretty rough

ionic sedge
#

I'll set it up so that individual entities can opt into or out of scheduling.

frail flax
#

or insert it myself?

ionic sedge
#

You can mutate it or (re)insert it after the seedling plugin is added.

#

Oh actually I use init_resource so it doesn't matter when you insert it.

frail flax
#

that particular sound is still quiet, so i think it might be unrelated

ionic sedge
#

Do you know which version you're on? (Just double checking!)

frail flax
#

bevy_seedling v0.7.1

ionic sedge
#

Okay that's good. That is -- if it were just the declicker from playing in the past, disabling the scheduling should have fixed it.

frail flax
#

ok yes i wasn't expecting this sound was being played with low volume, but when i turn it up, it's much more audible

#

i wonder if this is expecting a different loudness curve than linear

static quest
#

It depends on what kind of sound. If it's a short sound with a loud transient (like a impact sound), then cutting off the transient will make it very quiet.

obsidian tusk
#

Does seedling have a good way to "fast forward" the playback to render out audio? I want to prebake some audio but still use the ECS to define the graph while developing

ionic sedge
#

you could make a little backend to do this!

#

(it would be easier to manage it on master for sure, but im not sure when that'll end up published)

obsidian tusk
#

Ah nice! I don't mind using upstream for my usecase, so long as it doesn't require updating bevy itself (which it doesn't seem to)

#

Is there a way to switch backends at runtime? I don't mind having either a separate binary or a separate mode that the main binary starts in, so if not then that's ok

#

I don't think multiplexing backends would work here though (in case that's possible). If only one of the backends can "fast-forward" then it'll still run at regular speed in order to support the non-fast-forwarding backend(s)

ionic sedge
#

hmm is startup runtimey enough?

obsidian tusk
#

Aha it'll do

ionic sedge
#

Oh you mean for this particular feature.

#

Hmmmm blobthink you might be able to write a backend based on the current cpal one that can change its timescale!

#

and like resamples the output accordingly

obsidian tusk
#

I think it'd be better just to have an entirely separate app just for baking with whatever the minimal set of plugins is, then I can spin it up in a separate thread. Basically the same approach as having a separate binary but with the possibility to transfer entities across

#

Like, do whatever runtime authoring I want to do and then start a baking task which just creates a new App, does whatever the process is to copy the nodes etc to be baked across, and then manually drives the app until the baking is done

#

Then I can do whatever custom entity handling I want too

obsidian tusk
ionic sedge
#

The backend previously was coupled to a somewhat heavy abstraction that made it difficult to manage less conventional setups. Now you can basically do whatever you want.

#

Oh, and that reminds me -- you can certainly switch the backend while a program is running, even after startup. In theory anyway, we might need to improve the API a bit in seedling to make this work properly.

#

But it's something that ought to work. The capability is there in Firewheel.

#

-# i think

obsidian tusk
#

Cool! I think having a separate app is a better choice anyway since it means I can do the rendering in the background, but I'll keep that in mind!

static quest
#

Most modern systems support both 44100 and 48000. It's mainly just older systems and maybe consoles where you might run into issues.

upbeat forge
#

Does anyone get this repeated error on linux?
ERROR bevy_seedling::platform::cpal: unexpected audio backend error: A backend-specific error has occurred: alsa::poll() returned POLLERR
My system uses Pipewire and I sometimes get this error with alsa when developing. Audio doesn't play when this happens. I can try rerunning several times and it would eventually go away, but this happens more often than I'd like. Until the CPAL version with Pipewire host gets released, does anyone have any ideas whats with this error? I think I never got this when I tried out bevy_kira_audio (or at least it doesnt log error?).

ionic sedge
upbeat forge
#

my error gets printed every update i think

ionic sedge
#

As in a few messages when the program starts, but otherwise no complaints.

#

I’m not familiar with alsa — I wonder if this is even an error blobthink

upbeat forge
#

ye. just rechecked to be sure and i have my error handler to panic in dev so the stream probably started successfully

#

i'll make an issue in a bit then

ionic sedge
#

Double check the existing one too! It may cover parts of this already! Let me link it

upbeat forge
# ionic sedge I’m not familiar with alsa — I wonder if this is even an error <:blobthink:56976...

but yeah. im not familiar with alsa either so I was gonna wait for the pipewire host. cuz the pipewire-alsa bridge seems quite finicky. i met another error where the cpal alsa stream fails to start (causing a panic) during a short period after I change the device output volume with pavucontrol (it's like the system audio config needs time to settle or sth). i was going to file an issue as well, but apparently it's reproducible in all kira and cpal as well (just that seedling panic due to the fallible start_stream system)

#

that doesnt happen with the new cpal pipewire host

#

so maybe this is a same thing. but im not sure since i havent been able to reliably reproduce it

static quest
static quest
ionic sedge
#

Hm, so this filters out the warnings?

static quest
#

No, it changes the way you respond to errors. Instead of telling users to stop the stream (or panic) as a result of stream errors, it tells the user to just log them, and to use the new more reliable is_running() method to actually check if the stream had stopped or not.

ionic sedge
#

Ah, the current system on (seedling) main looks like this:

fn poll_stream(mut stream: ResMut<CpalStream>, mut commands: Commands) -> Result {
    let stream = stream.get();
    if let Err(e) = stream.poll_status() {
        match e {
            StreamError::StreamInvalidated | StreamError::DeviceNotAvailable => {
                warn!("Audio stream stopped: {e:?}");
                commands.trigger(RestartAudioStream);
            }
            StreamError::BufferUnderrun => {
                warn!("audio stream encountered underrun");
            }
            StreamError::BackendSpecific { err } => {
                error!("unexpected audio backend error: {err}");
            }
        }
    }

    Ok(())
}

which i think basically does that, right?

#

now we just have a way to check in the BackendSpecific error if it's resulted in a stream stoppage, right?

static quest
#

Correct. Although note it is not gauranteed that the stream will have stopped before an error is returned. There may be a case where an error is returned, and the stream is still in the process of stopping. This unfortunately seems to be a limitation of CPAL's API.

ionic sedge
#

oaky

#

I guess I can always check that flag once a frame anyway.

static quest
#

I suppose we could also trial and error every BackendSpecificError to see if the stream actually stops as a result, but that sounds like a pain to test.

ionic sedge
#

ya that sounds annoying

static quest
#

And the BufferUnderrun variant is definitely not an error, just a warning.

#

I guess the only issue is if cpal for some reason doesn't drop the the data callback as a result of an error, but that doesn't seem likely.

#

(Essentially I'm just setting an atomic flag on the data callback's Drop method.)

#

Man, if only cross platform audio was easy and reliable.

obsidian tusk
ionic sedge
#

i would cosign their termination letters if it meant platforms could agree on sane standards :3

static quest
#

At the same time, I wish cross platform graphics was also easy and reliable.

#

Oh yeah, and windowing.

#

(Shout out to the winit team for all their hard work!)

#

And the wgpu team!

west zephyr
#

:3

static quest
#

Oh yeah, and the cpal team too!

frail flax
#

so, i've been getting deadlocks on app start, randomly, only after/when i have the bevy_seedling plugin enabled. i don't know if this is because of interactions with bevy_audio. i haven't been able to disable bevy_audio on the feature level, but .disable::<bevy_audio::AudioPlugin>() doesn't seem to be enough
is this a known issue at all? i'm trying to dig into a backtrace and i'm starting from nothing

ionic sedge
#

No, I haven't heard of this related to audio specifically.

frail flax
#

hm ok

ionic sedge
frail flax
#

so, i'd need to figure out what's going on there, but i can try

ionic sedge
#

Did you follow the recommendation in the seedling docs?

[dependencies]
bevy_seedling = "0.7.2"
bevy = { version = "0.18.0", default-features = false, features = [
  # 2d
  "2d_bevy_render",
  "default_app",
  "picking",
  "scene",

  # 3d
  "3d_bevy_render",

  # ui
  "ui_api",
  "ui_bevy_render",

  # default_platform
  "android-game-activity",
  "bevy_gilrs",
  "bevy_winit",
  "default_font",
  "multi_threaded",
  "std",
  "sysinfo_plugin",
  "wayland",
  "webgl2",
  "x11",
] }

this should cover everything!

ionic sedge
#

Ah, okay. It could be some papercut then.

frail flax
#

lol dang it i just realized. i think it's because i didn't enable 3d_bevy_render. i'm only doing 2d, but ..

#

that did, in fact, fix my rendering issues. sob

#

.. but it's still hanging

#

there's background audio playing, but the mainloop is wedged

#

the title/loading music is playing

ionic sedge
#

Hm, does it also occur when you don't add SeedlingPlugin at all?

frail flax
#

let's see!

#

it occurs at random, so it's hard to "test", but

#

well, i have some ideas about what it might be actually. less convinced it's an audio thing

frail flax
#

i’m pretty sure i resolved it and it wasn’t an audio issue. it was just exacerbated by the overhead from audio

next otter
#

I left my game running overnight and saw these logs spammed (100s):

...
2026-04-09T01:05:21.173991Z ERROR bevy_seedling::node: graph error: MsgChannelFull
...

And then after all of those about 10 of these in a row:

2026-04-09T01:17:20.634270Z ERROR firewheel_graph::context: Firewheel scheduled event buffer is full! Please increase capacity to avoid audio glitches.

Is there anything that seedling or firewheel should do here? Note: I only realized these logs existed after I exited the game, so I don't actually know what the user impact was here, if any.

ionic sedge
#

Hm, I wonder if very occasionally some messages get stuck or something blobthink Or maybe you could just increase the capacity a bit. We're soon reducing the number of messages that'll be scheduled at least, so the symtoms should be much less noticeable.

#

Worst case scenario the allocation causes a stutter. Though the majority of the time you'll likely get away without any hitches.

#

The event buffers have a maximum size that by default can grow (in an overflow situation). It requires an audio thread allocation. You can set the default size in the FirewheelConfig struct.

next otter
#

I can look into the internals/knobs-to-tune myself and see if there's anything I could be doing but naively is there any way for this simply to not even be a userland problem to think about? If my game is essentially just sitting on idle and it has sound effects that are spawning and despawning and has music samples that are spawning and despawning, does this indicate a "leak" somewhere, or a lack of reusing/recycling resources? I guess what I'm trying to say is that unless I happen to have a silly leak of my own which manifests after hours, shouldn't seedling/firewheel be able to reuse/cycle its own resources? Again, this is just a naive view, I haven't made an effort to understand the internals here.

ionic sedge
#

Oh to be honest, I doubt there's a leak. What's most likely is that we're a bit conservative with our event storage size.

#

Although, there may be a degenerate case where events are getting absolutely spammed. I wouldn't expect to see that message a ton...

#

The messages would imply that it grew a ton, which shouldn't happen.

#

Even if sample players are lying around, unless they're actively connected to a Sampler entity via that relationship, they shouldn't contribute to node events.

next otter
#

Wonder if there's a relatively simple stress test example you can add the the repo and then just leave it running to see what happens.

ionic sedge
#

Yes that's probably a good idea.

static quest
ionic sedge
#

It's called once per frame, however fast that may be.

static quest
#

Hmm, I guess if you were running at a very high framerate it could be a problem.

#

It would probably be beneficial to add logic to bevy seedling to limit the rate at which parameters are synced and updates methods are called.

#

Something as simple as "has it been more than 10 milliseconds since the last parameter sync/update?" would do. (Keep in mind the nominal "frame time" of the audio thread is about 23 milliseconds, which is already larger than the nominal 16 milliseconds of a 60fps frame rate.)

#

Of course we could play around with the exact number to find the best balance of latency.

#

Oh yeah, I still need to add the profiling capabilities. While I'm at it I could add a "heartbeat" indicator to detect if the audio thread is being stalled for some reason.

ionic sedge
#

@next otter what are your typical frame rates, if you know off rip?

ionic sedge
#

It would be nice if we could reliably estimate the block rate so we could set a reasonable default for the diffing rate.

next otter
ionic sedge
#

Hm, I imagine there could have been moments where the frame rate shot way up.

#

A diff limiting system would mitigate this.

#

Obviously we couldhave a very rough system, maybe controlled via a resource, that sets the minimum diffing period. I'd prefer if we didn't force people to mess with it unless absolutely necessary, though.

solid vine
#

@static quest submitted a PR for firewheel-cpal. was crashing when using an asio interface. RME MADIface USB (tested here) delivers ASIOSTInt32LSB → SampleFormat::I32, so firewheel-cpal's f32-only path was failing

ionic sedge
solid vine
#

I haven't yet, I was using seedling 0.7.2 when I had this problem, is that something that was worked on? will try it

ionic sedge
#

The latest code is much more flexible, though it may not have leveraged it in a way that solves this problem yet.

solid vine
#

app.add_plugins(SeedlingPlugin { config: FirewheelConfig { num_graph_outputs: ChannelCount::new(64).unwrap(), ..FirewheelConfig::default() }, stream_config: CpalConfig { output: CpalOutputConfig { host: Some(HostId::Asio), device_id: Some(DeviceId(HostId::Asio, "JackRouter".to_string())), ..Default::default() }, input: None, }, ..SeedlingPlugin::default() });

#

this is how im configuring my sound card

#

but substituting jackrouter for rme madiface usb sorry

ionic sedge
#

oh nice! on master it would be

app
    .add_plugins(SeedlingPlugins)
    .insert_resource(AudioContextConfig(FirewheelConfig {
        num_graph_outputs: ChannelCount::new(64).unwrap(),
        ..Default::default()
    }))
    .insert_resource(AudioStreamConfig(CpalConfig {
        output: CpalOutputConfig {
            host: Some(HostId::Asio),
            device_id: Some(DeviceId(HostId::Asio, "JackRouter".to_string())),
            ..Default::default()
        },
        ..Default::default()
    }));

if you're interested in testing c:

static quest
ionic sedge
#

(looks like the new code still needs this treatment hehe)

solid vine
dark sonnet
#

(I was posting build issues over in Better Audio because Discord hid this channel :-p )

heady robin
#

So I wrote my pool update fn to only update on the next frame
But I can't recall why I did that

#

here it is:

    mut pooles: Query<(&mut VolumeNode, &SamplerPool<AudioPool>)>,
    player_preferences: Res<PlayerPreferences>,
    mut should_update_next_frame: Local<bool>,
) {
    if *should_update_next_frame {
        *should_update_next_frame = false;
        for (option_tag, sound_effects_pool) in [
            (OptionTag::MusicVolume, false),
            (OptionTag::SfxVolume, true),
        ] {
            let value = player_preferences.get_or_default(&option_tag);
            for (mut volume_node, sampler_pool) in &mut pooles {
                if sound_effects_pool ^ sampler_pool.id().of_sound_effects() {
                    continue;
                }
                volume_node.volume = Volume::Linear(value);
                print_info(
                    format!(
                        "Setting {:?} audio pool volume to: {:?}",
                        sampler_pool.id(),
                        value
                    ),
                    vec![LogCategory::Audio],
                );
            }
        }
    } else {
        *should_update_next_frame = player_preferences.is_changed();
    }
}
#

Anything about the way seedling works I might have forgotten? (Going to put a comment there, which is something I very rarely do, because I have absolutely no idea why I did that)

ionic sedge
#

It shouldn't matter for seedling itself. All updates happen in Last, so unless you schedule something in there, it shouldn't miss it.

#

Could this have been for your own ordering needs?

heady robin
heady robin
idle grotto
#

Hi! I'm curious if there is any work being done right now to making multi listener spatial audio work properly? I've been prototyping a doppler and propagation delay effect and they both are listener dependent (so are itd and spatialbasic).

#

Everything seems to be calculated off the nearest listener rn

#

If anyone is working on this or has ideas id love to know!

ionic sedge
#

What are your constraints? How many listeners do you expect?

#

Also, what should the output look like? Two speakers still? Or multiple different devices?

#

That's where "properly" comes into question in particular!

idle grotto
#

My game doesn't currently need multiple listeners 😅 (though the use cases i imagine are split screen multiplayer and something like spectating a esports match (where there are many cameras and listeners))

idle grotto
#

but thats a good question im not sure what would be best

ionic sedge
#

There's not really much to be done if you expect to still produce a stereo (or otherwise normal) output. Consider what happens if you process the full chain for two listeners: when one is close and the other is far, the closer listener would dominate the output. When both are far, the sound is quiet. When both are close, the sound is loud, with the only different being panning.

In other words, this is almost indistinguishable from simply choosing the closest listener.

As far as I know, this approach is standard. It seems like UE5 and FMOD also simply choose the closest listener.

#

It certainly gets interesting in a true multi-listener setup. But I suspect that's better left to a custom approach, possibly where sampler pools are completely ignored in favor of bespoke solutions.

#

And by "true multi-listener," I mean where the graph has more than one destination. For example, a bunch of dedicated listener sinks, perhaps for recording from different positions as you mentioned.

#

But I'll note that when spectating, there's still one graph output.

#

I could imagine something like a virtual performance that you want to record in realtime being an interesting use case.

idle grotto
#

ok that makes sense, maybe ill not put too much thought into it now and just work on getting doppler and propagation delay working nicely and make a pr

#

the way it works for each now is as a smaple effect like this

#
let sound = commands
        .spawn((
            LevelEntity,
            SamplePlayer::new(props.handle.clone()).with_volume(Volume::Linear(props.volume)),
            sample_effects![
                SpatialBasicNode::default(),
                ItdNode::default(),
                DopplerNode::default(),
                PropagationDelayNode::default(),
            ],
            Transform::from_translation(relative_position),
            PlaybackSettings::default(),
        ))
        .id();

#

do you think this is roughly the right shape?

ionic sedge
#

The above is quite simple -- rather than another node, it simply modifies the sample player's playback speed.

#

You could take a more physically-inspired approach and combine both doppler and propagation into one node. Imagine a ring buffer to which you push samples. Assuming some maximum distance between source and sink, you could simulate both doppler and propagation delay by using the distance as an index into the buffer.

Cute, but expensive. Much, much more efficient to simply delay (initial) playback and modify the pitch of the sample player directly.

idle grotto
#

agreed

#

what do you think the user surface should be for setting these effects?

#

also one other thing is that itd, doppler, propagation delay (maybe also spatial muffling?) all rely on speed of sound

#

so some resource holding a speed of sound value that each of them pull from makes sense to me (you would want to make the speed of sound higher if a player was in water for instance)

idle grotto
#

should it just be part of spatial basic? (but then theres hrtf to think about)

#

not super familiar with the best practices with firewheel/seedling

ionic sedge
#

I think the code I linked demonstrates my thinking here -- since it controls the PlaybackSettings component directly, they should probably be companion components.

ionic sedge
ionic sedge
#

And I don't think they should be fields on PlaybackSettings -- they're much less frequently needed.

idle grotto
#

ill work on making that happen and make a pr soonish!

#

thanks for your help! bevy

ionic sedge
dark sonnet
#

I have a question. A while ago I updated and reset my feature flags for bevy_seedling and now occasionally see
Firewheel node profiling buffer is full! Please increase FirewheelConfig::initial_node_capacity to avoid audio glitches..
This used to be a field in FirewheelPlugin (before the s) directly. I had to dig through git to find how to actually set this up now:

.insert_resource(AudioContextConfig(FirewheelConfig {
                initial_node_capacity: 1024,
                ..default()
            }))

I'd recommend SeedlingPlugins, or FirewheelConfig, or the warning message point a little closer to the solution 🙂

ionic sedge
#

Hm, that may be something that needs attention regardless blobthink

dark sonnet
#

Not really. I'm (ab)using things by spawning e.g. 1000 cubes with embedded custom midi synths 😆

ionic sedge
#

oh i see

#

Sorry, I'm used to seeing that show up for messages.

#

The docs haven't been fully upgraded for the resource approach yet. That should help a little bit!

idle grotto
idle grotto
#

pr for doppler effect and propagation delay, lmk if theres any changes needed!

obsidian tusk
#

Sorry if this has been posted here already, I searched but didn’t see anything. Maybe worth having a wrapper impl to use a truce effect as a firewheel node? https://truce.audio

truce.audio

CLAP, VST3, LV2, AU v2, AU v3, AAX, and standalone — from a single Rust crate. Install and load your plugin in a DAW in five minutes.

#

(Also just potentially interesting for the people here)

obsidian tusk
obsidian tusk
#

Btw since I’m on the subject of writing a VST, does anyone know if there’s an efficient Karplus-Strong and/or waveguide-based string synthesis crate in Rust? I found a couple of crates that claim to implement it but don’t, and one crate that has an implementation but it was, frankly, barely a proof-of-concept (unoptimised, does a bunch of allocation in the audio thread, kinda looks like AI code). If it doesn’t exist then I’ll write a crate myself, maybe it’ll be useful for someone. In case it’s interesting, it’s for transient synthesis for a kick/tom VST.

static quest
obsidian tusk
#

Oh interesting that you say that, I was kinda suspicious of it being slop because it’s only been released recently so I dug into everything I had time to look at and it seems totally legit. Everything looks carefully-designed and well-documented, AI tends to be pretty good at implementation but bad at API design and has a signature way of writing docs and I didn’t see any of that. The website looks like a standard template too, rather than an AI design

static quest
#

The readmes do read a bit like AI to me.

#

And the doc comments.

obsidian tusk
#

Hmmm, remains to be seen then

#

I was gonna build my VST with nih-plug but I’ll give this a go and report back

static quest
static quest
obsidian tusk
#

Damn, I got got 😅 And here I was thinking I was alright at detecting slop

static quest
#

Yeah, it wasn't immediately obvious (it's becoming harder to tell 💀 ). I had to do some more digging than usual.

obsidian tusk
#

I mean honestly though I do like the API and the general design so it could serve as decent inspiration if nothing else. The way they handle in-out buffers and transitioning from an auto-gui to a more complete UI is pretty clever conceptually

static quest
#

NIH-plug also has "generic slider widgets" that can be used for a quick "generic" gui.

obsidian tusk
static quest
#

And we're actually currently discussing a new name for the fork to avoid confusion, so prepare for the name of the repo changing!

obsidian tusk
static quest
#

I think we've mostly landed on "nice plug".

obsidian tusk
#

qDot is gonna love that

idle grotto
#

fix for toggling effect bypass not working consistently + example with 3d spatial sound where you can the toggle effects

obsidian tusk
static quest
obsidian tusk
#

Oh I’m not saying that in favour of Truce 😅 I’m saying that it stole even more than I expected and there’s even less reason to pay it any mind

#

Last time I looked at nih-plug the ergonomics were pretty rough and I thought that truce had improved on that if nothing else

static quest
#

Hmm, what do you mean by that? NIH-plug has one of if not the best ergonomics of any cross-platform plugin development framework. (Well, maybe aside from GUI.)

obsidian tusk
#

Yeah, I just last looked at it a long, long time ago and was under the mistaken belief that it basically hadn’t been updated since then. It’s a pebkac bug 😅 My message earlier was saying that nih-plug actually does have good ergonomics after all, even though I thought it didn’t when I was looking at truce

#

Does that make sense? 🙈

static quest
#

I suppose

#

At least compared to JUCE, NIH-plug has a lot less boilerplate.

obsidian tusk
#

Yeah for sure