Initial commit.

This commit is contained in:
Nolan Darilek 2022-05-18 13:04:58 -05:00
commit 8aca79dfab
6 changed files with 482 additions and 0 deletions

3
.gitignore vendored Normal file
View File

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

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "bevy_synthizer"
version = "0.1.0"
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
license = "MIT OR Apache-2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
bevy = { version = "0.7", default-features = false }
derive_more = "0.99"
synthizer = { git = "https://github.com/synthizer/synthizer-rs" }

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Nolan Darilek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

BIN
assets/footstep.wav Normal file

Binary file not shown.

72
examples/game.rs Normal file
View File

@ -0,0 +1,72 @@
use std::f32;
use bevy::{asset::LoadState, prelude::*};
use bevy_synthizer::*;
#[derive(Component, Deref, DerefMut)]
struct RotationTimer(Timer);
impl Default for RotationTimer {
fn default() -> Self {
Self(Timer::from_seconds(30., true))
}
}
#[derive(Default)]
struct AssetHandles {
sounds: Vec<HandleUntyped>,
loaded: bool,
}
fn setup(asset_server: Res<AssetServer>, mut handles: ResMut<AssetHandles>) {
handles.sounds = asset_server.load_folder(".").expect("Failed to load sfx");
}
fn load_and_create(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut handles: ResMut<AssetHandles>,
) {
if handles.loaded {
return;
}
handles.loaded = asset_server
.get_group_load_state(handles.sounds.iter().map(|handle| handle.id))
== LoadState::Loaded;
if handles.loaded {
commands
.spawn()
.insert(Listener)
.insert(Transform::default())
.insert(RotationTimer::default());
let handle = handles.sounds[0].clone();
let buffer = asset_server.get_handle(handle);
commands
.spawn()
.insert(Transform::from_translation(Vec3::new(15., 0., 0.)))
.insert(Sound {
buffer,
looping: true,
..default()
});
}
}
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());
transform.rotation = Quat::from_rotation_z(f32::consts::PI * 2. * timer.percent());
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_system(bevy::input::system::exit_on_esc_system)
.add_plugin(SynthizerPlugin)
.init_resource::<AssetHandles>()
.add_startup_system(setup)
.add_system(load_and_create)
.add_system(rotate_listener)
.run();
}

372
src/lib.rs Normal file
View File

