use core::f32::consts::TAU;
use core::time::Duration;
use std::collections::HashMap;
use mirage_engine::prelude::*;
use mirage_engine::{MAX_VOICES, ray};
const ROOM_HALF: f32 = 6.0;
const WALL_THICKNESS: f32 = 0.3;
const WALL_HEIGHT: f32 = 2.4;
const PLAY_BOUND: f32 = ROOM_HALF - WALL_THICKNESS - 0.4;
const EYE_HEIGHT: f32 = 1.6;
const WALK_SPEED: f32 = 4.0;
const CHASE_BACK: f32 = 6.0;
const CHASE_UP: f32 = 5.0;
const SOURCE_HEIGHT: f32 = 0.4;
const SOURCE_HALF: f32 = 0.22;
const SOURCE_REFERENCE: f32 = 1.5;
const SOURCE_PICK_RADIUS: f32 = 0.5;
const LISTENER_WIDTH: f32 = 0.4;
const LISTENER_DEPTH: f32 = 0.3;
const EAR_SIZE: f32 = 0.14;
const EAR_OFFSET: f32 = 0.24;
const FACING_MARKER_SIZE: f32 = 0.22;
const FLOOR_COLOR: Color = Color::rgb(0.14, 0.14, 0.17);
const WALL_COLOR: Color = Color::rgb(0.22, 0.24, 0.30);
const SUN_COLOR: Color = Color::rgb(0.85, 0.85, 0.90);
const LISTENER_COLOR: Color = Color::rgb(0.85, 0.85, 0.75);
const RIGHT_EAR_COLOR: Color = Color::rgb(0.85, 0.2, 0.2);
const LEFT_EAR_COLOR: Color = Color::rgb(0.92, 0.92, 0.88);
const SOURCE_COLORS: [Color; 3] = [
Color::rgb(0.85, 0.35, 0.35),
Color::rgb(0.35, 0.75, 0.85),
Color::rgb(0.85, 0.75, 0.30),
];
const RANGE_COLOR: Color = Color::rgba(1.0, 1.0, 1.0, 0.35);
const REFERENCE_COLOR: Color = Color::rgba(1.0, 0.85, 0.35, 0.5);
const SKY_ZENITH: Color = Color::rgb(0.10, 0.11, 0.16);
const SKY_HORIZON: Color = Color::rgb(0.20, 0.20, 0.24);
const SKY_NADIR: Color = Color::rgb(0.06, 0.06, 0.08);
const SKY_LIGHT: f32 = 0.2;
const MERGE_POS_A: Vec3 = Vec3::new(-4.0, SOURCE_HEIGHT, 4.5);
const MERGE_POS_B: Vec3 = Vec3::new(4.0, SOURCE_HEIGHT, 4.5);
const MERGE_GAIN: f32 = 0.5;
const MERGE_COLOR_A: Color = Color::rgb(0.95, 0.55, 0.15);
const MERGE_COLOR_B: Color = Color::rgb(0.55, 0.4, 0.85);
const THEME_LOOP_FROM: Duration = Duration::from_secs(130);
const RING_COUNT: u32 = MAX_VOICES as u32 + 8;
const RING_RADIUS: f32 = 4.6;
const RING_REFERENCE: f32 = 2.5;
const RING_GAIN: f32 = 0.35;
const RING_COLOR: Color = Color::rgb(0.35, 0.75, 0.95);
const ASSET_FILES: [&str; 9] = [
"examples/assets/bounce.ogg",
"examples/assets/break.ogg",
"examples/assets/serve.ogg",
"examples/assets/gameover.ogg",
"examples/assets/lost.ogg",
"examples/assets/win.ogg",
"examples/assets/click.ogg",
"examples/assets/music.ogg",
"examples/assets/menu_music.ogg",
];
fn main() {
run(
Config::new("Mirage: sound lab")
.with_size(1280, 720)
.with_assets(ASSET_FILES),
SoundCheck::init,
);
}
#[derive(Catalog, Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Sky {
Room,
}
impl Skyboxes for Sky {
fn build(&self, _assets: &Assets) -> SkyboxData {
match self {
Self::Room => {
SkyboxData::gradient(SKY_ZENITH, SKY_HORIZON, SKY_NADIR).lit_by(SKY_LIGHT)
}
}
}
}
#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
struct Ring;
impl Mesh for Ring {
fn build(&self, _: &Assets) -> MeshData {
ring_outline()
}
}
#[derive(Catalog, Clone, PartialEq, Eq, Hash)]
struct Facing;
impl Mesh for Facing {
fn build(&self, _: &Assets) -> MeshData {
facing_marker()
}
}
meshes! { enum Shape { Plane, Cube, Ring, Sphere, Facing } }
fn ring_outline() -> MeshData {
const SEGMENTS: u32 = 48;
const OUTER: f32 = 1.0;
const INNER: f32 = 0.94;
let mut vertices = Vec::with_capacity(SEGMENTS as usize * 4);
let mut indices = Vec::with_capacity(SEGMENTS as usize * 6);
for segment in 0..SEGMENTS {
let a0 = segment as f32 / SEGMENTS as f32 * TAU;
let a1 = (segment + 1) as f32 / SEGMENTS as f32 * TAU;
let (u0, v0) = (a0.cos(), a0.sin());
let (u1, v1) = (a1.cos(), a1.sin());
let base = vertices.len() as u32;
vertices.extend([
Vertex::new(Vec3::new(INNER * u0, 0.0, -INNER * v0), Vec3::Y, Vec2::ZERO),
Vertex::new(Vec3::new(OUTER * u0, 0.0, -OUTER * v0), Vec3::Y, Vec2::ZERO),
Vertex::new(Vec3::new(OUTER * u1, 0.0, -OUTER * v1), Vec3::Y, Vec2::ZERO),
Vertex::new(Vec3::new(INNER * u1, 0.0, -INNER * v1), Vec3::Y, Vec2::ZERO),
]);
indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
}
MeshData::new(vertices, indices)
}
fn facing_marker() -> MeshData {
const TIP: Vec3 = Vec3::new(0.0, 0.0, -0.5);
const BACK: [Vec3; 4] = [
Vec3::new(-0.5, -0.5, 0.5),
Vec3::new(0.5, -0.5, 0.5),
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(-0.5, 0.5, 0.5),
];
let mut vertices = Vec::with_capacity(BACK.len() * 3);
for (corner, next) in BACK.iter().zip(BACK.iter().cycle().skip(1)) {
let normal = (next - corner).cross(TIP - corner).normalize();
vertices.extend([
Vertex::new(*corner, normal, Vec2::new(0.0, 1.0)),
Vertex::new(*next, normal, Vec2::new(1.0, 1.0)),
Vertex::new(TIP, normal, Vec2::new(0.5, 0.0)),
]);
}
let indices = (0..vertices.len() as u32).collect();
MeshData::new(vertices, indices)
}
#[derive(Catalog, Clone, Copy, PartialEq, Eq, Hash)]
enum Sound {
Bounce,
Break,
Serve,
GameOver,
Lost,
Win,
Click,
Theme,
ThemeDecoded,
MenuTheme,
Pulse,
}
impl Sound {
const ONE_SHOTS: [Sound; 7] = [
Sound::Bounce,
Sound::Break,
Sound::Serve,
Sound::GameOver,
Sound::Lost,
Sound::Win,
Sound::Click,
];
const SOURCE_CHOICES: [Sound; 9] = [
Sound::Bounce,
Sound::Break,
Sound::Serve,
Sound::GameOver,
Sound::Lost,
Sound::Win,
Sound::Click,
Sound::Theme,
Sound::ThemeDecoded,
];
fn label(self) -> &'static str {
match self {
Sound::Bounce => "bounce",
Sound::Break => "break",
Sound::Serve => "serve",
Sound::GameOver => "game over",
Sound::Lost => "lost",
Sound::Win => "win",
Sound::Click => "click",
Sound::Theme => "theme (streamed)",
Sound::ThemeDecoded => "theme (decoded)",
Sound::MenuTheme => "menu theme",
Sound::Pulse => "pulse",
}
}
}
impl Sounds for Sound {
fn build(&self, assets: &Assets) -> SoundData {
match self {
Sound::Bounce => assets.sound("bounce"),
Sound::Break => assets.sound("break"),
Sound::Serve => assets.sound("serve"),
Sound::GameOver => assets.sound("gameover"),
Sound::Lost => assets.sound("lost"),
Sound::Win => assets.sound("win"),
Sound::Click => assets.sound("click"),
Sound::Theme => assets.sound("music").streamed(),
Sound::ThemeDecoded => assets.sound("music"),
Sound::MenuTheme => assets.sound("menu_music").streamed(),
Sound::Pulse => assets.sound("break"),
}
}
}
#[derive(InputButtonAction, Clone, Copy, PartialEq)]
enum Button {
Select,
}
impl InputButtonAction for Button {
fn bindings(&self) -> Vec<ButtonBinding> {
match self {
Button::Select => vec![MouseButton::Left.into()],
}
}
}
#[derive(InputAxis2Action, Clone, Copy, PartialEq)]
enum Move {
Walk,
}
impl InputAxis2Action for Move {
fn bindings(&self) -> Vec<Axis2Binding> {
match self {
Move::Walk => vec![
Axis2Binding::from(ButtonAxis2 {
left: Key::A,
right: Key::D,
down: Key::S,
up: Key::W,
}),
Axis2Binding::stick(Stick::Left),
],
}
}
}
struct Controls;
impl InputActions for Controls {
type Button = Button;
type Axis = NoInputAxes;
type Axis2 = Move;
}
struct Source {
position: Vec3,
sound: Sound,
gain: f32,
reference: f32,
range: f32,
pitch: f32,
enabled: bool,
}
impl Source {
fn new(x: f32, z: f32, sound: Sound, range: f32, enabled: bool) -> Self {
Self {
position: Vec3::new(x, SOURCE_HEIGHT, z),
sound,
gain: 0.5,
reference: SOURCE_REFERENCE,
range,
pitch: 1.0,
enabled,
}
}
fn cue(&self) -> SoundCue<Sound> {
let cue = self
.sound
.at(self.position)
.gain(self.gain)
.reference(self.reference)
.range(self.range)
.pitch(self.pitch);
match self.sound {
Sound::Theme | Sound::ThemeDecoded => cue.loop_from(THEME_LOOP_FROM),
_ => cue,
}
}
}
struct SoundCheck {
master_volume: f32,
picked: Sound,
one_shot_gain: f32,
one_shot_pitch: f32,
one_shot_fade: f32,
trim_start: f32,
trim_end: f32,
one_shot_loop_from: f32,
theme_on: bool,
menu_on: bool,
pulse_on: bool,
cue_fade: f32,
merge_demo: bool,
ring_demo: bool,
player: Vec2,
player_prev: Vec2,
sources: [Source; 3],
dragging: Option<usize>,
durations: HashMap<Sound, Duration>,
}
impl SoundCheck {
fn init(ctx: &mut InitContext<'_, SoundCheck>) -> Result<Self, Error> {
let durations = ctx.durations();
let picked = Sound::Bounce;
let trim_end = durations.get(&picked).copied().unwrap_or_default();
Ok(Self {
master_volume: 1.0,
picked,
one_shot_gain: 1.0,
one_shot_pitch: 1.0,
one_shot_fade: SoundCue::<Sound>::DEFAULT_FADE.as_secs_f32(),
trim_start: 0.0,
trim_end: trim_end.as_secs_f32(),
one_shot_loop_from: 0.0,
theme_on: false,
menu_on: false,
pulse_on: false,
cue_fade: 1.0,
merge_demo: false,
ring_demo: false,
player: Vec2::ZERO,
player_prev: Vec2::ZERO,
sources: [
Source::new(-2.5, -2.0, Sound::Bounce, 4.0, false),
Source::new(2.5, -2.0, Sound::Serve, 4.0, false),
Source::new(0.0, 2.8, Sound::Theme, 7.0, true),
],
dragging: None,
durations,
})
}
fn camera(player: Vec2) -> Camera {
let ground = Vec3::new(player.x, 0.0, player.y);
Camera::new(
View::look_at(
ground + Vec3::new(0.0, CHASE_UP, CHASE_BACK),
ground + Vec3::Y * 0.5,
),
Projection::perspective(55.0),
)
}
fn handle_walk(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
self.player_prev = self.player;
if ctx.ui_wants_keyboard() {
return;
}
let walk = ctx.axis2(Move::Walk);
let world = Vec2::new(walk.x, -walk.y);
self.player = (self.player + world * WALK_SPEED * ctx.dt().as_secs_f32())
.clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
}
fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
if ctx.released(Button::Select) {
self.dragging = None;
}
if ctx.ui_wants_pointer() {
return;
}
let ray = ctx
.last_camera()
.ray_through(ctx.pointer(), ctx.window_size());
if ctx.pressed(Button::Select) {
self.dragging = self.sources.iter().position(|source| {
ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
.is_some()
});
}
let Some(index) = self.dragging else {
return;
};
let Some(distance) = ray.hit_plane(ray::Plane {
point: Vec3::ZERO,
normal: Vec3::Y,
}) else {
return;
};
let hit = ray.at(distance);
let dropped =
Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
}
fn draw_room(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
ctx.draw(
Plane
.at(Transform::from_scale(Vec3::new(
ROOM_HALF * 2.0,
1.0,
ROOM_HALF * 2.0,
)))
.material(Material::lit(FLOOR_COLOR)),
);
let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, ROOM_HALF);
for side in [-1.0, 1.0] {
let x = side * (ROOM_HALF - WALL_THICKNESS * 0.5);
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
side_half * 2.0,
Quat::IDENTITY,
Vec3::new(x, side_half.y, 0.0),
))
.material(Material::lit(WALL_COLOR)),
);
}
let end_half = Vec3::new(ROOM_HALF, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
for side in [-1.0, 1.0] {
let z = side * (ROOM_HALF - WALL_THICKNESS * 0.5);
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
end_half * 2.0,
Quat::IDENTITY,
Vec3::new(0.0, end_half.y, z),
))
.material(Material::lit(WALL_COLOR)),
);
}
}
fn draw_sources(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
for (index, source) in self.sources.iter().enumerate() {
let color = SOURCE_COLORS[index];
let picked_up = self.dragging == Some(index);
let scale = if picked_up { 1.3 } else { 1.0 };
let emissive = if source.enabled {
Color::rgb(color.red * 3.0, color.green * 3.0, color.blue * 3.0)
} else {
color.dimmed(0.15)
};
for (radius, ring_color) in [
(source.range, RANGE_COLOR),
(source.reference, REFERENCE_COLOR),
] {
ctx.draw(
Ring.at(Transform::from_scale_rotation_translation(
Vec3::new(radius, 1.0, radius),
Quat::IDENTITY,
Vec3::new(source.position.x, 0.01, source.position.z),
))
.material(Material::color(ring_color)),
);
}
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::splat(SOURCE_HALF * 2.0 * scale),
Quat::IDENTITY,
source.position,
))
.material(Material::shaded(color, 0.6).emissive(emissive)),
);
}
}
fn draw_listener(&self, ctx: &mut FrameContext<'_, SoundCheck>, view: View) {
let head = view.eye();
let ground = Vec3::new(head.x, 0.0, head.z);
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::new(LISTENER_WIDTH, head.y, LISTENER_DEPTH),
Quat::IDENTITY,
ground + Vec3::Y * head.y * 0.5,
))
.material(Material::lit(LISTENER_COLOR)),
);
let right = listener_right(view) * EAR_OFFSET;
for (offset, color) in [(right, RIGHT_EAR_COLOR), (-right, LEFT_EAR_COLOR)] {
ctx.draw(
Sphere { subdivisions: 1 }
.at(Transform::from_scale_rotation_translation(
Vec3::splat(EAR_SIZE),
Quat::IDENTITY,
head + offset,
))
.material(Material::lit(color)),
);
}
ctx.draw(
Facing
.at(Transform::from_scale_rotation_translation(
Vec3::splat(FACING_MARKER_SIZE),
Quat::IDENTITY,
head + Vec3::NEG_Z * (FACING_MARKER_SIZE * 0.5),
))
.material(Material::lit(LISTENER_COLOR)),
);
}
fn draw_merge_markers(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
if !self.merge_demo {
return;
}
for (position, color) in [(MERGE_POS_A, MERGE_COLOR_A), (MERGE_POS_B, MERGE_COLOR_B)] {
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::splat(SOURCE_HALF * 2.0),
Quat::IDENTITY,
position,
))
.material(Material::lit(color)),
);
}
}
fn draw_ring(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
if !self.ring_demo {
return;
}
for nth in 0..RING_COUNT {
let over = 1.0 - nth as f32 / RING_COUNT as f32;
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::splat(SOURCE_HALF),
Quat::IDENTITY,
ring_place(nth),
))
.material(Material::lit(RING_COLOR.dimmed(over))),
);
}
}
fn side_panel(&mut self, ctx: &mut FrameContext<'_, SoundCheck>) {
#[cfg(target_arch = "wasm32")]
let unlocked = ctx.sound_unlocked();
let master_volume = &mut self.master_volume;
let theme_on = &mut self.theme_on;
let menu_on = &mut self.menu_on;
let pulse_on = &mut self.pulse_on;
let cue_fade = &mut self.cue_fade;
let merge_demo = &mut self.merge_demo;
let ring_demo = &mut self.ring_demo;
let ring_label = format!("cap demo: sustain {RING_COUNT} sounds at once");
let ring_note = format!(
"each one is quieter than the one before it, so the engine plays the loudest {MAX_VOICES} and the rest go silent without stopping"
);
let sources = &mut self.sources;
ctx.ui(|ui| {
egui::Panel::left("controls").show(ui, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
ui.heading("master");
ui.add(egui::Slider::new(master_volume, 0.0..=1.5).text("volume"));
#[cfg(target_arch = "wasm32")]
if !unlocked {
ui.label("audio unlocks on the first click or key in the browser");
}
ui.separator();
ui.heading("cue lab");
ui.label("a checked box is the sustain declaration");
ui.label("unchecking fades it out and parks it");
ui.checkbox(theme_on, Sound::Theme.label());
ui.checkbox(menu_on, Sound::MenuTheme.label());
ui.checkbox(pulse_on, Sound::Pulse.label());
ui.add(egui::Slider::new(cue_fade, 0.0..=3.0).text("fade (seconds)"));
ui.separator();
ui.heading("spatial lab");
ui.label("drag a source's marker on the floor to move it");
ui.label(
"a source is at full level inside its gold ring and falls to nothing at the white one",
);
ui.label("red is the right ear (RCA convention), white is the left");
ui.label("the point on the listener always faces -Z");
for (index, source) in sources.iter_mut().enumerate() {
ui.push_id(index, |ui| {
ui.separator();
ui.label(format!("source {}", index + 1));
ui.checkbox(&mut source.enabled, "enabled");
egui::ComboBox::from_label("clip")
.selected_text(source.sound.label())
.show_ui(ui, |ui| {
for choice in Sound::SOURCE_CHOICES {
ui.selectable_value(
&mut source.sound,
choice,
choice.label(),
);
}
});
ui.add(egui::Slider::new(&mut source.gain, 0.0..=2.0).text("gain"));
ui.add(
egui::Slider::new(&mut source.range, 1.0..=12.0).text("range"),
);
let range = source.range;
ui.add(
egui::Slider::new(&mut source.reference, 0.25..=range)
.text("reference"),
);
ui.add(
egui::Slider::new(&mut source.pitch, 0.5..=2.0).text("pitch"),
);
});
}
ui.separator();
ui.label(
"each enabled source above sustains at its own instance (0, 1, 2 by position), so the same clip can play at every one without merging into one voice",
);
ui.checkbox(merge_demo, "merge demo: same clip, both at instance 0");
ui.label(
"both declarations below target the same clip at the default instance",
);
ui.label(
"only the one declared last is heard, proof of what the sources above avoid",
);
ui.separator();
ui.checkbox(ring_demo, &ring_label);
ui.label(&ring_note);
ui.label(
"walk into the ring, or turn a source up, and what is played changes with what is loudest",
);
});
});
});
}
fn one_shot_panel(&mut self, ctx: &mut FrameContext<'_, SoundCheck>) -> (bool, bool) {
let mut play_once = false;
let mut play_many = false;
let durations = &self.durations;
let picked = &mut self.picked;
let gain = &mut self.one_shot_gain;
let pitch = &mut self.one_shot_pitch;
let fade = &mut self.one_shot_fade;
let trim_start = &mut self.trim_start;
let trim_end = &mut self.trim_end;
let loop_from = &mut self.one_shot_loop_from;
let duration = durations
.get(picked)
.copied()
.unwrap_or_default()
.as_secs_f32()
.max(0.001);
ctx.ui(|ui| {
egui::Panel::bottom("one-shot").show(ui, |ui| {
ui.heading("one-shot lab");
egui::ComboBox::from_label("clip")
.selected_text(picked.label())
.show_ui(ui, |ui| {
for choice in Sound::ONE_SHOTS {
if ui
.selectable_label(*picked == choice, choice.label())
.clicked()
&& *picked != choice
{
*picked = choice;
*trim_start = 0.0;
*trim_end = durations
.get(&choice)
.copied()
.unwrap_or_default()
.as_secs_f32();
*loop_from = 0.0;
}
}
});
ui.add(egui::Slider::new(gain, 0.0..=2.0).text("gain"));
ui.add(egui::Slider::new(pitch, 0.5..=2.0).text("pitch"));
ui.add(egui::Slider::new(fade, 0.0..=2.0).text("fade (seconds)"));
duration_bar(ui, duration, trim_start, trim_end, loop_from);
ui.label(
"the marker sets loop_from, which a one-shot ignores: only sustain reads it",
);
ui.horizontal(|ui| {
play_once = ui.button("play").clicked();
play_many = ui.button("play ×32 (overruns the voice cap)").clicked();
});
});
});
(play_once, play_many)
}
fn one_shot_cue(&self) -> SoundCue<Sound> {
self.picked
.gain(self.one_shot_gain)
.pitch(self.one_shot_pitch)
.fade(Duration::from_secs_f32(self.one_shot_fade))
.trim_to(
Duration::from_secs_f32(self.trim_start),
Duration::from_secs_f32(self.trim_end),
)
.loop_from(Duration::from_secs_f32(self.one_shot_loop_from))
}
fn sustain_ring(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
if !self.ring_demo {
return;
}
for nth in 0..RING_COUNT {
let gain = RING_GAIN * (1.0 - nth as f32 / RING_COUNT as f32);
ctx.sustain(
Sound::Pulse
.at(ring_place(nth))
.gain(gain)
.reference(RING_REFERENCE)
.range(RING_RADIUS * 3.0)
.instance(nth + 1),
);
}
}
fn sustain_cues(&self, ctx: &mut FrameContext<'_, SoundCheck>) {
let fade = Duration::from_secs_f32(self.cue_fade);
if self.theme_on {
ctx.sustain(Sound::Theme.gain(0.5).fade(fade));
}
if self.menu_on {
ctx.sustain(Sound::MenuTheme.gain(0.5).fade(fade));
}
if self.pulse_on {
ctx.sustain(Sound::Pulse.gain(0.3).fade(fade));
}
}
}
fn ring_place(nth: u32) -> Vec3 {
let turn = TAU * nth as f32 / RING_COUNT as f32;
Vec3::new(
turn.sin() * RING_RADIUS,
SOURCE_HEIGHT,
turn.cos() * RING_RADIUS,
)
}
fn listener_right(view: View) -> Vec3 {
(view.target() - view.eye())
.normalize_or_zero()
.cross(view.up())
}
fn duration_bar(
ui: &mut egui::Ui,
duration: f32,
trim_start: &mut f32,
trim_end: &mut f32,
loop_from: &mut f32,
) {
let size = egui::vec2(ui.available_width().min(420.0), 28.0);
let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
let painter = ui.painter();
painter.rect_filled(rect, 3.0, egui::Color32::from_gray(35));
let x_of = |seconds: f32| rect.left() + (seconds / duration).clamp(0.0, 1.0) * rect.width();
let seconds_of = |x: f32| ((x - rect.left()) / rect.width()).clamp(0.0, 1.0) * duration;
let span = egui::Rect::from_min_max(
egui::pos2(x_of(*trim_start), rect.top()),
egui::pos2(x_of(*trim_end), rect.bottom()),
);
painter.rect_filled(span, 3.0, egui::Color32::from_rgb(70, 120, 95));
let start_x = x_of(*trim_start);
if let Some(x) = drag_handle(
ui,
rect,
"trim-start",
start_x,
egui::Color32::from_rgb(230, 200, 80),
) {
*trim_start = seconds_of(x).min(*trim_end);
}
let end_x = x_of(*trim_end);
if let Some(x) = drag_handle(
ui,
rect,
"trim-end",
end_x,
egui::Color32::from_rgb(230, 200, 80),
) {
*trim_end = seconds_of(x).max(*trim_start);
}
let loop_x = x_of(*loop_from);
if let Some(x) = drag_handle(
ui,
rect,
"loop-from",
loop_x,
egui::Color32::from_rgb(90, 170, 230),
) {
*loop_from = seconds_of(x).clamp(*trim_start, *trim_end);
}
}
fn drag_handle(
ui: &mut egui::Ui,
bar: egui::Rect,
salt: &str,
x: f32,
color: egui::Color32,
) -> Option<f32> {
let radius = 6.0;
let center = egui::pos2(x, bar.center().y);
let sense_rect = egui::Rect::from_center_size(center, egui::Vec2::splat(radius * 2.5));
let id = ui.id().with(salt);
let response = ui.interact(sense_rect, id, egui::Sense::drag());
ui.painter().circle_filled(center, radius, color);
response
.dragged()
.then(|| response.interact_pointer_pos())
.flatten()
.map(|pos| pos.x)
}
impl Game for SoundCheck {
type Meshes = Shape;
type Sounds = Sound;
type InputActions = Controls;
type Skyboxes = Sky;
type SurfaceStyles = NoSurfaceStyles;
type PostEffects = NoPostEffects;
fn tick(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
self.handle_walk(ctx);
self.handle_drag(ctx);
}
fn frame(&mut self, ctx: &mut FrameContext<'_, SoundCheck>) {
ctx.set_volume(self.master_volume);
let player = self.player_prev.lerp(self.player, ctx.alpha());
let ear = Vec3::new(player.x, EYE_HEIGHT, player.y);
let listener = View::look_at(ear, ear + Vec3::NEG_Z);
ctx.set_listener(listener);
ctx.set_camera(Self::camera(player));
ctx.set_skybox(Sky::Room);
ctx.set_bloom(0.2);
ctx.light(Light::directional(Vec3::new(-0.4, -1.0, -0.5), SUN_COLOR).shadow());
self.draw_room(ctx);
self.draw_sources(ctx);
self.draw_listener(ctx, listener);
self.draw_merge_markers(ctx);
self.draw_ring(ctx);
self.sustain_cues(ctx);
for (index, source) in self.sources.iter().enumerate() {
if source.enabled {
ctx.sustain(source.cue().instance(index as u32));
}
}
if self.merge_demo {
ctx.sustain(Sound::Click.at(MERGE_POS_A).gain(MERGE_GAIN));
ctx.sustain(Sound::Click.at(MERGE_POS_B).gain(MERGE_GAIN));
}
self.sustain_ring(ctx);
self.side_panel(ctx);
let (play_once, play_many) = self.one_shot_panel(ctx);
if play_once {
ctx.play(self.one_shot_cue());
}
if play_many {
for _ in 0..32 {
ctx.play(self.one_shot_cue());
}
}
}
}