khora_agents/audio_agent/mod.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The Intelligent Subsystem Agent responsible for managing the audio system.
16
17use anyhow::Result;
18use khora_core::audio::device::AudioDevice;
19use khora_data::ecs::World;
20use khora_lanes::audio_lane::{AudioMixingLane, SpatialMixingLane};
21use std::sync::{Arc, RwLock};
22
23/// The ISA that orchestrates the entire audio system.
24pub struct AudioAgent {
25 /// The audio device used for playback.
26 device: Option<Box<dyn AudioDevice>>,
27 /// The audio mixing lane responsible for mixing audio sources.
28 mixing_lane: Arc<dyn AudioMixingLane>,
29 /// A thread-safe, shareable reference to the ECS `World`.
30 world: Arc<RwLock<World>>,
31}
32
33impl AudioAgent {
34 /// Creates a new `AudioAgent`.
35 ///
36 /// # Arguments
37 /// * `world`: A thread-safe, shareable reference to the ECS `World`.
38 /// * `device`: A boxed, concrete implementation of the `AudioDevice` trait.
39 pub fn new(world: Arc<RwLock<World>>, device: Box<dyn AudioDevice>) -> Self {
40 Self {
41 device: Some(device),
42 mixing_lane: Arc::new(SpatialMixingLane::new()),
43 world,
44 }
45 }
46
47 /// Initializes the audio backend and starts the audio stream.
48 /// This method consumes the device, so it can only be called once.
49 pub fn start(&mut self) -> Result<()> {
50 // Take ownership of the device. This ensures `start` can't be called twice.
51 if let Some(device_boxed) = self.device.take() {
52 let mixing_lane = self.mixing_lane.clone();
53 let world = self.world.clone();
54
55 let on_mix_needed = Box::new(
56 move |output_buffer: &mut [f32],
57 stream_info: &khora_core::audio::device::StreamInfo| {
58 if let Ok(mut world) = world.write() {
59 mixing_lane.mix(&mut world, output_buffer, stream_info);
60 }
61 },
62 );
63
64 // Start the device stream.
65 device_boxed.start(on_mix_needed)
66 } else {
67 // The device has already been started or was never provided.
68 Ok(())
69 }
70 }
71}