Compare commits

..

No commits in common. "main" and "v0.4.0" have entirely different histories.
main ... v0.4.0

13 changed files with 380 additions and 596 deletions

47
.drone.yml Normal file
View File

@ -0,0 +1,47 @@
kind: pipeline
type: docker
name: default
environment:
DEPENDENCIES: cmake pkg-config libx11-dev libasound2-dev libudev-dev libxcb-xfixes0-dev libwayland-dev libxkbcommon-dev libvulkan-dev libpulse-dev
steps:
- name: test
image: rust:bullseye
pull: always
commands:
- apt-get update -qq
- apt-get install -qqy $DEPENDENCIES
- rustup component add clippy rustfmt
- cargo fmt --check
- cargo test
- cargo clippy
- name: release
image: rust:bullseye
pull: always
commands:
- apt-get update -qq
- apt-get install -qqy $DEPENDENCIES
- cargo publish
when:
ref:
- refs/tags/v*
environment:
CARGO_REGISTRY_TOKEN:
from_secret: cargo_registry_token
- name: discord notification
image: appleboy/drone-discord
when:
status: [success, failure]
settings:
webhook_id:
from_secret: discord_webhook_id
webhook_token:
from_secret: discord_webhook_token
tts: true
message: >
{{#success build.status}}
{{repo.name}} build {{build.number}} succeeded: <{{build.link}}>
{{else}}
{{repo.name}} build {{build.number}} failed: <{{build.link}}>
{{/success}}

1
.envrc
View File

@ -1 +0,0 @@
use flake

View File

@ -1,39 +0,0 @@
name: Release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: rustfmt, clippy
- name: install Linux build dependencies
run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev libwayland-dev libclang-dev cmake
if: runner.os == 'linux'
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.1
- name: Publish
run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View File

@ -1,28 +0,0 @@
name: Test
on:
pull_request:
push:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v31
- name: Test
run: nix develop --command pre-commit run -a

3
.gitignore vendored
View File

@ -1,4 +1,3 @@
/target /target
*.dll *.dll
Cargo.lock Cargo.lock
.direnv

View File

@ -1,10 +0,0 @@
fail_fast: true
repos:
- repo: https://github.com/doublify/pre-commit-rust
rev: v1.0
hooks:
- id: fmt
args: [--, --check]
- id: cargo-check
args: [--bins, --examples]
- id: clippy

View File

@ -2,69 +2,6 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## Version 0.10.0 - 2025-05-16
### Miscellaneous Tasks
- Add Nix-based build and CI configuration.
- Update to Bevy 0.16.
## Version 0.9.1 - 2025-01-07
### Miscellaneous Tasks
- Don't set position if values are NaN.
## Version 0.9.0 - 2024-12-06
### Miscellaneous Tasks
- Upgrade to Bevy 0.15.
## Version 0.8.0 - 2024-12-02
### Bug Fixes
- Clear generator when source is cleared, and improve handling for changing source types.
### Features
- Add `Sound.playback_position` to support initializing new buffers at non-zero playback position.
### Miscellaneous Tasks
- Clean up code.
## Version 0.7.0 - 2024-07-07
### Miscellaneous Tasks
- Add pre-commit.
- Switch to Gitea Actions.
- Upgrade to Bevy 0.14.
## Version 0.6.0 - 2024-03-14
### Miscellaneous Tasks
- Upgrade Bevy to v0.13.
## Version 0.5.0 - 2024-02-09
### Bug Fixes
- Clean up `LastAudio` when `Sound` is removed, not `Source`.
### Features
- [**breaking**] Renamed `SynthizerSets::First` to `SynthizerSets::PreUpdate` and moved remaining systems into `PostUpdate`.
- [**breaking**] Removed `Sound.restart`. Clear `Sound.generator` for equivalent functionality.
### Miscellaneous Tasks
- Clean up code.
- Bump dependencies.
## Version 0.4.0 - 2023-07-16 ## Version 0.4.0 - 2023-07-16
### Miscellaneous Tasks ### Miscellaneous Tasks

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bevy_synthizer" name = "bevy_synthizer"
version = "0.10.0" version = "0.4.0"
authors = ["Nolan Darilek <nolan@thewordnerd.info>"] authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
description = "A Bevy plugin for Synthizer, a library for 3D audio and synthesis with a focus on games and VR applications" description = "A Bevy plugin for Synthizer, a library for 3D audio and synthesis with a focus on games and VR applications"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -10,12 +10,12 @@ repository = "https://labs.lightsout.games/projects/bevy_synthizer"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
bevy = { version = "0.16", default-features = false, features = ["bevy_asset"] } anyhow = "1"
bevy = { version = "0.11", default-features = false, features = ["bevy_asset"] }
synthizer = "0.5.6" synthizer = "0.5.6"
thiserror = "2"
[dev-dependencies] [dev-dependencies]
bevy = { version = "0.16", default-features = true } bevy = { version = "0.11", default-features = true }
[package.metadata.release] [package.metadata.release]
publish = false publish = false

View File

@ -1,6 +1,6 @@
use std::f32; use std::f32;
use bevy::{asset::LoadedFolder, prelude::*}; use bevy::{asset::LoadState, prelude::*};
use bevy_synthizer::*; use bevy_synthizer::*;
#[derive(Component, Deref, DerefMut)] #[derive(Component, Deref, DerefMut)]
@ -12,42 +12,51 @@ impl Default for RotationTimer {
} }
} }
#[derive(Resource, Default, Deref, DerefMut)] #[derive(Resource, Default)]
struct AssetHandles(Handle<LoadedFolder>); struct AssetHandles {
sounds: Vec<HandleUntyped>,
loaded: bool,
}
fn setup(asset_server: Res<AssetServer>, mut handles: ResMut<AssetHandles>) { fn setup(asset_server: Res<AssetServer>, mut handles: ResMut<AssetHandles>) {
**handles = asset_server.load_folder("."); handles.sounds = asset_server.load_folder(".").expect("Failed to load sfx");
} }
fn load_and_create( fn load_and_create(
mut commands: Commands, mut commands: Commands,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
handles: Res<AssetHandles>, mut handles: ResMut<AssetHandles>,
listeners: Query<&Listener>,
) { ) {
if !asset_server.is_loaded_with_dependencies(&**handles) { if handles.loaded {
return; return;
} }
if !listeners.is_empty() { handles.loaded = asset_server
return; .get_group_load_state(handles.sounds.iter().map(|handle| handle.id()))
== LoadState::Loaded;
if handles.loaded {
commands.spawn((
TransformBundle::default(),
Listener,
RotationTimer::default(),
));
let handle = handles.sounds[0].clone();
let buffer = asset_server.get_handle(handle);
commands.spawn((
TransformBundle::from(Transform::from_translation(Vec3::new(10., 0., 0.))),
Source::default(),
Sound {
audio: buffer.into(),
looping: true,
..default()
},
));
} }
commands.spawn((Transform::default(), Listener, RotationTimer::default()));
let handle = asset_server.load("footstep.wav");
commands.spawn((
Transform::from_translation(Vec3::new(10., 0., 0.)),
Source::default(),
Sound {
audio: handle.into(),
looping: true,
..default()
},
));
} }
fn rotate_listener(time: Res<Time>, mut query: Query<(&mut RotationTimer, &mut Transform)>) { fn rotate_listener(time: Res<Time>, mut query: Query<(&mut RotationTimer, &mut Transform)>) {
for (mut timer, mut transform) in query.iter_mut() { for (mut timer, mut transform) in query.iter_mut() {
timer.tick(time.delta()); timer.tick(time.delta());
let angle = f32::consts::PI * 2. * timer.fraction(); let angle = f32::consts::PI * 2. * timer.percent();
transform.rotation = Quat::from_rotation_z(angle); transform.rotation = Quat::from_rotation_z(angle);
} }
} }
@ -64,6 +73,9 @@ fn main() {
)) ))
.init_resource::<AssetHandles>() .init_resource::<AssetHandles>()
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, (load_and_create, rotate_listener)) .add_systems(
Update,
(bevy::window::close_on_esc, load_and_create, rotate_listener),
)
.run(); .run();
} }

