aboutsummaryrefslogtreecommitdiff
path: root/apps/fparkan-game/src/audio.rs
diff options
context:
space:
mode:
Diffstat (limited to 'apps/fparkan-game/src/audio.rs')
-rw-r--r--apps/fparkan-game/src/audio.rs120
1 files changed, 117 insertions, 3 deletions
diff --git a/apps/fparkan-game/src/audio.rs b/apps/fparkan-game/src/audio.rs
index 2de6464..a89e275 100644
--- a/apps/fparkan-game/src/audio.rs
+++ b/apps/fparkan-game/src/audio.rs
@@ -629,6 +629,7 @@ pub(crate) struct GameAudio {
music: Player,
variations: Player,
weather_loop: NativeSpatialPlayer,
+ control_sounds: BTreeMap<usize, NativeSpatialPlayer>,
one_shots: Vec<SpatialOneShot>,
library: AudioLibrary,
ambience: AmbienceScheduler,
@@ -672,6 +673,7 @@ impl GameAudio {
music,
variations,
weather_loop,
+ control_sounds: BTreeMap::new(),
one_shots: Vec::new(),
library,
ambience: AmbienceScheduler::new(random_seed(), random_seed()),
@@ -705,6 +707,11 @@ impl GameAudio {
self.weather_loop
.set_emitter_position(self.weather_position);
self.retain_active_one_shots();
+ self.control_sounds.retain(|_, player| !player.empty());
+ for player in self.control_sounds.values() {
+ player.set_left_ear_position(left);
+ player.set_right_ear_position(right);
+ }
for one_shot in &self.one_shots {
one_shot.player.set_left_ear_position(left);
one_shot.player.set_right_ear_position(right);
@@ -825,19 +832,74 @@ impl GameAudio {
SoundAction::OneShot => {
self.retain_active_one_shots();
let asset = self.dynamic_sound(&event.archive, &event.name)?;
- self.one_shots.push(self.new_spatial_one_shot(
+ self.one_shots.push(self.new_spatial_voice(
&asset,
event.position,
volume,
min_distance,
max_distance,
frequency_ratio,
+ false,
)?);
}
}
Ok(())
}
+ /// Each Control command owns its source; it must not replace the rain
+ /// loop or another instance using the same sample.
+ pub(crate) fn handle_control_sound(
+ &mut self,
+ id: usize,
+ event: &SoundEvent,
+ ) -> Result<(), AudioError> {
+ validate_point(event.position, "Control emitter")?;
+ let frequency_ratio = if event.frequency_ratio.is_finite() && event.frequency_ratio > 0.0 {
+ event.frequency_ratio
+ } else {
+ 1.0
+ };
+ match event.action {
+ SoundAction::StartLoop | SoundAction::OneShot => {
+ let asset = self.dynamic_sound(&event.archive, &event.name)?;
+ let voice = self.new_spatial_voice(
+ &asset,
+ event.position,
+ event.volume,
+ event.min_distance,
+ event.max_distance,
+ frequency_ratio,
+ event.action == SoundAction::StartLoop,
+ )?;
+ if let Some(previous) = self.control_sounds.insert(id, voice.player) {
+ previous.stop();
+ }
+ eprintln!("control_sound_start id={id} resource={}:{} loop={} position={:?} range={}/{} frequency={}", event.archive, event.name, event.action == SoundAction::StartLoop, event.position, event.min_distance, event.max_distance, event.frequency_ratio);
+ }
+ SoundAction::SetLoopVolume => {
+ if let Some(player) = self.control_sounds.get(&id) {
+ player.set_emitter_position(event.position);
+ player.set_sound_parameters(event.min_distance, event.max_distance);
+ player.set_volume(event.volume);
+ player.set_speed(frequency_ratio);
+ }
+ }
+ SoundAction::StopLoop => {
+ if let Some(player) = self.control_sounds.remove(&id) {
+ player.stop();
+ eprintln!("control_sound_stop id={id}");
+ }
+ }
+ }
+ Ok(())
+ }
+
+ pub(crate) fn control_sound_is_playing(&self, id: usize) -> bool {
+ self.control_sounds
+ .get(&id)
+ .is_some_and(|player| !player.empty())
+ }
+
/// Selects one of the original MUSIC/Track02..10.ogg files and loops it.
pub(crate) fn play_music_track(&mut self, track: u8) -> Result<(), AudioError> {
let bytes = self.library.music_bytes(track)?;
@@ -875,6 +937,13 @@ impl GameAudio {
one_shot.player.play();
}
}
+ for player in self.control_sounds.values() {
+ if paused {
+ player.pause();
+ } else {
+ player.play();
+ }
+ }
}
/// Focus callback used by the window event loop.
@@ -888,6 +957,9 @@ impl GameAudio {
self.music.stop();
self.variations.stop();
self.weather_loop.stop();
+ for player in self.control_sounds.values() {
+ player.stop();
+ }
for one_shot in &self.one_shots {
one_shot.player.stop();
}
@@ -904,7 +976,7 @@ impl GameAudio {
Ok(())
}
- fn new_spatial_one_shot(
+ fn new_spatial_voice(
&self,
asset: &SoundAsset,
emitter: [f32; 3],
@@ -912,6 +984,7 @@ impl GameAudio {
min_distance: f32,
max_distance: f32,
frequency_ratio: f32,
+ looping: bool,
) -> Result<SpatialOneShot, AudioError> {
validate_point(emitter, "emitter")?;
let (left, right) = ear_positions(self.listener)?;
@@ -919,7 +992,12 @@ impl GameAudio {
player.set_sound_parameters(min_distance, max_distance);
player.set_volume(volume);
player.set_speed(frequency_ratio);
- player.append(decoder_for(asset)?);
+ let source = decoder_for(asset)?;
+ if looping {
+ player.append(source.repeat_infinite());
+ } else {
+ player.append(source);
+ }
if self.paused {
player.pause();
} else {
@@ -1695,6 +1773,42 @@ mod tests {
})
.expect("update original rain loop volume");
assert_eq!(audio.weather_loop.volume(), 0.5);
+ let mut machine = SoundEvent {
+ archive: "sounds.lib".to_string(),
+ name: "f_gener.wav".to_string(),
+ position: [1.0, 2.0, 3.0],
+ volume: 0.0,
+ min_distance: 15.0,
+ max_distance: 100.0,
+ frequency_ratio: 1.0,
+ action: SoundAction::StartLoop,
+ };
+ audio
+ .handle_control_sound(0, &machine)
+ .expect("first machine");
+ audio
+ .handle_control_sound(1, &machine)
+ .expect("second machine");
+ assert!(audio.control_sound_is_playing(0));
+ assert!(audio.control_sound_is_playing(1));
+ assert!(!audio.weather_loop.empty());
+ audio.set_paused(true);
+ assert!(audio
+ .control_sounds
+ .values()
+ .all(|player| player.is_paused()));
+ audio.set_paused(false);
+ assert!(audio
+ .control_sounds
+ .values()
+ .all(|player| !player.is_paused()));
+ machine.action = SoundAction::StopLoop;
+ audio
+ .handle_control_sound(0, &machine)
+ .expect("stop one machine");
+ assert!(!audio.control_sound_is_playing(0));
+ assert!(audio.control_sound_is_playing(1));
+ assert!(!audio.weather_loop.empty());
audio
.update_listener([10.0, 20.0, 30.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0])
.expect("move listener");