Skip to main content

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//!
17//! Per CLAD the agent owns no hardware state. The audio device is opened
18//! by the application during bootstrap; the resulting [`AudioStream`]
19//! handle and the shared [`AudioMixBus`] live in the runtime. Each frame
20//! the agent reads the per-tick `AudioView` from the `LaneBus`, builds a
21//! `LaneContext` containing the bus + the view + a slot to the
22//! `OutputDeck`, and dispatches its audio lanes (currently only
23//! [`SpatialMixingLane`]). Lanes mix into a staging buffer and push
24//! samples to the bus; the backend's audio callback drains the bus on a
25//! dedicated real-time thread.
26
27use std::sync::Arc;
28use std::time::Duration;
29
30use khora_core::agent::{Agent, AgentImportance, ExecutionPhase, ExecutionTiming};
31use khora_core::audio::AudioMixBus;
32use khora_core::control::gorna::{
33    AgentId, AgentStatus, NegotiationRequest, NegotiationResponse, ResourceBudget, StrategyId,
34    StrategyOption,
35};
36use khora_core::lane::{LaneContext, LaneRegistry, Ref, Slot};
37use khora_core::EngineContext;
38use khora_data::flow::AudioView;
39use khora_lanes::audio_lane::SpatialMixingLane;
40
41/// The ISA that orchestrates the audio subsystem.
42pub struct AudioAgent {
43    /// Audio processing lanes. Today only `SpatialMixingLane`; future
44    /// strategies (occlusion, reverb, music streaming) plug in here.
45    lanes: LaneRegistry,
46    /// Currently selected lane, picked by `apply_budget` from
47    /// [`StrategyId`].
48    current_lane: &'static str,
49    /// Current GORNA strategy.
50    current_strategy: StrategyId,
51    /// Max audio sources to process per frame (from budget).
52    max_sources_per_frame: usize,
53}
54
55impl Default for AudioAgent {
56    fn default() -> Self {
57        let mut lanes = LaneRegistry::new();
58        lanes.register(Box::new(SpatialMixingLane::new()));
59
60        Self {
61            lanes,
62            current_lane: "SpatialMixing",
63            current_strategy: StrategyId::Balanced,
64            max_sources_per_frame: 32,
65        }
66    }
67}
68
69impl Agent for AudioAgent {
70    fn id(&self) -> AgentId {
71        AgentId::Audio
72    }
73
74    fn negotiate(&mut self, _request: NegotiationRequest) -> NegotiationResponse {
75        NegotiationResponse {
76            strategies: vec![
77                StrategyOption {
78                    id: StrategyId::LowPower,
79                    estimated_time: Duration::from_micros(100),
80                    estimated_vram: 0,
81                },
82                StrategyOption {
83                    id: StrategyId::Balanced,
84                    estimated_time: Duration::from_micros(500),
85                    estimated_vram: 0,
86                },
87                StrategyOption {
88                    id: StrategyId::HighPerformance,
89                    estimated_time: Duration::from_micros(2000),
90                    estimated_vram: 0,
91                },
92            ],
93            timing_adjustment: None,
94        }
95    }
96
97    fn apply_budget(&mut self, budget: ResourceBudget) {
98        log::info!("AudioAgent: Strategy update to {:?}", budget.strategy_id);
99
100        self.current_strategy = budget.strategy_id;
101        self.max_sources_per_frame = match budget.strategy_id {
102            StrategyId::LowPower => 8,
103            StrategyId::Balanced => 32,
104            StrategyId::HighPerformance => 128,
105            StrategyId::Custom(n) => n as usize,
106        };
107    }
108
109    fn on_initialize(&mut self, _context: &mut EngineContext<'_>) {
110        let mut init_ctx = LaneContext::new();
111        for lane in self.lanes.all() {
112            if let Err(e) = lane.on_initialize(&mut init_ctx) {
113                log::error!(
114                    "AudioAgent: Failed to initialize lane {}: {}",
115                    lane.strategy_name(),
116                    e
117                );
118            }
119        }
120
121        log::info!("AudioAgent: Initialized with {} lanes", self.lanes.len());
122    }
123
124    fn execute(&mut self, context: &mut EngineContext<'_>) {
125        let Some(mix_bus) = context
126            .runtime
127            .resources
128            .get::<Arc<dyn AudioMixBus>>()
129            .cloned()
130        else {
131            log::debug!("AudioAgent: no AudioMixBus in resources, skipping mix");
132            return;
133        };
134
135        let Some(view): Option<&AudioView> = context.bus.get() else {
136            // AudioFlow not run yet (or no audio content) — nothing to mix.
137            return;
138        };
139
140        let mut ctx = LaneContext::new();
141        ctx.insert(mix_bus);
142        // SAFETY: `view` is borrowed from the LaneBus, which lives for
143        // the whole frame and is read-only; the Ref's pointer outlives
144        // its only consumer (the lane below).
145        ctx.insert(Ref::new(view));
146        // SAFETY: `deck` is borrowed from EngineContext for the duration
147        // of this agent.execute() call. The lane writes its
148        // `AudioPlaybackWriteback` slot through this borrow.
149        ctx.insert(Slot::new(&mut *context.deck));
150
151        if let Some(lane) = self.lanes.get(self.current_lane) {
152            if let Err(e) = lane.execute(&mut ctx) {
153                log::error!("Audio lane {} failed: {}", lane.strategy_name(), e);
154            }
155        }
156    }
157
158    fn report_status(&self) -> AgentStatus {
159        AgentStatus {
160            agent_id: self.id(),
161            health_score: 1.0,
162            current_strategy: self.current_strategy,
163            is_stalled: false,
164            message: format!("max_sources={}", self.max_sources_per_frame),
165        }
166    }
167
168    fn as_any(&self) -> &dyn std::any::Any {
169        self
170    }
171
172    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
173        self
174    }
175
176    fn execution_timing(&self) -> ExecutionTiming {
177        ExecutionTiming {
178            allowed_phases: vec![ExecutionPhase::TRANSFORM],
179            default_phase: ExecutionPhase::TRANSFORM,
180            priority: 0.5,
181            importance: AgentImportance::Important,
182            fixed_timestep: None,
183            dependencies: Vec::new(),
184        }
185    }
186}