284 lines
10 KiB
Rust
284 lines
10 KiB
Rust
//! 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::<AudioSourceDesc>(entity).cloned() else {
|
|
return;
|
|
};
|
|
let original = source.clone();
|
|
let audio_assets: Vec<EditorAsset> = world
|
|
.resource::<EditorAssets>()
|
|
.assets
|
|
.iter()
|
|
.filter(|asset| matches!(asset.kind, EditorAssetKind::AudioClip))
|
|
.cloned()
|
|
.collect();
|
|
let dragged_audio = world
|
|
.resource::<EditorAssets>()
|
|
.dragging_selection()
|
|
.and_then(|selection| {
|
|
world
|
|
.resource::<EditorAssets>()
|
|
.asset_for_selection(selection)
|
|
})
|
|
.filter(|asset| matches!(asset.kind, EditorAssetKind::AudioClip))
|
|
.cloned();
|
|
let buses = world
|
|
.resource::<settings::ProjectSettings>()
|
|
.audio
|
|
.buses
|
|
.clone();
|
|
let previewing = world.resource::<AudioPreviewState>().source == Some(entity);
|
|
let diagnostics = world
|
|
.resource::<game::audio::AudioRuntimeDiagnostics>()
|
|
.clone();
|
|
let mut pending_clip: Option<EditorAsset> = 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::<EditorAssets>().clear_drag();
|
|
}
|
|
Err(error) => {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().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::<crate::scene_io::SceneIo>().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::<AudioListenerDesc>(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);
|
|
}
|