mic level bar

This commit is contained in:
Emerald 2023-10-10 14:27:24 -04:00
parent 07233e0e12
commit 022f414acb
Signed by: emerald
GPG Key ID: 420C9E1863CCB30F
6 changed files with 325 additions and 264 deletions

View File

@ -3,13 +3,17 @@
left: 0px;
}
.right {
position-type: absolute;
right: 0px;
}
.frames-container {
width: 150px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.frame {
@ -33,3 +37,36 @@
height: auto;
max-height: 75%;
}
.levels-container {
height: 100%;
}
.levels-container div {
height: 100%;
align-items: center;
justify-content: center;
}
.levels-container range {
padding: 0px;
margin: 10px;
}
.range-back {
display: none;
}
.mic-level {
padding: 0px;
}
.levels-container .active .range-low {
background-color: green
}
.levels-container .range-low {
background-color: blue
}

View File

@ -10,39 +10,57 @@ use anyhow::{anyhow, Result};
pub struct MicMonitor(Stream);
pub type Level = Arc<Mutex<f32>>;
#[derive(Resource, Deref)]
pub struct MicLevel(Level);
pub type Level = Mutex<f32>;
pub static MIC_LEVEL_RAW: Level = Mutex::new(0.);
pub static MIC_SENS_RAW: Level = Mutex::new(1.);
#[derive(Resource, Deref, Default)]
pub struct MicLevel(f32);
impl MicLevel {
pub fn value(&self) -> f32 {
**self
}
}
#[derive(Resource, Deref)]
pub struct MicSens(Level);
pub struct MicSens(f32);
pub struct AudioPlugin;
impl Plugin for AudioPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(monitor);
app.add_startup_system(monitor)
.add_systems((update_mic_level, update_mic_sens).in_base_set(CoreSet::PreUpdate))
.insert_resource(MicSens(1.))
.init_resource::<MicLevel>();
}
}
pub fn update_mic_level(mut level: ResMut<MicLevel>) {
level.0 = *MIC_LEVEL_RAW.lock().expect("Poisoned mutex");
}
pub fn update_mic_sens(mut sens: ResMut<MicSens>) {
sens.0 = *MIC_SENS_RAW.lock().expect("Poisoned mutex");
}
pub fn monitor(world: &mut World) {
let device = initialize().expect("Unable to init audio");
info!("Using device {}", device.name().unwrap());
let config = device.default_input_config().unwrap();
let level = Arc::new(Mutex::new(0.));
world.insert_resource(MicLevel(level.clone()));
let sens = Arc::new(Mutex::new(1.));
world.insert_resource(MicSens(sens.clone()));
*MIC_SENS_RAW.lock().expect("Poisoned sens mutex") = 100.0;
let stream = device
.build_input_stream(
&config.config(),
move |data: &[f32], _: &InputCallbackInfo| {
let cur_level = { *level.lock().unwrap() };
*level.lock().unwrap() = (data
let cur_level = { *MIC_LEVEL_RAW.lock().unwrap() };
*MIC_LEVEL_RAW.lock().unwrap() = (data
.iter()
.map(|e| e.abs() * *sens.lock().unwrap())
.map(|e| e.abs() * *MIC_SENS_RAW.lock().unwrap())
.max_by(|a, b| a.total_cmp(b))
.unwrap()
* 0.5)

View File

@ -64,14 +64,12 @@ fn main() {
}
fn logging(level: Res<MicLevel>, threshold: Res<TriggerThreshold>) {
info!("Audio in: {}", level.lock().unwrap());
info!("Audio in: {}", **level);
info!("Mic thresh: {}", **threshold);
}
fn setup(mut commands: Commands, sens: ResMut<MicSens>) {
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
*sens.lock().expect("Poisoned sens mutex") = 100.0;
}
// window systems
@ -87,7 +85,7 @@ fn react_to_focus(
}
if event.focused {
commands.insert_resource(ClearColor(Color::ALICE_BLUE));
commands.insert_resource(ClearColor(Color::PURPLE));
ui_events.send(UiMessage::Show);
} else {
commands.insert_resource(ClearColor(Color::NONE));

View File

@ -3,7 +3,7 @@ use bevy::prelude::*;
use bevy_tweening::{lens::TransformPositionLens, Animator, EaseMethod, Tween};
use std::time::Duration;
use crate::audio::MicLevel;
use crate::audio::{update_mic_level, MicLevel};
#[derive(Resource, Clone, Copy)]
pub enum MouthState {
@ -42,7 +42,11 @@ pub struct TubePlugin;
impl Plugin for TubePlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(setup)
.add_system(audio_trigger.in_base_set(CoreSet::PreUpdate))
.add_system(
audio_trigger
.in_base_set(CoreSet::PreUpdate)
.after(update_mic_level),
)
.add_system(run_animation.run_if(resource_changed::<MouthState>()))
.add_system(sync_images)
.insert_resource(MouthState::Closed)
@ -87,15 +91,14 @@ fn setup(mut commands: Commands, ass: Res<AssetServer>) {
const MAX_THRESHOLD: f32 = 10.0;
fn audio_trigger(
mut mouth_state: ResMut<MouthState>,
mic_level: Res<MicLevel>,
level: Res<MicLevel>,
threshold: Res<TriggerThreshold>,
mut buffer: ResMut<ActivationBuffer>,
mut was_zero: Local<bool>,
time: Res<Time>,
) {
let level = mic_level.lock().expect("Poisoned mutex");
// info!("{}", ((**threshold / 100.) * MAX_THRESHOLD));
if *level > ((**threshold / 100.) * MAX_THRESHOLD) {
if level.value() > ((**threshold / 100.) * MAX_THRESHOLD) {
if buffer.finished() {
info!("Open mouth");
*mouth_state = MouthState::Opened;

View File

@ -1,9 +1,9 @@
use std::{collections::HashMap, marker::PhantomData, time::Duration};
use belly::prelude::*;
use bevy::prelude::*;
use bevy_eventlistener::callbacks::ListenerInput;
use bevy_mod_picking::prelude::*;
use bevy_ninepatch::*;
use bevy_tweening::{lens::UiPositionLens, *};
use crate::{
@ -12,19 +12,21 @@ use crate::{
utils::UiSizeLens,
};
#[derive(Component)]
use super::setup_ui;
#[derive(Component, Default)]
pub struct LevelBars;
#[derive(Component)]
#[derive(Component, Default)]
pub struct MicLevelBar;
#[derive(Component)]
#[derive(Component, Default)]
pub struct MicLevelContainer;
#[derive(Component)]
#[derive(Component, Default)]
pub struct ActivationLevelBar;
#[derive(Component)]
#[derive(Component, Default)]
pub struct MicLevelArrow;
pub struct ArrowDrag<C: Component> {
@ -70,250 +72,240 @@ pub(super) struct LevelsPlugin;
impl Plugin for LevelsPlugin {
fn build(&self, app: &mut App) {
app.add_startup_system(spawn_levels)
.add_systems((set_max_size::<MicLevelContainer, MicLevelArrow>
.after(spawn_levels)
.in_base_set(CoreSet::Last)
.run_if(run_once()),))
.add_event::<ArrowDrag<MicLevelArrow>>()
app.add_event::<ArrowDrag<MicLevelArrow>>()
.add_system(
handle_mic_thresh_set.run_if(resource_exists::<MaxPercent<MicLevelContainer>>()),
)
.add_system(react_mic_level_color.run_if(resource_changed::<MouthState>()))
.add_system(animate_mic_level)
.add_system(animate_active_level);
.add_system(react_mic_level_color.run_if(resource_changed::<MouthState>()));
}
}
const RESTING_POS: f32 = 20.;
pub(super) fn spawn_levels(
mut commands: Commands,
ass: Res<AssetServer>,
mut patches: ResMut<Assets<NinePatchBuilder<()>>>,
mic_threshold: Res<TriggerThreshold>,
// mut commands: Commands,
// mic_threshold: Res<TriggerThreshold>,
mut elements: Elements,
) {
let bar_img: Handle<Image> = ass.load("square-small.png");
let bar_patch = patches.add(NinePatchBuilder::by_margins(2, 2, 2, 2));
let mic_bar = commands
.spawn(NodeBundle {
background_color: Color::GREEN.into(),
style: Style {
position_type: PositionType::Absolute,
size: Size::new(Val::Percent(100.), Val::Percent(0.)),
position: UiRect::bottom(Val::Px(0.)),
..default()
},
..default()
})
.insert(MicLevelBar)
.id();
let active_bar = commands
.spawn(NodeBundle {
background_color: Color::PURPLE.into(),
style: Style {
position_type: PositionType::Absolute,
size: Size::new(Val::Percent(100.), Val::Percent(0.)),
position: UiRect::bottom(Val::Px(0.)),
..default()
},
..default()
})
.insert(ActivationLevelBar)
.id();
// let bar_img: Handle<Image> = ass.load("square-small.png");
// let bar_patch = patches.add(NinePatchBuilder::by_margins(2, 2, 2, 2));
// let mic_bar = commands
// .spawn(NodeBundle {
// background_color: Color::GREEN.into(),
// style: Style {
// position_type: PositionType::Absolute,
// size: Size::new(Val::Percent(100.), Val::Percent(0.)),
// position: UiRect::bottom(Val::Px(0.)),
// ..default()
// },
// ..default()
// })
// .insert(MicLevelBar)
// .id();
commands
.spawn(NodeBundle {
style: Style {
size: Size::new(Val::Px(200.), Val::Percent(100.)),
position_type: PositionType::Absolute,
gap: Size::all(Val::Px(20.)),
position: UiRect::right(Val::Px(RESTING_POS)),
justify_content: JustifyContent::FlexEnd,
padding: UiRect::vertical(Val::Px(20.)),
..default()
},
..default()
})
.insert(LevelBars)
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
gap: Size::height(Val::Px(10.)),
// size: Size::all(Val::Percent(100.)),
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn(ImageBundle {
image: UiImage::new(ass.load("microphone.png")),
background_color: Color::BLACK.into(),
..default()
});
// let active_bar = commands
// .spawn(NodeBundle {
// background_color: Color::PURPLE.into(),
// style: Style {
// position_type: PositionType::Absolute,
// size: Size::new(Val::Percent(100.), Val::Percent(0.)),
// position: UiRect::bottom(Val::Px(0.)),
// ..default()
// },
// ..default()
// })
// .insert(ActivationLevelBar)
// .id();
let mut content = HashMap::new();
content.insert((), mic_bar);
parent
.spawn(NodeBundle {
style: Style {
size: Size::height(Val::Percent(100.)),
..default()
},
..default()
})
.with_children(|parent| {
parent
.spawn(ImageBundle {
image: UiImage::new(ass.load("plain-arrow.png")),
background_color: Color::BLACK.into(),
style: Style {
size: Size::all(Val::Px(16.)),
position_type: PositionType::Absolute,
position: UiRect::new(
Val::Px(-16.),
Val::Auto,
Val::Auto,
Val::Percent(**mic_threshold),
),
..default()
},
z_index: ZIndex::Global(100),
..default()
})
.insert(MicLevelArrow)
.insert(
On::<Pointer<Drag>>::send_event::<ArrowDrag<MicLevelArrow>>(),
);
parent
.spawn(NinePatchBundle {
style: Style {
size: Size::new(Val::Px(32.), Val::Percent(100.)),
..default()
},
nine_patch_data: NinePatchData {
texture: bar_img.clone(),
nine_patch: bar_patch.clone(),
content: Some(content),
..default()
},
..default()
})
.insert(MicLevelContainer);
});
});
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
gap: Size::height(Val::Px(10.)),
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn(ImageBundle {
image: UiImage::new(ass.load("lips.png")),
background_color: Color::BLACK.into(),
..default()
});
let mut content = HashMap::new();
content.insert((), active_bar);
parent.spawn(NinePatchBundle {
style: Style {
size: Size::new(Val::Px(32.), Val::Percent(100.)),
..default()
},
nine_patch_data: NinePatchData {
texture: bar_img.clone(),
nine_patch: bar_patch.clone(),
content: Some(content),
..default()
},
..default()
});
});
});
// commands
// .spawn(NodeBundle {
// style: Style {
// size: Size::new(Val::Px(200.), Val::Percent(100.)),
// position_type: PositionType::Absolute,
// gap: Size::all(Val::Px(20.)),
// position: UiRect::right(Val::Px(RESTING_POS)),
// justify_content: JustifyContent::FlexEnd,
// padding: UiRect::vertical(Val::Px(20.)),
// ..default()
// },
// ..default()
// })
// .insert(LevelBars)
// .with_children(|parent| {
// parent
// .spawn(NodeBundle {
// style: Style {
// flex_direction: FlexDirection::Column,
// gap: Size::height(Val::Px(10.)),
// // size: Size::all(Val::Percent(100.)),
// ..default()
// },
// ..default()
// })
// .with_children(|parent| {
// parent.spawn(ImageBundle {
// image: UiImage::new(ass.load("microphone.png")),
// background_color: Color::BLACK.into(),
// ..default()
// });
// let mut content = HashMap::new();
// content.insert((), mic_bar);
// parent
// .spawn(NodeBundle {
// style: Style {
// size: Size::height(Val::Percent(100.)),
// ..default()
// },
// ..default()
// })
// .with_children(|parent| {
// parent
// .spawn(ImageBundle {
// image: UiImage::new(ass.load("plain-arrow.png")),
// background_color: Color::BLACK.into(),
// style: Style {
// size: Size::all(Val::Px(16.)),
// position_type: PositionType::Absolute,
// position: UiRect::new(
// Val::Px(-16.),
// Val::Auto,
// Val::Auto,
// Val::Percent(**mic_threshold),
// ),
// ..default()
// },
// z_index: ZIndex::Global(100),
// ..default()
// })
// .insert(MicLevelArrow)
// .insert(
// On::<Pointer<Drag>>::send_event::<ArrowDrag<MicLevelArrow>>(),
// );
// parent
// .spawn(NinePatchBundle {
// style: Style {
// size: Size::new(Val::Px(32.), Val::Percent(100.)),
// ..default()
// },
// nine_patch_data: NinePatchData {
// texture: bar_img.clone(),
// nine_patch: bar_patch.clone(),
// content: Some(content),
// ..default()
// },
// ..default()
// })
// .insert(MicLevelContainer);
// });
// });
// parent
// .spawn(NodeBundle {
// style: Style {
// flex_direction: FlexDirection::Column,
// gap: Size::height(Val::Px(10.)),
// ..default()
// },
// ..default()
// })
// .with_children(|parent| {
// parent.spawn(ImageBundle {
// image: UiImage::new(ass.load("lips.png")),
// background_color: Color::BLACK.into(),
// ..default()
// });
// let mut content = HashMap::new();
// content.insert((), active_bar);
// parent.spawn(NinePatchBundle {
// style: Style {
// size: Size::new(Val::Px(32.), Val::Percent(100.)),
// ..default()
// },
// nine_patch_data: NinePatchData {
// texture: bar_img.clone(),
// nine_patch: bar_patch.clone(),
// content: Some(content),
// ..default()
// },
// ..default()
// });
// });
// });
}
fn set_max_size<B: Component, C: Component>(
mut commands: Commands,
bar_q: Query<&Node, With<B>>,
other: Query<&Node, With<C>>,
) {
let Ok(bar) = bar_q.get_single() else { return };
let Ok(other) = other.get_single() else { return };
let size = ((bar.size().y - (other.size().y / 2.)) / bar.size().y) * 100.;
info!("{size}");
commands.insert_resource(MaxPercent::<B>::new(size));
}
// fn set_max_size<B: Component, C: Component>(
// mut commands: Commands,
// bar_q: Query<&Node, With<B>>,
// other: Query<&Node, With<C>>,
// ) {
// let Ok(bar) = bar_q.get_single() else { return };
// let Ok(other) = other.get_single() else {
// return;
// };
// let size = ((bar.size().y - (other.size().y / 2.)) / bar.size().y) * 100.;
// info!("{size}");
// commands.insert_resource(MaxPercent::<B>::new(size));
// }
pub(super) fn animate_mic_level(
mut commands: Commands,
bar_q: Query<(Entity, &Style), With<MicLevelBar>>,
level: Res<MicLevel>,
) {
const SCALE: f32 = 10.;
for (ent, style) in bar_q.iter() {
let current = style.size;
let new = Size::new(
style.size.width,
Val::Percent((*level.lock().expect("Poisoned mic level") * SCALE).clamp(0., 100.)),
);
let tween = Tween::new(
EaseMethod::Linear,
Duration::from_millis(1),
UiSizeLens {
start: current,
end: new,
},
);
commands.entity(ent).insert(Animator::new(tween));
}
}
// pub(super) fn animate_mic_level(
// mut commands: Commands,
// bar_q: Query<(Entity, &Style), With<MicLevelBar>>,
// level: Res<MicLevel>,
// ) {
// const SCALE: f32 = 10.;
// for (ent, style) in bar_q.iter() {
// let current = style.size;
// let new = Size::new(
// style.size.width,
// Val::Percent((level.value() * SCALE).clamp(0., 100.)),
// );
// let tween = Tween::new(
// EaseMethod::Linear,
// Duration::from_millis(1),
// UiSizeLens {
// start: current,
// end: new,
// },
// );
// commands.entity(ent).insert(Animator::new(tween));
// }
// }
pub(super) fn animate_active_level(
mut commands: Commands,
bar_q: Query<(Entity, &Style), With<ActivationLevelBar>>,
level: Res<ActivationBuffer>,
) {
const SCALE: f32 = 100.;
for (ent, style) in bar_q.iter() {
let current = style.size;
let new = Size::new(
style.size.width,
Val::Percent((level.percent_left() * SCALE).clamp(0., 100.)),
);
// pub(super) fn animate_active_level(
// mut commands: Commands,
// bar_q: Query<(Entity, &Style), With<ActivationLevelBar>>,
// level: Res<ActivationBuffer>,
// ) {
// const SCALE: f32 = 100.;
// for (ent, style) in bar_q.iter() {
// let current = style.size;
// let new = Size::new(
// style.size.width,
// Val::Percent((level.percent_left() * SCALE).clamp(0., 100.)),
// );
// info!("UI mic level: {:?}", new.height);
let tween = Tween::new(
EaseMethod::Linear,
Duration::from_millis(1),
UiSizeLens {
start: current,
end: new,
},
);
commands.entity(ent).insert(Animator::new(tween));
}
}
// // info!("UI mic level: {:?}", new.height);
// let tween = Tween::new(
// EaseMethod::Linear,
// Duration::from_millis(1),
// UiSizeLens {
// start: current,
// end: new,
// },
// );
// commands.entity(ent).insert(Animator::new(tween));
// }
// }
pub(super) fn react_mic_level_color(
mut bar_q: Query<&mut BackgroundColor, With<MicLevelBar>>,
mouth_state: Res<MouthState>,
) {
for mut color in bar_q.iter_mut() {
match *mouth_state {
MouthState::Opened => {
*color = Color::GREEN.into();
}
pub(super) fn react_mic_level_color(mut elements: Elements, mouth_state: Res<MouthState>) {
match *mouth_state {
MouthState::Opened => {
elements.select(".mic-level").add_class("active");
}
MouthState::Closed => {
*color = Color::BLUE.into();
}
MouthState::Closed => {
elements.select(".mic-level").remove_class("active");
}
}
}
@ -326,8 +318,12 @@ fn handle_mic_thresh_set(
max_size: Res<MaxPercent<MicLevelContainer>>,
) {
for event in events.iter() {
let Ok((mut style, arrow)) = styles.get_mut(event.target) else { continue };
let Ok(bar) = bar_q.get_single() else { continue };
let Ok((mut style, arrow)) = styles.get_mut(event.target) else {
continue;
};
let Ok(bar) = bar_q.get_single() else {
continue;
};
if let Val::Percent(pos) = style.position.bottom {
let delta = (event.delta.y / bar.size().y) * 100.;
let new_percent = (delta + pos).clamp(0., **max_size);

View File

@ -2,13 +2,15 @@ mod frames;
mod levels;
mod widgets;
use belly::prelude::*;
use belly::{prelude::*, widgets::range::RangeWidgetExtension};
use bevy::prelude::*;
use bevy_ninepatch::NinePatchPlugin;
use crate::audio::MicLevel;
use self::{
frames::{CardImages, FrameCards},
levels::LevelBars,
levels::{ActivationLevelBar, LevelBars, MicLevelArrow, MicLevelContainer},
};
#[derive(Clone, Debug)]
@ -23,15 +25,10 @@ impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(BellyPlugin)
.add_event::<UiMessage>()
.add_plugin(levels::LevelsPlugin)
.add_startup_system(setup_ui)
.add_system(handle_msg)
.add_system(frames::update_card_images);
// app.add_plugin(NinePatchPlugin::<()>::default())
// .add_plugin(levels::LevelsPlugin)
// .init_resource::<CardImages>()
// .add_startup_system(frames::update_card_images)
// .add_system(frames::update_card_images.before(frames::render_cards))
// .add_startup_system(frames::render_cards);
}
}
@ -45,6 +42,16 @@ fn setup_ui(mut commands: Commands) {
</div>
</for>
</div>
<div c:right c:levels-container with=LevelBars s:right="0px">
<div s:flex-direction="column">
<range c:mic-level s:height="75%" s:width="50px" mode="vertical" maximum=5.0 bind:value=from!(MicLevel:value())/>
<img src="microphone.png" modulate=Color::BLACK/>
</div>
<div c:level-container>
</div>
</div>
</body>
});
}
@ -56,7 +63,9 @@ fn handle_msg(
levels_q: Query<(Entity, &Style), With<LevelBars>>,
) {
for event in events.iter() {
let Ok((frames, frame_style)) = frame_q.get_single() else { return };
let Ok((frames, frame_style)) = frame_q.get_single() else {
return;
};
// let Ok((bars, bar_style)) = levels_q.get_single() else { return };
match event {
UiMessage::Hide => {