@ -0,0 +1,372 @@
#![allow(clippy::type_complexity)]
use std::collections::HashMap;
use bevy::{
asset::{AssetLoader, LoadContext, LoadedAsset},
prelude::*,
reflect::TypeUuid,
transform::TransformSystem,
utils::BoxedFuture,
};
use derive_more::{Deref, DerefMut};
pub use synthizer as syz;
#[derive(Clone, Deref, DerefMut, PartialEq, TypeUuid)]
#[uuid = "6b6b533a-bb1f-11ec-bda2-00155d8fdde9"]
pub struct Buffer(syz::Buffer);
#[derive(Clone, Copy, Debug, Default)]
struct BufferAssetLoader;
impl AssetLoader for BufferAssetLoader {
fn load<'a>(
&'a self,
_bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
Box::pin(async move {
let buffer: Option<Buffer> =
match load_context.path().extension().unwrap().to_str().unwrap() {
"flac" | "mp3" | "wav" => {
if let Ok(path) = load_context.path().canonicalize() {
println!("Supported asset: {:?}", path);
syz::Buffer::from_file(path).map(Buffer).ok()
} else {
println!("Couldn't");
None
}
}
_ => None,
};
if let Some(buffer) = buffer {
println!("Got a buffer");
load_context.set_default_asset(LoadedAsset::new(buffer));
}
Ok(())
})
}
fn extensions(&self) -> &[&str] {
&["flac", "mp3", "wav"]
}
}
#[derive(Component, Clone, Reflect)]
#[reflect(Component)]
pub struct Sound {
pub buffer: Handle<Buffer>,
pub gain: f64,
pub pitch: f64,
pub looping: bool,
pub paused: bool,
pub restart: bool,
#[reflect(ignore)]
pub source: Option<syz::Source>,
#[reflect(ignore)]
pub generator: Option<syz::BufferGenerator>,
}
impl Default for Sound {
fn default() -> Self {
Self {
buffer: Default::default(),
gain: 0.,
pitch: 1.,
looping: false,
paused: false,
restart: false,
source: None,
generator: None,
}
}
}
#[derive(Component, Clone, Copy, Debug, Deref, DerefMut)]
pub struct DistanceModel(syz::DistanceModel);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct DistanceRef(f64);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct DistanceMax(f64);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct Rolloff(f64);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct ClosenessBoost(f64);
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct ClosenessBoostDistance(f64);
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct Listener;
pub fn update_listener(
context: ResMut<syz::Context>,
listener: Query<(Option<&Transform>, Option<&GlobalTransform>), With<Listener>>,
) {
if let Ok((transform, global_transform)) = listener.get_single() {
let transform: Option<Transform> = global_transform
.map(|v| {
let transform: Transform = (*v).into();
transform
})
.or_else(|| transform.cloned());
if let Some(transform) = transform {
let look = transform.local_x();
let up = transform.local_z();
if let Err(e) = context.position().set((
transform.translation.x as f64,
transform.translation.y as f64,
transform.translation.z as f64,
)) {
error!("Error setting listener position: {:?}", e);
}
if let Err(e) = context.orientation().set((
look.x as f64,
look.y as f64,
look.z as f64,
up.x as f64,
up.y as f64,
up.z as f64,
)) {
error!("Error setting listener orientation: {:?}", e);
}
} else {
context.position().set((0., 0., 0.)).ok();
context.orientation().set((0., 0., 1., 0., 1., 0.)).ok();
}
} else {
context.position().set((0., 0., 0.)).ok();
context.orientation().set((0., 0., 1., 0., 1., 0.)).ok();
}
}
#[derive(Default, Deref, DerefMut)]
struct LastBuffer(HashMap<Entity, Handle<Buffer>>);
fn swap_buffers(mut last_buffer: ResMut<LastBuffer>, mut query: Query<(Entity, &mut Sound)>) {
for (entity, mut sound) in query.iter_mut() {
let buffer = sound.buffer.clone();
if let Some(l) = last_buffer.get(&entity) {
if buffer != *l {
sound.source = None;
sound.generator = None;
}
}
last_buffer.insert(entity, buffer);
}
}
pub fn update_sound_properties(
context: Res<syz::Context>,
buffers: Res<Assets<Buffer>>,
mut query: Query<(
&mut Sound,
Option<&DistanceModel>,
Option<&DistanceRef>,
Option<&DistanceMax>,
Option<&Rolloff>,
Option<&ClosenessBoost>,
Option<&ClosenessBoostDistance>,
Option<&Transform>,
Option<&GlobalTransform>,
)>,
) {
for (
mut sound,
distance_model,
distance_ref,
distance_max,
rolloff,
closeness_boost,
closeness_boost_distance,
transform,
global_transform,
) in query.iter_mut()
{
let has_transform = transform.is_some();
let Sound {
gain,
pitch,
looping,
..
} = *sound;
if sound.restart {
if let Some(generator) = sound.generator.as_mut() {
generator
.playback_position()
.set(0.)
.expect("Failed to restart");
}
sound.restart = false;
}
if let Some(b) = buffers.get(sound.buffer.clone()) {
if sound.source.is_none() {
let generator =
syz::BufferGenerator::new(&context).expect("Failed to create generator");
generator.buffer().set(&**b).expect("Unable to set buffer");
if has_transform {
let source = syz::Source3D::new(
&context,
syz::PannerStrategy::Delegate,
(0.0, 0.0, 0.0),
)
.expect("Failed to create source");
source
.add_generator(&generator)
.expect("Unable to add generator");
sound.source = Some(source.into());
} else {
let source = syz::DirectSource::new(&context).expect("Failed to create source");
source
.add_generator(&generator)
.expect("Failed to add generator");
sound.source = Some(source.into());
}
sound.generator = Some(generator);
}
}
if let Some(generator) = sound.generator.as_mut() {
generator
.pitch_bend()
.set(pitch)
.expect("Failed to set pitch");
generator
.looping()
.set(looping)
.expect("Failed to set looping");
}
if let Some(source) = sound.source.as_mut() {
source.gain().set(gain).expect("Failed to set gain");
let translation = global_transform
.map(|v| v.translation)
.or_else(|| transform.map(|v| v.translation));
if let Some(translation) = translation {
let source = source.cast_to::<syz::Source3D>().unwrap().unwrap();
source
.position()
.set((
translation.x as f64,
translation.y as f64,
translation.z as f64,
))
.expect("Failed to set position");
if let Some(distance_model) = distance_model {
source
.distance_model()
.set(**distance_model)
.expect("Failed to set distance_model");
}
if let Some(distance_ref) = distance_ref {
source
.distance_ref()
.set(**distance_ref)
.expect("Failed to set distance_ref");
}
if let Some(distance_max) = distance_max {
source
.distance_max()
.set(**distance_max)
.expect("Failed to set distance_max");
}
if let Some(rolloff) = rolloff {
source
.rolloff()
.set(**rolloff)
.expect("Failed to set rolloff");
}
if let Some(closeness_boost) = closeness_boost {
source
.closeness_boost()
.set(**closeness_boost)
.expect("Failed to set closeness_boost");
}
if let Some(closeness_boost_distance) = closeness_boost_distance {
source
.closeness_boost_distance()
.set(**closeness_boost_distance)
.expect("Failed to set closeness_boost_distance");
}
}
}
}
}
pub fn update_playback_state(query: Query<&Sound>) {
for sound in query.iter() {
if let Some(generator) = &sound.generator {
if sound.paused {
generator.pause().expect("Failed to pause");
} else {
generator.play().expect("Failed to play");
}
}
}
}
fn remove_sound(mut last_buffer: ResMut<LastBuffer>, removed: RemovedComponents<Sound>) {
for entity in removed.iter() {
last_buffer.remove(&entity);
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SynthizerConfig {
pub hrtf: bool,
}
pub struct SynthizerPlugin;
impl Plugin for SynthizerPlugin {
fn build(&self, app: &mut App) {
let guard = syz::initialize().expect("Failed to initialize Synthizer");
let context = syz::Context::new().expect("Failed to create Synthizer context");
if !app.world.contains_resource::<SynthizerConfig>() {
app.insert_resource(SynthizerConfig::default());
}
let config = *app.world.get_resource::<SynthizerConfig>().unwrap();
if config.hrtf {
context
.default_panner_strategy()
.set(syz::PannerStrategy::Hrtf)
.expect("Failed to set panner strategy");
} else {
context
.default_panner_strategy()
.set(syz::PannerStrategy::Stereo)
.expect("Failed to set panner strategy");
}
app.add_asset::<Buffer>()
.init_asset_loader::<BufferAssetLoader>()
.register_type::<Listener>()
.insert_resource(guard)
.insert_resource(context)
.init_resource::<LastBuffer>()
.add_system_to_stage(
CoreStage::PostUpdate,
swap_buffers.before(update_sound_properties),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_listener
.after(TransformSystem::TransformPropagate)
.before(update_playback_state),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_sound_properties
.after(TransformSystem::TransformPropagate)
.before(update_playback_state),
)
.add_system_to_stage(CoreStage::PostUpdate, update_playback_state)
.add_system_to_stage(CoreStage::PostUpdate, remove_sound);
}
}