//! Audio source/listener authoring cards and transient audition controls. use bevy::prelude::*; use bevy_egui::egui; use egui_phosphor_icons::icons; use settings::{AUDIO_GAIN_DB_MAX, AUDIO_GAIN_DB_MIN}; use shared::{ AudioListenerDesc, AudioRolloff, AudioSourceDesc, COMPONENT_AUDIO_LISTENER_DESC, COMPONENT_AUDIO_SOURCE_DESC, }; use crate::assets::{audio_clip_ref_for_asset, EditorAsset, EditorAssetKind, EditorAssets}; use crate::history::{set_audio_listener_with_history, set_audio_source_with_history}; use crate::play::audio_preview::{audition_audio_source, stop_audio_preview, AudioPreviewState}; use crate::ui::inspector::{ apply_component_card_response, component_card, component_card_context, property_row, ComponentCardOptions, }; use crate::ui::theme::{SELECTION, TEXT_DIM}; pub fn audio_source_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut source) = world.get::(entity).cloned() else { return; }; let original = source.clone(); let audio_assets: Vec = world .resource::() .assets .iter() .filter(|asset| matches!(asset.kind, EditorAssetKind::AudioClip)) .cloned() .collect(); let dragged_audio = world .resource::() .dragging_selection() .and_then(|selection| { world .resource::() .asset_for_selection(selection) }) .filter(|asset| matches!(asset.kind, EditorAssetKind::AudioClip)) .cloned(); let buses = world .resource::() .audio .buses .clone(); let previewing = world.resource::().source == Some(entity); let diagnostics = world .resource::() .clone(); let mut pending_clip: Option = None; let mut clear_clip = false; let mut audition = false; let mut stop = false; let card = component_card_context( world, entity, ComponentCardOptions::removable( COMPONENT_AUDIO_SOURCE_DESC, "Audio Source", icons::SPEAKER_HIGH, ), ); let card_response = component_card(ui, &card, |ui| { let clip_response = property_row(ui, "Clip", |ui| { ui.horizontal_wrapped(|ui| { let selected_text = source .clip .as_ref() .map(|clip| clip.label.as_str()) .filter(|label| !label.trim().is_empty()) .unwrap_or("None"); egui::ComboBox::from_id_salt(("audio_clip", entity)) .selected_text(selected_text) .width(150.0) .show_ui(ui, |ui| { for asset in &audio_assets { let selected = source.clip.as_ref().is_some_and(|clip| { clip.source_path.as_deref() == asset.path.as_deref() }); if ui.selectable_label(selected, &asset.label).clicked() { pending_clip = Some(asset.clone()); } } }); if ui .add_enabled(source.clip.is_some(), egui::Button::new(icons::X.as_str())) .on_hover_text("Clear clip") .clicked() { clear_clip = true; } }) .response }); if dragged_audio.is_some() && clip_response.hovered() { ui.painter().rect_stroke( clip_response.rect.expand(3.0), 3.0, egui::Stroke::new(1.5, SELECTION), egui::StrokeKind::Outside, ); if ui.input(|input| input.pointer.any_released()) { pending_clip = dragged_audio.clone(); } } property_row(ui, "Playback", |ui| { ui.horizontal_wrapped(|ui| { if ui .add_enabled( source.clip.is_some() && !previewing, egui::Button::new(format!("{} Audition", icons::PLAY.as_str())), ) .clicked() { audition = true; } if ui .add_enabled( previewing, egui::Button::new(format!("{} Stop", icons::STOP.as_str())), ) .clicked() { stop = true; } }); }); property_row(ui, "Gain", |ui| { ui.add( egui::Slider::new(&mut source.gain_db, AUDIO_GAIN_DB_MIN..=AUDIO_GAIN_DB_MAX) .suffix(" dB"), ); }); property_row(ui, "Pitch", |ui| { ui.add( egui::DragValue::new(&mut source.pitch) .range(0.25..=4.0) .speed(0.01), ); }); property_row(ui, "Mode", |ui| { ui.horizontal_wrapped(|ui| { ui.checkbox(&mut source.autoplay, "Autoplay"); ui.checkbox(&mut source.looping, "Loop"); }); }); property_row(ui, "Spatial blend", |ui| { ui.add(egui::Slider::new(&mut source.spatial_blend, 0.0..=1.0)); }); property_row(ui, "Min distance", |ui| { ui.add( egui::DragValue::new(&mut source.attenuation.min_distance) .range(0.01..=10_000.0) .speed(0.1) .suffix(" m"), ); }); property_row(ui, "Max distance", |ui| { ui.add( egui::DragValue::new(&mut source.attenuation.max_distance) .range(0.02..=100_000.0) .speed(0.25) .suffix(" m"), ); }); property_row(ui, "Rolloff", |ui| { ui.horizontal_wrapped(|ui| { egui::ComboBox::from_id_salt(("audio_rolloff", entity)) .selected_text(format!("{:?}", source.attenuation.rolloff)) .show_ui(ui, |ui| { for (rolloff, label) in [ (AudioRolloff::Linear, "Linear"), (AudioRolloff::Inverse, "Inverse"), (AudioRolloff::Exponential, "Exponential"), ] { ui.selectable_value(&mut source.attenuation.rolloff, rolloff, label); } }); ui.add( egui::DragValue::new(&mut source.attenuation.rolloff_factor) .range(0.01..=8.0) .speed(0.05), ); }); }); property_row(ui, "Bus", |ui| { egui::ComboBox::from_id_salt(("audio_bus", entity)) .selected_text( buses .iter() .find(|bus| bus.id == source.bus) .map(|bus| bus.label.as_str()) .unwrap_or(source.bus.as_str()), ) .show_ui(ui, |ui| { for bus in &buses { ui.selectable_value(&mut source.bus, bus.id.clone(), &bus.label); } }); }); ui.small( egui::RichText::new(&diagnostics.message) .color(TEXT_DIM) .italics(), ); }); if clear_clip { source.clip = None; } if let Some(asset) = pending_clip { match audio_clip_ref_for_asset(world, &asset) { Ok(clip) => { source.clip = Some(clip); world.resource_mut::().clear_drag(); } Err(error) => { world.resource_mut::().status = format!("Audio clip assignment failed: {error}"); } } } source.attenuation.max_distance = source .attenuation .max_distance .max(source.attenuation.min_distance + 0.01); if source != original { set_audio_source_with_history(world, entity, source); } if stop { stop_audio_preview(world); } else if audition { if let Err(error) = audition_audio_source(world, entity) { world.resource_mut::().status = format!("Audio audition failed: {error}"); } } apply_component_card_response(world, entity, card_response); } pub fn audio_listener_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) { let Some(mut listener) = world.get::(entity).copied() else { return; }; let original = listener; let card = component_card_context( world, entity, ComponentCardOptions::removable( COMPONENT_AUDIO_LISTENER_DESC, "Audio Listener", icons::HEADPHONES, ), ); let card_response = component_card(ui, &card, |ui| { property_row(ui, "Enabled", |ui| { ui.checkbox(&mut listener.enabled, "Receive spatial audio"); }); property_row(ui, "Priority", |ui| { ui.add(egui::DragValue::new(&mut listener.priority).speed(1)); }); property_row(ui, "Ear gap", |ui| { ui.add( egui::DragValue::new(&mut listener.ear_gap) .range(0.01..=2.0) .speed(0.01) .suffix(" m"), ); }); ui.small( egui::RichText::new("Highest priority enabled listener wins; actor ID breaks ties.") .color(TEXT_DIM), ); }); if listener != original { set_audio_listener_with_history(world, entity, listener); } apply_component_card_response(world, entity, card_response); }