Compare commits

..

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

13 changed files with 310 additions and 536 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

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
/target
*.dll
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,53 +2,6 @@
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

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_synthizer"
version = "0.10.0"
version = "0.5.0"
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"
license = "MIT OR Apache-2.0"
@ -10,12 +10,11 @@ repository = "https://labs.lightsout.games/projects/bevy_synthizer"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = { version = "0.16", default-features = false, features = ["bevy_asset"] }
bevy = { version = "0.12", default-features = false, features = ["bevy_asset"] }
synthizer = "0.5.6"
thiserror = "2"
[dev-dependencies]
bevy = { version = "0.16", default-features = true }
bevy = { version = "0.12", default-features = true }
[package.metadata.release]
publish = false

View File

@ -31,10 +31,14 @@ fn load_and_create(
if !listeners.is_empty() {
return;
}
commands.spawn((Transform::default(), Listener, RotationTimer::default()));
commands.spawn((
TransformBundle::default(),
Listener,
RotationTimer::default(),
));
let handle = asset_server.load("footstep.wav");
commands.spawn((
Transform::from_translation(Vec3::new(10., 0., 0.)),
TransformBundle::from(Transform::from_translation(Vec3::new(10., 0., 0.))),
Source::default(),
Sound {
audio: handle.into(),
@ -47,7 +51,7 @@ fn load_and_create(
fn rotate_listener(time: Res<Time>, mut query: Query<(&mut RotationTimer, &mut Transform)>) {
for (mut timer, mut transform) in query.iter_mut() {
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);
}
}
@ -64,6 +68,9 @@ fn main() {
))
.init_resource::<AssetHandles>()
.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();
}

View File

@ -13,12 +13,16 @@ impl Default for RotationTimer {
}
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.)
.expect("Failed to create generator")
.into();
commands.spawn((
Transform::from_translation(Vec3::new(10., 0., 0.)),
TransformBundle::from(Transform::from_translation(Vec3::new(10., 0., 0.))),
Source::default(),
Sound {
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)>) {
for (mut timer, mut transform) in query.iter_mut() {
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);
}
}
@ -47,6 +51,6 @@ fn main() {
},
))
.add_systems(Startup, setup)
.add_systems(Update, rotate_listener)
.add_systems(Update, (bevy::window::close_on_esc, rotate_listener))
.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,12 +2,16 @@
use std::collections::HashMap;
use bevy::{
asset::{io::Reader, AssetLoader, LoadContext},
asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext},
prelude::*,
reflect::TypePath,
transform::TransformSystem,
utils::{
thiserror::{self, Error},
BoxedFuture,
},
};
pub use synthizer as syz;
use thiserror::Error;
#[derive(Asset, Clone, Debug, Deref, DerefMut, PartialEq, Eq, TypePath)]
pub struct Buffer(syz::Buffer);
@ -29,16 +33,18 @@ impl AssetLoader for BufferAssetLoader {
type Settings = ();
type Error = BufferAssetLoaderError;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
fn load<'a>(
&'a self,
reader: &'a mut Reader,
_settings: &'a (),
_load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
Box::pin(async move {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let buffer = syz::Buffer::from_encoded_data(&bytes).map(Buffer)?;
Ok(buffer)
})
}
fn extensions(&self) -> &[&str] {
@ -155,7 +161,6 @@ pub struct Sound {
pub gain: f64,
pub pitch: f64,
pub looping: bool,
pub playback_position: f64,
pub paused: bool,
pub generator: Option<syz::Generator>,
}
@ -167,7 +172,6 @@ impl Default for Sound {
gain: 1.,
pitch: 1.,
looping: false,
playback_position: default(),
paused: false,
generator: None,
}
@ -188,7 +192,7 @@ fn update_listener(
context: ResMut<Context>,
listener: Query<Option<&GlobalTransform>, With<Listener>>,
) {
if let Ok(transform) = listener.single() {
if let Ok(transform) = listener.get_single() {
let transform: Transform = transform
.map(|v| {
let transform: Transform = (*v).into();
@ -230,9 +234,7 @@ fn add_source_handle(
)>,
) {
for (mut source, panner_strategy, transform, angular_pan, scalar_pan) in &mut query {
if source.handle.is_some() {
continue;
}
if source.handle.is_none() {
let panner_strategy = panner_strategy.cloned().unwrap_or_default();
let handle: syz::Source = if let Some(transform) = transform {
let translation = transform.translation();
@ -268,29 +270,28 @@ fn add_source_handle(
source.handle = Some(handle);
}
}
}
fn add_generator(
context: Res<Context>,
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>,
parents: Query<&ChildOf>,
parents: Query<&Parent>,
) {
for (entity, parent, mut sound) in &mut query {
if sound.generator.is_some() {
continue;
}
if sound.generator.is_none() {
let mut source = if let Ok(s) = sources.get_mut(entity) {
Some(s)
} else if parent.is_some() {
let mut parent = parent;
let mut target = None;
while let Some(p) = parent {
if sources.get(p.parent()).is_ok() {
target = Some(p.parent());
if sources.get(**p).is_ok() {
target = Some(**p);
break;
}
parent = parents.get(p.parent()).ok();
parent = parents.get(**p).ok();
}
target.map(|v| sources.get_mut(v).unwrap())
} else {
@ -304,11 +305,6 @@ fn add_generator(
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
@ -316,9 +312,7 @@ fn add_generator(
}
Audio::Generator(generator) => Some(generator.clone()),
};
let Some(generator) = generator else {
continue;
};
if let Some(generator) = generator {
assert!(sound.gain >= 0.);
generator
.gain()
@ -337,11 +331,13 @@ fn add_generator(
}
}
}
}
}
fn add_sound_without_source(
mut commands: Commands,
query: Query<Entity, (Added<Sound>, Without<Source>)>,
parents: Query<(&ChildOf, Option<&Source>)>,
parents: Query<(&Parent, Option<&Source>)>,
) {
for entity in &query {
let mut has_source = false;
@ -351,7 +347,7 @@ fn add_sound_without_source(
has_source = true;
break;
}
target = parent.parent();
target = **parent;
}
if !has_source {
commands.entity(entity).insert(Source::default());
@ -370,7 +366,6 @@ fn swap_buffers(
if let Some(l) = last_audio.get(&entity) {
if sound.generator.is_some() && sound.audio != *l {
sound.generator = None;
sound.playback_position = 0.;
}
}
last_audio.insert(entity, sound.audio.clone());
@ -392,14 +387,13 @@ fn change_panner_strategy(
check.push(entity);
}
for entity in check.iter() {
let Ok(mut source) = sources.get_mut(*entity) else {
continue;
};
if let Ok(mut source) = sources.get_mut(*entity) {
if source.handle.is_some() {
source.handle = None;
}
}
}
}
fn update_source_properties(
context: Res<Context>,
@ -414,7 +408,6 @@ fn update_source_properties(
Option<&AngularPan>,
Option<&ScalarPan>,
Option<&GlobalTransform>,
Option<&mut Sound>,
)>,
) {
for (
@ -428,20 +421,16 @@ fn update_source_properties(
angular_pan,
scalar_pan,
transform,
sound,
) in &mut query
{
let Source { gain, .. } = *source;
assert!(gain >= 0.);
let Some(handle) = source.handle.as_mut() else {
continue;
};
if let Some(handle) = source.handle.as_mut() {
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(source) = handle.cast_to::<syz::Source3D>().expect("Failed to cast") {
let translation = transform.translation();
if !translation.x.is_nan() && !translation.y.is_nan() && !translation.z.is_nan() {
source
.position()
.set((
@ -450,7 +439,6 @@ fn update_source_properties(
translation.z as f64,
))
.expect("Failed to set position");
}
let distance_model = distance_model
.cloned()
.map(|v| *v)
@ -479,7 +467,6 @@ fn update_source_properties(
.map(|v| **v)
.unwrap_or_else(|| context.default_rolloff().get().unwrap());
assert!(rolloff >= 0.);
assert!(rolloff >= 0.);
source
.rolloff()
.set(rolloff)
@ -492,9 +479,10 @@ fn update_source_properties(
.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());
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()
@ -503,11 +491,11 @@ fn update_source_properties(
} else {
clear_source = true;
}
} else if let Some(source) = handle
} else if let Some(angular_pan) = angular_pan {
if let Some(source) = handle
.cast_to::<syz::AngularPannedSource>()
.expect("Failed to cast")
{
if let Some(angular_pan) = angular_pan {
assert!(angular_pan.azimuth >= 0. && angular_pan.azimuth <= 360.);
source
.azimuth()
@ -521,11 +509,11 @@ fn update_source_properties(
} else {
clear_source = true;
}
} else if let Some(source) = handle
} else if let Some(scalar_pan) = scalar_pan {
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()
@ -534,22 +522,9 @@ fn update_source_properties(
} 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,21 +540,7 @@ fn update_sound_properties(mut query: Query<&mut Sound>) {
} = *sound;
assert!(gain >= 0.);
assert!(pitch > 0. && pitch <= 2.);
let Some(generator) = &sound.generator else {
continue;
};
if let Some(generator) = generator
.cast_to::<syz::BufferGenerator>()
.expect("Failed to cast")
{
sound.playback_position = generator
.playback_position()
.get()
.expect("Failed to getplayback position");
}
let Some(generator) = sound.generator.as_mut() else {
continue;
};
if let Some(generator) = sound.generator.as_mut() {
generator.gain().set(gain).expect("Failed to set gain");
generator
.pitch_bend()
@ -596,12 +557,11 @@ fn update_sound_properties(mut query: Query<&mut Sound>) {
}
}
}
}
fn update_source_playback_state(query: Query<&Source>) {
for source in &query {
let Some(handle) = &source.handle else {
continue;
};
if let Some(handle) = &source.handle {
if source.paused {
handle.pause().expect("Failed to pause");
} else {
@ -609,12 +569,11 @@ fn update_source_playback_state(query: Query<&Source>) {
}
}
}
}
fn update_sound_playback_state(query: Query<&Sound>) {
for sound in &query {
let Some(generator) = &sound.generator else {
continue;
};
if let Some(generator) = &sound.generator {
if sound.paused {
generator.pause().expect("Failed to pause");
} else {
@ -622,6 +581,7 @@ fn update_sound_playback_state(query: Query<&Sound>) {
}
}
}
}
fn remove_sound(mut last_buffer: ResMut<LastAudio>, mut removed: RemovedComponents<Sound>) {
for entity in removed.read() {
@ -699,26 +659,24 @@ fn events(
mut output: EventWriter<SynthizerEvent>,
) {
context.get_events().for_each(|event| {
let Ok(event) = event else {
return;
};
if let Ok(event) = event {
for (entity, sound) in &sounds {
let Some(generator) = &sound.generator else {
continue;
};
if let Some(generator) = &sound.generator {
if *generator.handle() == event.source {
match event.r#type {
syz::EventType::Finished => {
output.write(SynthizerEvent::Finished(entity));
output.send(SynthizerEvent::Finished(entity));
}
syz::EventType::Looped => {
output.write(SynthizerEvent::Looped(entity));
output.send(SynthizerEvent::Looped(entity));
}
_ => {}
}
break;
}
}
}
}
});
}
@ -749,10 +707,10 @@ pub struct SynthizerPlugin {
impl Plugin for SynthizerPlugin {
fn build(&self, app: &mut App) {
if !app.world().contains_resource::<SynthizerPlugin>() {
if !app.world.contains_resource::<SynthizerPlugin>() {
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();
syz_config.log_level(config.log_level);
if config.log_to_stderr {