View File

@ -13,12 +13,16 @@ impl Default for RotationTimer {
} }
fn setup(mut commands: Commands, context: Res<Context>) { fn setup(mut commands: Commands, context: Res<Context>) {
commands.spawn((Transform::default(), Listener, RotationTimer::default())); commands.spawn((
TransformBundle::default(),
Listener,
RotationTimer::default(),
));
let generator: syz::Generator = syz::FastSineBankGenerator::new_sine(&context, 440.) let generator: syz::Generator = syz::FastSineBankGenerator::new_sine(&context, 440.)
.expect("Failed to create generator") .expect("Failed to create generator")
.into(); .into();
commands.spawn(( commands.spawn((
Transform::from_translation(Vec3::new(10., 0., 0.)), TransformBundle::from(Transform::from_translation(Vec3::new(10., 0., 0.))),
Source::default(), Source::default(),
Sound { Sound {
audio: generator.into(), audio: generator.into(),
@ -31,7 +35,7 @@ fn setup(mut commands: Commands, context: Res<Context>) {
fn rotate_listener(time: Res<Time>, mut query: Query<(&mut RotationTimer, &mut Transform)>) { fn rotate_listener(time: Res<Time>, mut query: Query<(&mut RotationTimer, &mut Transform)>) {
for (mut timer, mut transform) in query.iter_mut() { for (mut timer, mut transform) in query.iter_mut() {
timer.tick(time.delta()); timer.tick(time.delta());
let angle = f32::consts::PI * 2. * timer.fraction(); let angle = f32::consts::PI * 2. * timer.percent();
transform.rotation = Quat::from_rotation_z(angle); transform.rotation = Quat::from_rotation_z(angle);
} }
} }
@ -47,6 +51,6 @@ fn main() {
}, },
)) ))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, rotate_listener) .add_systems(Update, (bevy::window::close_on_esc, rotate_listener))
.run(); .run();
} }

View File

@ -1,61 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1747312588,
"narHash": "sha256-MmJvj6mlWzeRwKGLcwmZpKaOPZ5nJb/6al5CXqJsgjo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b1bebd0fe266bbd1820019612ead889e96a8fa2d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,54 +0,0 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
utils,
}:
utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
devShell =
with pkgs;
mkShell.override { stdenv = pkgs.clangStdenv; } rec {
nativeBuildInputs = [
cargo
rustc
rustfmt
rustPackages.clippy
cmake
pkg-config
pre-commit
git-cliff
cargo-release
cargo-outdated
];
buildInputs = [
udev
alsa-lib
vulkan-loader
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.libXrandr
libxkbcommon
wayland
];
shellHook = ''
export LIBCLANG_PATH="${pkgs.libclang.lib}/lib"
export RUSTFLAGS="-C link-arg=-Wl,-rpath,${lib.makeLibraryPath buildInputs}"
pre-commit install
'';
RUST_SRC_PATH = rustPlatform.rustLibSrc;
};
}
);
}

