use std::time::Duration;
use bevy::prelude::*;
use bevy_egui::{
EguiContextPass, EguiContexts, EguiPlugin,
egui::{self},
};
use bevy_mod_kira::{
KiraContext, KiraPlaySoundEvent, KiraPlugin, KiraStaticSoundAsset, KiraStaticSoundHandle,
KiraTrackHandle,
};
use egui_plot::{BarChart, HLine, LineStyle};
use kira::{sound::static_sound::StaticSoundSettings, track::TrackBuilder};
mod effects;
use effects::{LevelMonitorBuilder, LevelMonitorHandle};
const SAMPLES: usize = 16;
mod color_utils;
use color_utils::*;
pub fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy mod Kira - Level Monitor".into(),
resolution: (800., 460.).into(),
fit_canvas_to_parent: true,
prevent_default_event_handling: false,
..default()
}),
..default()
}))
.add_plugins((
KiraPlugin,
EguiPlugin {
enable_multipass_for_primary_context: true,
},
))
.add_systems(Startup, setup_sys)
.add_systems(Update, trigger_play_sys)
.add_systems(EguiContextPass, ui_sys)
.run();
}
struct TimerMs<const N: i32> {
timer: Timer,
}
impl<const N: i32> Default for TimerMs<N> {
fn default() -> Self {
let mut timer = Timer::from_seconds(N as f32 / 1000.0, TimerMode::Repeating);
timer.tick(Duration::from_millis(N as u64 - 500));
Self { timer }
}
}
#[derive(Component)]
struct LevelsHandle<const N: usize>(LevelMonitorHandle<N>);
unsafe impl<const N: usize> Sync for LevelsHandle<N> {}
#[derive(Component)]
struct Panning(f32);
fn setup_sys(mut commands: Commands, loader: Res<AssetServer>, mut kira: NonSendMut<KiraContext>) {
let a = loader.load("hit.ogg");
let mut entity = commands.spawn(KiraStaticSoundHandle(a));
let monitor = LevelMonitorBuilder::<SAMPLES>;
let mut track = TrackBuilder::new();
let monitor_handle = track.add_effect(monitor);
entity.insert(LevelsHandle(monitor_handle));
entity.insert(Panning(0.0));
let track_handle = kira
.add_track(track)
.expect("Failed to add track to KiraContext");
entity.insert(KiraTrackHandle(track_handle));
}
fn trigger_play_sys(
assets: Res<Assets<KiraStaticSoundAsset>>,
query: Query<(Entity, &KiraStaticSoundHandle, &Panning)>,
time: Res<Time>,
mut looper: Local<TimerMs<1000>>,
mut ev_play: EventWriter<KiraPlaySoundEvent>,
) {
looper.timer.tick(time.delta());
if !looper.timer.just_finished() {
return;
}
for (eid, sound_handle, panning) in query.iter() {
if let Some(sound_asset) = assets.get(&sound_handle.0) {
let sound_data = sound_asset.sound.clone();
let sound = sound_data.0.with_settings(StaticSoundSettings {
panning: panning.0.into(),
..default()
});
ev_play.write(KiraPlaySoundEvent::new(eid, Some(eid), sound));
}
}
}
#[derive(Default)]
struct Peaks {
left: f32,
right: f32,
left_peak: f32,
right_peak: f32,
}
fn dbs_from_rms(rms: f32) -> f32 {
100.0 + (20.0 * rms.log10()).max(-100.0)
}
fn ui_sys(
mut ctx: EguiContexts,
mut query: Query<(&mut LevelsHandle<SAMPLES>, &mut Panning)>,
mut peaks: Local<Peaks>,
) -> Result<(), BevyError> {
let ctx = ctx.try_ctx_mut();
if ctx.is_none() {
return Ok(());
}
let (mut monitor_handle, mut panning) = query.single_mut()?;
if let Ok(levels) = monitor_handle.0.get_sample() {
let samples = levels.window.len() as f32;
let squares = levels
.window
.iter()
.map(|x| (x.left * x.left, x.right * x.right))
.fold((0.0, 0.0), |(l, r), (nl, nr)| (l + nl, r + nr));
let rms = ((squares.0 / samples).sqrt(), (squares.1 / samples).sqrt());
let dbs = (dbs_from_rms(rms.0), dbs_from_rms(rms.1));
let (left, right) = dbs;
peaks.left = peaks.left.max(left);
peaks.left_peak = peaks.left_peak.max(left);
peaks.right = peaks.right.max(right);
peaks.right_peak = peaks.right_peak.max(right);
}
let fast_decay = 0.90;
let slow_decay = 0.99;
peaks.left *= fast_decay;
peaks.right *= fast_decay;
peaks.right_peak *= slow_decay;
peaks.left_peak *= slow_decay;
egui::Window::new("Level Monitor")
.default_width(600.0)
.default_height(400.0)
.show(ctx.unwrap(), |ui| {
ui.horizontal(|ui| {
ui.label("Panning");
ui.add(egui::Slider::new(&mut panning.0, -1.0..=1.0).text("Panning"));
});
let left_bar = egui_plot::Bar::new(-0.5, peaks.left as f64);
let right_bar = egui_plot::Bar::new(0.5, peaks.right as f64);
let plot = egui_plot::Plot::new("Levels")
.legend(egui_plot::Legend::default())
.label_formatter(|_name, value| format!("{:.2} db", value.y - 100.0))
.show_background(false)
.include_x(-1.0)
.include_x(1.0)
.include_y(100.0)
.include_y(0.0);
ui.painter().rect_filled(
ui.available_rect_before_wrap(),
0.0,
dark_color(Pallete::DeepBlue),
);
let left_color = Pallete::MintGreen;
let right_color = Pallete::AquaBlue;
plot.show(ui, |plot_ui| {
plot_ui.hline(
HLine::new("Left", peaks.left_peak)
.style(LineStyle::Dashed { length: 5.0 })
.width(2.0)
.color(left_color),
);
plot_ui.hline(
HLine::new("Right", peaks.right_peak)
.style(LineStyle::Dashed { length: 5.0 })
.width(2.0)
.color(right_color),
);
plot_ui.bar_chart(BarChart::new("Left", vec![left_bar]).color(left_color));
plot_ui.bar_chart(BarChart::new("Right", vec![right_bar]).color(right_color));
});
});
Ok(())
}