Audio: WAV + tinyaudio, Ambient-Emitter mit Distance ramps; artist.md
engine/wav.rs (Decoder) und render/audio.rs (Mixer, 8 SFX-Voices + Loop-Voice je Emitter) aus irl3d eingebettet; Emitter-Logik headless in engine/audio.rs. Empties mit sound/radius-Property senden positional, play_sound-Signale laufen als SFX über Session::take_sounds. Platzhalter-Sounds + Emitter in der Testszene; artist.md fasst die Content-Regeln für die Autorin zusammen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+51
-4
@@ -14,6 +14,7 @@
|
||||
//! Schicht-Regel wie `cli`: Geschwister von `engine`/`session`, konsumiert
|
||||
//! deren Schnittstellen — nie umgekehrt.
|
||||
|
||||
mod audio;
|
||||
mod camera;
|
||||
mod font;
|
||||
mod gpu;
|
||||
@@ -23,6 +24,7 @@ mod scene;
|
||||
mod sprite;
|
||||
mod ui;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc::{self, Receiver};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -35,7 +37,7 @@ use winit::window::{CursorGrabMode, Window, WindowId};
|
||||
|
||||
use crate::engine::player;
|
||||
use crate::engine::tga::Image;
|
||||
use crate::engine::{assets, gltf, model, tga};
|
||||
use crate::engine::{assets, gltf, model, tga, wav};
|
||||
use crate::session::{FrameInput, Mode, Session};
|
||||
use camera::Camera;
|
||||
use gpu::Gpu;
|
||||
@@ -79,10 +81,22 @@ pub fn run(mut session: Session) {
|
||||
println!("[scene] {} Modelle, Texturen {:?} → {} Vertices, {} Dreiecke",
|
||||
models.len(), tex_names, mesh.verts.len(), mesh.indices.len() / 3);
|
||||
|
||||
// Sim-Welt (Collider, Trigger, Spawn-Empty) in die Session einspielen —
|
||||
// die Simulation gehört der Session, nicht dem Render-Frontend.
|
||||
// Sim-Welt (Collider, Trigger, Emitter, Spawn-Empty) in die Session
|
||||
// einspielen — die Simulation gehört der Session, nicht dem
|
||||
// Render-Frontend.
|
||||
for (_, m) in &models { session.load_props(m); }
|
||||
|
||||
// Audio-Treiber öffnen (vor dem stdin-Thread, siehe Audio::open) und je
|
||||
// Ambient-Emitter eine Loop-Voice anlegen; die Gains zieht redraw()
|
||||
// dann jeden Frame aus der Session. Ohne Gerät läuft das Spiel stumm.
|
||||
let audio = audio::Audio::open();
|
||||
if let Some(a) = &audio {
|
||||
let sounds: Vec<Option<audio::Sound>> = session.emitters().iter()
|
||||
.map(|e| load_sound(&e.sound))
|
||||
.collect();
|
||||
a.set_ambients(&sounds);
|
||||
}
|
||||
|
||||
// UI-Texturen, Reihenfolge = die Index-Konstanten in ui (WHITE, FONT_EGA,
|
||||
// FONT_CGA, CURSORS, ORN). Decode (CPU) bleibt in run(); Fonts, Cursor und
|
||||
// Ornament sind weiß-auf-schwarz und bekommen ihre Alpha-Maske aus der
|
||||
@@ -126,7 +140,7 @@ pub fn run(mut session: Session) {
|
||||
// Poll statt Wait: wir rendern kontinuierlich (Spiel, kein Editor) und
|
||||
// pollen nebenbei den Befehls-Channel.
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
let mut app = App::new(session, rx, mesh, images, ui_textures, fonts);
|
||||
let mut app = App::new(session, rx, mesh, images, ui_textures, fonts, audio);
|
||||
event_loop.run_app(&mut app).expect("winit: run");
|
||||
}
|
||||
|
||||
@@ -170,6 +184,18 @@ fn load_models(dir: &str) -> Vec<(String, model::Model)> {
|
||||
/// Eine Welt-Textur laden: `assets/textures/{name}.tga`. Fehlt die Datei
|
||||
/// (Material ohne Textur, Tippfehler im Blender-Materialnamen), gibt es den
|
||||
/// Platzhalter statt eines Boot-Panics — Texturnamen sind Autorinnen-Content.
|
||||
/// Sound laden: `name` → `assets/sounds/{name}.wav`. Autorinnen-Content —
|
||||
/// Fehler werden gemeldet und ergeben Stille (`None`), nie einen Absturz.
|
||||
fn load_sound(name: &str) -> Option<audio::Sound> {
|
||||
match wav::load(&assets::path(&format!("assets/sounds/{name}.wav"))) {
|
||||
Ok(w) => Some(audio::Sound::new(w)),
|
||||
Err(e) => {
|
||||
eprintln!("[audio] {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_texture(name: &str) -> Image {
|
||||
let path = assets::path(&format!("assets/textures/{name}.tga"));
|
||||
if std::path::Path::new(&path).exists() {
|
||||
@@ -220,6 +246,11 @@ struct App {
|
||||
images: Vec<Image>,
|
||||
ui_textures: Vec<Image>,
|
||||
fonts: font::Fonts,
|
||||
/// Audio-Treiber (`None` = kein Gerät, Spiel läuft stumm) und der
|
||||
/// Lazy-Cache der Einmal-SFX (`None` = Laden schlug fehl, nur einmal
|
||||
/// melden, nicht jeden Klick).
|
||||
audio: Option<audio::Audio>,
|
||||
sfx_cache: HashMap<String, Option<audio::Sound>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -230,6 +261,7 @@ impl App {
|
||||
images: Vec<Image>,
|
||||
ui_textures: Vec<Image>,
|
||||
fonts: font::Fonts,
|
||||
audio: Option<audio::Audio>,
|
||||
) -> Self {
|
||||
// Kamera-Augenhöhe initial auf den Spawn des Players (gehört der Session).
|
||||
let eye_y = session.player.pos[1] + player::EYE_HEIGHT;
|
||||
@@ -249,6 +281,8 @@ impl App {
|
||||
images,
|
||||
ui_textures,
|
||||
fonts,
|
||||
audio,
|
||||
sfx_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,6 +446,19 @@ impl App {
|
||||
self.input.mouse_dy = 0.0;
|
||||
self.input.jump = false;
|
||||
|
||||
// Audio: Distance ramps der Ambient-Emitter nachführen und
|
||||
// aufgelaufene Einmal-SFX (play_sound-Actions) starten. Die Queue
|
||||
// immer leeren, auch ohne Gerät — sonst wüchse sie unbegrenzt.
|
||||
let sfx = self.session.take_sounds();
|
||||
if let Some(a) = &self.audio {
|
||||
a.set_ambient_gains(&self.session.emitter_gains());
|
||||
for name in sfx {
|
||||
let sound = self.sfx_cache.entry(name.clone())
|
||||
.or_insert_with(|| load_sound(&name));
|
||||
if let Some(s) = sound { a.play(s); }
|
||||
}
|
||||
}
|
||||
|
||||
// Eye-Height-Smoothing: die Kamera-Augenhöhe gleitet am Boden sanft auf
|
||||
// den Zielwert nach, statt Step-up-/Slope-Kanten-Sprüngen sofort zu
|
||||
// folgen (versteckt den AABB-auf-Slope-„Pop" und macht Treppen weich).
|
||||
|
||||
Reference in New Issue
Block a user