View File

@ -2,43 +2,40 @@
use std::collections::HashMap; use std::collections::HashMap;
use bevy::{ use bevy::{
asset::{io::Reader, AssetLoader, LoadContext}, asset::{AssetLoader, LoadContext, LoadedAsset},
prelude::*, prelude::*,
reflect::{TypePath, TypeUuid},
transform::TransformSystem, transform::TransformSystem,
utils::BoxedFuture,
}; };
pub use synthizer as syz; pub use synthizer as syz;
use thiserror::Error;
#[derive(Asset, Clone, Debug, Deref, DerefMut, PartialEq, Eq, TypePath)] #[derive(Clone, Debug, Deref, DerefMut, PartialEq, Eq, TypePath, TypeUuid)]
#[uuid = "6b6b533a-bb1f-11ec-bda2-00155d8fdde9"]
pub struct Buffer(syz::Buffer); pub struct Buffer(syz::Buffer);
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
struct BufferAssetLoader; struct BufferAssetLoader;
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum BufferAssetLoaderError {
#[error("Could not load asset: {0}")]
Io(#[from] std::io::Error),
#[error("Synthizer error: {0}")]
SynthizerError(#[from] synthizer::Error),
}
impl AssetLoader for BufferAssetLoader { impl AssetLoader for BufferAssetLoader {
type Asset = Buffer; fn load<'a>(
type Settings = (); &'a self,
type Error = BufferAssetLoaderError; bytes: &'a [u8],
load_context: &'a mut LoadContext,
async fn load( ) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
&self, Box::pin(async move {
reader: &mut dyn Reader, let buffer: Option<Buffer> =
_settings: &(), match load_context.path().extension().unwrap().to_str().unwrap() {
_load_context: &mut LoadContext<'_>, "flac" | "mp3" | "wav" => {
) -> Result<Self::Asset, Self::Error> { syz::Buffer::from_encoded_data(bytes).map(Buffer).ok()
let mut bytes = Vec::new(); }
reader.read_to_end(&mut bytes).await?; _ => None,
let buffer = syz::Buffer::from_encoded_data(&bytes).map(Buffer)?; };
Ok(buffer) if let Some(buffer) = buffer {
load_context.set_default_asset(LoadedAsset::new(buffer));
}
Ok(())
})
} }
fn extensions(&self) -> &[&str] { fn extensions(&self) -> &[&str] {
@ -155,8 +152,8 @@ pub struct Sound {
pub gain: f64, pub gain: f64,
pub pitch: f64, pub pitch: f64,
pub looping: bool, pub looping: bool,
pub playback_position: f64,
pub paused: bool, pub paused: bool,
pub restart: bool,
pub generator: Option<syz::Generator>, pub generator: Option<syz::Generator>,
} }
@ -167,8 +164,8 @@ impl Default for Sound {
gain: 1., gain: 1.,
pitch: 1., pitch: 1.,
looping: false, looping: false,
playback_position: default(),
paused: false, paused: false,
restart: false,
generator: None, generator: None,
} }
} }
@ -188,7 +185,7 @@ fn update_listener(
context: ResMut<Context>, context: ResMut<Context>,
listener: Query<Option<&GlobalTransform>, With<Listener>>, listener: Query<Option<&GlobalTransform>, With<Listener>>,
) { ) {
if let Ok(transform) = listener.single() { if let Ok(transform) = listener.get_single() {
let transform: Transform = transform let transform: Transform = transform
.map(|v| { .map(|v| {
let transform: Transform = (*v).into(); let transform: Transform = (*v).into();
@ -230,109 +227,101 @@ fn add_source_handle(
)>, )>,
) { ) {
for (mut source, panner_strategy, transform, angular_pan, scalar_pan) in &mut query { for (mut source, panner_strategy, transform, angular_pan, scalar_pan) in &mut query {
if source.handle.is_some() { if source.handle.is_none() {
continue; let panner_strategy = panner_strategy.cloned().unwrap_or_default();
let handle: syz::Source = if let Some(transform) = transform {
let translation = transform.translation();
syz::Source3D::new(
&context,
*panner_strategy,
(
translation.x as f64,
translation.y as f64,
translation.z as f64,
),
)
.expect("Failed to create source")
.into()
} else if let Some(scalar_pan) = scalar_pan {
syz::ScalarPannedSource::new(&context, *panner_strategy, **scalar_pan)
.expect("Failed to create source")
.into()
} else if let Some(angular_pan) = angular_pan {
syz::AngularPannedSource::new(
&context,
*panner_strategy,
angular_pan.azimuth,
angular_pan.elevation,
)
.expect("Failed to create source")
.into()
} else {
syz::DirectSource::new(&context)
.expect("Failed to create source")
.into()
};
source.handle = Some(handle);
} }
let panner_strategy = panner_strategy.cloned().unwrap_or_default();
let handle: syz::Source = if let Some(transform) = transform {
let translation = transform.translation();
syz::Source3D::new(
&context,
*panner_strategy,
(
translation.x as f64,
translation.y as f64,
translation.z as f64,
),
)
.expect("Failed to create source")
.into()
} else if let Some(scalar_pan) = scalar_pan {
syz::ScalarPannedSource::new(&context, *panner_strategy, **scalar_pan)
.expect("Failed to create source")
.into()
} else if let Some(angular_pan) = angular_pan {
syz::AngularPannedSource::new(
&context,
*panner_strategy,
angular_pan.azimuth,
angular_pan.elevation,
)
.expect("Failed to create source")
.into()
} else {
syz::DirectSource::new(&context)
.expect("Failed to create source")
.into()
};
source.handle = Some(handle);
} }
} }
fn add_generator( fn add_generator(
context: Res<Context>, context: Res<Context>,
buffers: Res<Assets<Buffer>>, buffers: Res<Assets<Buffer>>,
mut query: Query<(Entity, Option<&ChildOf>, &mut Sound)>, mut query: Query<(Entity, Option<&Parent>, &mut Sound)>,
mut sources: Query<&mut Source>, mut sources: Query<&mut Source>,
parents: Query<&ChildOf>, parents: Query<&Parent>,
) { ) {
for (entity, parent, mut sound) in &mut query { for (entity, parent, mut sound) in &mut query {
if sound.generator.is_some() { if sound.generator.is_none() {
continue; let mut source = if let Ok(s) = sources.get_mut(entity) {
} Some(s)
let mut source = if let Ok(s) = sources.get_mut(entity) { } else if let Some(parent) = parent {
Some(s) let mut parent: Option<&Parent> = Some(parent);
} else if parent.is_some() { let mut target = None;
let mut parent = parent; while let Some(p) = parent {
let mut target = None; if sources.get(**p).is_ok() {
while let Some(p) = parent { target = Some(**p);
if sources.get(p.parent()).is_ok() { break;
target = Some(p.parent());
break;
}
parent = parents.get(p.parent()).ok();
}
target.map(|v| sources.get_mut(v).unwrap())
} else {
None
};
if let Some(source) = source.as_mut() {
if let Some(handle) = source.handle.as_mut() {
let generator: Option<syz::Generator> = match &sound.audio {
Audio::Buffer(buffer) => {
if let Some(b) = buffers.get(buffer) {
let generator = syz::BufferGenerator::new(&context)
.expect("Failed to create generator");
generator.buffer().set(&**b).expect("Unable to set buffer");
assert!(sound.playback_position >= 0.);
generator
.playback_position()
.set(sound.playback_position)
.expect("Failed to set playback position");
Some(generator.into())
} else {
None
}
} }
Audio::Generator(generator) => Some(generator.clone()), parent = parents.get(**p).ok();
}; }
let Some(generator) = generator else { target.map(|v| sources.get_mut(v).unwrap())
continue; } else {
}; None
assert!(sound.gain >= 0.); };
generator if let Some(source) = source.as_mut() {
.gain() if let Some(handle) = source.handle.as_mut() {
.set(sound.gain) let generator: Option<syz::Generator> = match &sound.audio {
.expect("Failed to set gain"); Audio::Buffer(buffer) => {
assert!(sound.pitch > 0. && sound.pitch <= 2.); if let Some(b) = buffers.get(buffer) {
generator let generator = syz::BufferGenerator::new(&context)
.pitch_bend() .expect("Failed to create generator");
.set(sound.pitch) generator.buffer().set(&**b).expect("Unable to set buffer");
.expect("Failed to set pitch"); Some(generator.into())
handle } else {
.add_generator(generator.handle()) None
.expect("Unable to add generator"); }
sound.generator = Some(generator); }
Audio::Generator(generator) => Some(generator.clone()),
};
if let Some(generator) = generator {
assert!(sound.gain >= 0.);
generator
.gain()
.set(sound.gain)
.expect("Failed to set gain");
assert!(sound.pitch > 0. && sound.pitch <= 2.);
generator
.pitch_bend()
.set(sound.pitch)
.expect("Failed to set pitch");
handle
.add_generator(generator.handle())
.expect("Unable to add generator");
sound.generator = Some(generator);
}
}
} }
} }
} }
@ -341,7 +330,7 @@ fn add_generator(
fn add_sound_without_source( fn add_sound_without_source(
mut commands: Commands, mut commands: Commands,
query: Query<Entity, (Added<Sound>, Without<Source>)>, query: Query<Entity, (Added<Sound>, Without<Source>)>,
parents: Query<(&ChildOf, Option<&Source>)>, parents: Query<(&Parent, Option<&Source>)>,
) { ) {
for entity in &query { for entity in &query {
let mut has_source = false; let mut has_source = false;
@ -351,7 +340,7 @@ fn add_sound_without_source(
has_source = true; has_source = true;
break; break;
} }
target = parent.parent(); target = **parent;
} }
if !has_source { if !has_source {
commands.entity(entity).insert(Source::default()); commands.entity(entity).insert(Source::default());
@ -368,9 +357,8 @@ fn swap_buffers(
) { ) {
for (entity, mut sound) in &mut query { for (entity, mut sound) in &mut query {
if let Some(l) = last_audio.get(&entity) { if let Some(l) = last_audio.get(&entity) {
if sound.generator.is_some() && sound.audio != *l { if sound.audio != *l {
sound.generator = None; sound.generator = None;
sound.playback_position = 0.;
} }
} }
last_audio.insert(entity, sound.audio.clone()); last_audio.insert(entity, sound.audio.clone());
@ -388,15 +376,14 @@ fn change_panner_strategy(
check.push(entity); check.push(entity);
} }
} }
for entity in removed.read() { for entity in removed.iter() {
check.push(entity); check.push(entity);
} }
for entity in check.iter() { for entity in check.iter() {
let Ok(mut source) = sources.get_mut(*entity) else { if let Ok(mut source) = sources.get_mut(*entity) {
continue; if source.handle.is_some() {
}; source.handle = None;
if source.handle.is_some() { }
source.handle = None;
} }
} }
} }
@ -414,7 +401,6 @@ fn update_source_properties(
Option<&AngularPan>, Option<&AngularPan>,
Option<&ScalarPan>, Option<&ScalarPan>,
Option<&GlobalTransform>, Option<&GlobalTransform>,
Option<&mut Sound>,
)>, )>,
) { ) {
for ( for (
@ -428,20 +414,16 @@ fn update_source_properties(
angular_pan, angular_pan,
scalar_pan, scalar_pan,
transform, transform,
sound,
) in &mut query ) in &mut query
{ {
let Source { gain, .. } = *source; let Source { gain, .. } = *source;
assert!(gain >= 0.); assert!(gain >= 0.);
let Some(handle) = source.handle.as_mut() else { if let Some(handle) = source.handle.as_mut() {
continue; handle.gain().set(gain).expect("Failed to set gain");
}; let mut clear_source = false;
handle.gain().set(gain).expect("Failed to set gain");
let mut clear_source = false;
if let Some(source) = handle.cast_to::<syz::Source3D>().expect("Failed to cast") {
if let Some(transform) = transform { if let Some(transform) = transform {
let translation = transform.translation(); if let Some(source) = handle.cast_to::<syz::Source3D>().expect("Failed to cast") {
if !translation.x.is_nan() && !translation.y.is_nan() && !translation.z.is_nan() { let translation = transform.translation();
source source
.position() .position()
.set(( .set((
@ -450,106 +432,92 @@ fn update_source_properties(
translation.z as f64, translation.z as f64,
)) ))
.expect("Failed to set position"); .expect("Failed to set position");
let distance_model = distance_model
.cloned()
.map(|v| *v)
.unwrap_or_else(|| context.default_distance_model().get().unwrap());
source
.distance_model()
.set(distance_model)
.expect("Failed to set distance_model");
let distance_ref = distance_ref
.map(|v| **v)
.unwrap_or_else(|| context.default_distance_ref().get().unwrap());
assert!(distance_ref >= 0.);
source
.distance_ref()
.set(distance_ref)
.expect("Failed to set distance_ref");
let distance_max = distance_max
.map(|v| **v)
.unwrap_or_else(|| context.default_distance_max().get().unwrap());
assert!(distance_max >= 0.);
source
.distance_max()
.set(distance_max)
.expect("Failed to set distance_max");
let rolloff = rolloff
.map(|v| **v)
.unwrap_or_else(|| context.default_rolloff().get().unwrap());
assert!(rolloff >= 0.);
source
.rolloff()
.set(rolloff)
.expect("Failed to set rolloff");
let closeness_boost = closeness_boost
.map(|v| **v)
.unwrap_or_else(|| context.default_closeness_boost().get().unwrap());
assert!(closeness_boost >= 0.);
source
.closeness_boost()
.set(closeness_boost)
.expect("Failed to set closeness_boost");
let closeness_boost_distance =
closeness_boost_distance.map(|v| **v).unwrap_or_else(|| {
context.default_closeness_boost_distance().get().unwrap()
});
assert!(closeness_boost_distance >= 0.);
source
.closeness_boost_distance()
.set(closeness_boost_distance)
.expect("Failed to set closeness_boost_distance");
} else {
clear_source = true;
}
} else if let Some(angular_pan) = angular_pan {
if let Some(source) = handle
.cast_to::<syz::AngularPannedSource>()
.expect("Failed to cast")
{
assert!(angular_pan.azimuth >= 0. && angular_pan.azimuth <= 360.);
source
.azimuth()
.set(angular_pan.azimuth)
.expect("Failed to set azimuth");
assert!(angular_pan.elevation >= -90. && angular_pan.elevation <= 90.);
source
.elevation()
.set(angular_pan.elevation)
.expect("Failed to set elevation");
} else {
clear_source = true;
}
} else if let Some(scalar_pan) = scalar_pan {
if let Some(source) = handle
.cast_to::<syz::ScalarPannedSource>()
.expect("Failed to cast")
{
assert!(**scalar_pan >= -1. && **scalar_pan <= 1.);
source
.panning_scalar()
.set(**scalar_pan)
.expect("Failed to set scalar panning");
} else {
clear_source = true;
} }
let distance_model = distance_model
.cloned()
.map(|v| *v)
.unwrap_or_else(|| context.default_distance_model().get().unwrap());
source
.distance_model()
.set(distance_model)
.expect("Failed to set distance_model");
let distance_ref = distance_ref
.map(|v| **v)
.unwrap_or_else(|| context.default_distance_ref().get().unwrap());
assert!(distance_ref >= 0.);
source
.distance_ref()
.set(distance_ref)
.expect("Failed to set distance_ref");
let distance_max = distance_max
.map(|v| **v)
.unwrap_or_else(|| context.default_distance_max().get().unwrap());
assert!(distance_max >= 0.);
source
.distance_max()
.set(distance_max)
.expect("Failed to set distance_max");
let rolloff = rolloff
.map(|v| **v)
.unwrap_or_else(|| context.default_rolloff().get().unwrap());
assert!(rolloff >= 0.);
assert!(rolloff >= 0.);
source
.rolloff()
.set(rolloff)
.expect("Failed to set rolloff");
let closeness_boost = closeness_boost
.map(|v| **v)
.unwrap_or_else(|| context.default_closeness_boost().get().unwrap());
assert!(closeness_boost >= 0.);
source
.closeness_boost()
.set(closeness_boost)
.expect("Failed to set closeness_boost");
let closeness_boost_distance = closeness_boost_distance
.map(|v| **v)
.unwrap_or_else(|| context.default_closeness_boost_distance().get().unwrap());
assert!(closeness_boost_distance >= 0.);
source
.closeness_boost_distance()
.set(closeness_boost_distance)
.expect("Failed to set closeness_boost_distance");
} else {
clear_source = true;
} }
} else if let Some(source) = handle if clear_source {
.cast_to::<syz::AngularPannedSource>() source.handle = None;
.expect("Failed to cast")
{
if let Some(angular_pan) = angular_pan {
assert!(angular_pan.azimuth >= 0. && angular_pan.azimuth <= 360.);
source
.azimuth()
.set(angular_pan.azimuth)
.expect("Failed to set azimuth");
assert!(angular_pan.elevation >= -90. && angular_pan.elevation <= 90.);
source
.elevation()
.set(angular_pan.elevation)
.expect("Failed to set elevation");
} else {
clear_source = true;
}
} else if let Some(source) = handle
.cast_to::<syz::ScalarPannedSource>()
.expect("Failed to cast")
{
if let Some(scalar_pan) = scalar_pan {
assert!(**scalar_pan >= -1. && **scalar_pan <= 1.);
source
.panning_scalar()
.set(**scalar_pan)
.expect("Failed to set scalar panning");
} else {
clear_source = true;
}
} else if handle
.cast_to::<syz::DirectSource>()
.expect("Failed to cast")
.is_some()
{
if transform.is_some() || angular_pan.is_some() || scalar_pan.is_some() {
clear_source = true;
}
} else if source.handle.is_some() {
clear_source = true;
}
if clear_source {
source.handle = None;
if let Some(mut sound) = sound {
sound.generator = None;
sound.playback_position = 0.;
} }
} }
} }
@ -565,66 +533,65 @@ fn update_sound_properties(mut query: Query<&mut Sound>) {
} = *sound; } = *sound;
assert!(gain >= 0.); assert!(gain >= 0.);
assert!(pitch > 0. && pitch <= 2.); assert!(pitch > 0. && pitch <= 2.);
let Some(generator) = &sound.generator else { if sound.restart {
continue; if let Some(generator) = sound.generator.as_mut() {
}; if let Some(generator) = generator
if let Some(generator) = generator .cast_to::<syz::BufferGenerator>()
.cast_to::<syz::BufferGenerator>() .expect("Failed to cast")
.expect("Failed to cast") {
{ generator
sound.playback_position = generator .playback_position()
.playback_position() .set(0.)
.get() .expect("Failed to restart");
.expect("Failed to getplayback position"); }
}
sound.restart = false;
} }
let Some(generator) = sound.generator.as_mut() else { if let Some(generator) = sound.generator.as_mut() {
continue; generator.gain().set(gain).expect("Failed to set gain");
};
generator.gain().set(gain).expect("Failed to set gain");
generator
.pitch_bend()
.set(pitch)
.expect("Failed to set pitch");
if let Some(generator) = generator
.cast_to::<syz::BufferGenerator>()
.expect("Failed to cast")
{
generator generator
.looping() .pitch_bend()
.set(looping) .set(pitch)
.expect("Failed to set looping"); .expect("Failed to set pitch");
if let Some(generator) = generator
.cast_to::<syz::BufferGenerator>()
.expect("Failed to cast")
{
generator
.looping()
.set(looping)
.expect("Failed to set looping");
}
} }
} }
} }
fn update_source_playback_state(query: Query<&Source>) { fn update_source_playback_state(query: Query<&Source>) {
for source in &query { for source in &query {
let Some(handle) = &source.handle else { if let Some(handle) = &source.handle {
continue; if source.paused {
}; handle.pause().expect("Failed to pause");
if source.paused { } else {
handle.pause().expect("Failed to pause"); handle.play().expect("Failed to play");
} else { }
handle.play().expect("Failed to play");
} }
} }
} }
fn update_sound_playback_state(query: Query<&Sound>) { fn update_sound_playback_state(query: Query<&Sound>) {
for sound in &query { for sound in &query {
let Some(generator) = &sound.generator else { if let Some(generator) = &sound.generator {
continue; if sound.paused {
}; generator.pause().expect("Failed to pause");
if sound.paused { } else {
generator.pause().expect("Failed to pause"); generator.play().expect("Failed to play");
} else { }
generator.play().expect("Failed to play");
} }
} }
} }
fn remove_sound(mut last_buffer: ResMut<LastAudio>, mut removed: RemovedComponents<Sound>) { fn remove_sound(mut last_buffer: ResMut<LastAudio>, mut removed: RemovedComponents<Source>) {
for entity in removed.read() { for entity in removed.iter() {
last_buffer.remove(&entity); last_buffer.remove(&entity);
} }
} }
@ -699,24 +666,27 @@ fn events(
mut output: EventWriter<SynthizerEvent>, mut output: EventWriter<SynthizerEvent>,
) { ) {
context.get_events().for_each(|event| { context.get_events().for_each(|event| {
let Ok(event) = event else { if let Ok(event) = event {
return; let mut matched = false;
}; for (entity, sound) in &sounds {
for (entity, sound) in &sounds { if let Some(generator) = &sound.generator {
let Some(generator) = &sound.generator else { if *generator.handle() == event.source {
continue; matched = true;
}; match event.r#type {
if *generator.handle() == event.source { syz::EventType::Finished => {
match event.r#type { output.send(SynthizerEvent::Finished(entity));
syz::EventType::Finished => { }
output.write(SynthizerEvent::Finished(entity)); syz::EventType::Looped => {
output.send(SynthizerEvent::Looped(entity));
}
_ => {}
}
break;
} }
syz::EventType::Looped => {
output.write(SynthizerEvent::Looped(entity));
}
_ => {}
} }
break; }
if !matched {
println!("No match");
} }
} }
}); });
@ -724,7 +694,7 @@ fn events(
#[derive(SystemSet, Clone, Hash, Debug, PartialEq, Eq)] #[derive(SystemSet, Clone, Hash, Debug, PartialEq, Eq)]
pub enum SynthizerSets { pub enum SynthizerSets {
PreUpdate, First,
UpdateHandles, UpdateHandles,
UpdateProperties, UpdateProperties,
UpdateState, UpdateState,
@ -749,10 +719,10 @@ pub struct SynthizerPlugin {
impl Plugin for SynthizerPlugin { impl Plugin for SynthizerPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
if !app.world().contains_resource::<SynthizerPlugin>() { if !app.world.contains_resource::<SynthizerPlugin>() {
app.insert_resource(*self); app.insert_resource(*self);
} }
let config = *app.world().get_resource::<SynthizerPlugin>().unwrap(); let config = *app.world.get_resource::<SynthizerPlugin>().unwrap();
let mut syz_config = syz::LibraryConfig::new(); let mut syz_config = syz::LibraryConfig::new();
syz_config.log_level(config.log_level); syz_config.log_level(config.log_level);
if config.log_to_stderr { if config.log_to_stderr {
@ -774,7 +744,7 @@ impl Plugin for SynthizerPlugin {
}; };
context.enable_events().expect("Failed to enable events"); context.enable_events().expect("Failed to enable events");
let context = Context(context); let context = Context(context);
app.init_asset::<Buffer>() app.add_asset::<Buffer>()
.init_asset_loader::<BufferAssetLoader>() .init_asset_loader::<BufferAssetLoader>()
.register_type::<DistanceRef>() .register_type::<DistanceRef>()
.register_type::<DistanceMax>() .register_type::<DistanceMax>()
@ -791,15 +761,23 @@ impl Plugin for SynthizerPlugin {
.add_event::<SynthizerEvent>() .add_event::<SynthizerEvent>()
.add_systems( .add_systems(
PreUpdate, PreUpdate,
(sync_config, swap_buffers, change_panner_strategy) (
.in_set(SynthizerSets::PreUpdate), sync_config,
swap_buffers,
change_panner_strategy,
add_sound_without_source,
)
.in_set(SynthizerSets::First),
)
.configure_set(
PreUpdate,
SynthizerSets::First.before(SynthizerSets::UpdateHandles),
) )
.add_systems( .add_systems(
PostUpdate, PostUpdate,
(add_sound_without_source, add_source_handle, add_generator) (add_source_handle, add_generator).in_set(SynthizerSets::UpdateHandles),
.in_set(SynthizerSets::UpdateHandles),
) )
.configure_sets( .configure_set(
PostUpdate, PostUpdate,
SynthizerSets::UpdateHandles.before(SynthizerSets::UpdateProperties), SynthizerSets::UpdateHandles.before(SynthizerSets::UpdateProperties),
) )
@ -813,7 +791,7 @@ impl Plugin for SynthizerPlugin {
.in_set(SynthizerSets::UpdateProperties) .in_set(SynthizerSets::UpdateProperties)
.after(TransformSystem::TransformPropagate), .after(TransformSystem::TransformPropagate),
) )
.configure_sets( .configure_set(
PostUpdate, PostUpdate,
SynthizerSets::UpdateProperties.before(SynthizerSets::UpdateState), SynthizerSets::UpdateProperties.before(SynthizerSets::UpdateState),
) )
@ -822,7 +800,7 @@ impl Plugin for SynthizerPlugin {
(update_source_playback_state, update_sound_playback_state) (update_source_playback_state, update_sound_playback_state)
.in_set(SynthizerSets::UpdateState), .in_set(SynthizerSets::UpdateState),
) )
.configure_sets( .configure_set(
PostUpdate, PostUpdate,
SynthizerSets::UpdateState.before(SynthizerSets::Last), SynthizerSets::UpdateState.before(SynthizerSets::Last),
) )