docs/superpowers/plans/2026-05-29-mixed-audio.md
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Wrap raylib's AttachAudioMixedProcessor / DetachAudioMixedProcessor with a safe RAII guard MixedAudioProcessorCallback that lives alongside the per-stream sibling in callbacks.rs, sharing its 30-slot trampoline pool.
Architecture: Two existing files modified. The slot-pool helper file (stream_processor_with_user_data_wrapper.rs) gets pub(crate) visibility on the three slot primitives + two new mixed-bus helpers wrapping the FFI calls. callbacks.rs gets the new MixedAudioProcessorCallback struct (mirrors AudioStreamProcessorCallback's shape) + the attach_audio_mixed_processor free fn + two Tier-2 lifecycle tests via test_harness::with_headless.
Tech Stack: Rust 1.85 (edition 2024), reuses existing paste + seq_macro machinery (no new deps), software_renderer feature for Tier-2 tests, RaylibAudio::init_audio_device() for the audio thread in tests.
Spec reference: docs/superpowers/specs/2026-05-29-mixed-audio-design.md.
pub(crate) + update header commentFiles:
raylib/src/core/callbacks/stream_processor_with_user_data_wrapper.rsThe slot primitives (set_context / clear_context / get_callback) are currently file-private. The new mixed-bus helpers need to call them, so widen the visibility to pub(crate). Also update the file header comment to reflect the broader role (both per-stream and mixed-bus consumers).
Run: grep -nE '^(fn|pub fn) (set_context|clear_context|get_callback|attach_audio_stream|detach_audio_stream)' raylib/src/core/callbacks/stream_processor_with_user_data_wrapper.rs
Expected: matches showing set_context, clear_context, get_callback at file-private visibility (no pub) — currently inside the paste! { ... } macro expansion. The attach_audio_stream_processor_with_user_data and detach_audio_stream_processor_with_user_data are the existing pub fn consumers.
set_context to pub(crate)Inside the paste! { ... } block, change the set_context declaration from fn set_context(...) to pub(crate) fn set_context(...). Keep all other code unchanged.
clear_context to pub(crate)Same: change fn clear_context(...) to pub(crate) fn clear_context(...).
get_callback to pub(crate)Same: change fn get_callback(...) to pub(crate) fn get_callback(...).
Replace the existing // region: -- ... comments at the top of the file (the section before the RawAudioCallbackWithUserData type alias) with this module-level doc comment:
//! Shared trampoline-slot pool for closure-driven audio callbacks.
//!
//! raylib's C-side audio processors (`AttachAudioStreamProcessor`,
//! `AttachAudioMixedProcessor`) accept only a function pointer — no
//! user-data parameter. To thread closure state through, we
//! pre-register **30 trampolines** (named `callback_0` through
//! `callback_29`), each with its own
//! `LazyLock<Mutex<AudioCallbackWithUserData>>` slot. A consumer
//! reserves a free slot via [`set_context`], the trampoline for that
//! slot looks up the closure context, and [`clear_context`] frees the
//! slot.
//!
//! Consumers:
//! - Per-stream: [`attach_audio_stream_processor_with_user_data`] /
//! [`detach_audio_stream_processor_with_user_data`].
//! - Mixed bus: [`attach_audio_mixed_processor_with_user_data`] /
//! [`detach_audio_mixed_processor_with_user_data`].
//!
//! Both consumers share the same 30-slot pool. The file is named
//! `stream_processor_with_user_data_wrapper.rs` for historical
//! reasons; it now covers both kinds of audio processors.
Insert this immediately after any use statements at the top of the file (the doc comment must be at module level, not preceded by other items).
Run: cargo build -p raylib --features full 2>&1 | tail -5
Expected: Finished line. The pub(crate) bumps add to surface but don't break anything; existing pub fn consumers still call the now-pub(crate) primitives identically.
Run via Bash tool:
git add raylib/src/core/callbacks/stream_processor_with_user_data_wrapper.rs
git commit -m "$(cat <<'EOF'
refactor(audio-callbacks): bump slot primitives to pub(crate) + module-level doc
Widens visibility on `set_context` / `clear_context` / `get_callback`
inside the slot-pool macro expansion so the upcoming mixed-bus
consumer can call them. Also replaces the legacy region comments
with a proper module-level doc comment explaining the shared
30-slot pool serves both per-stream and mixed-bus audio processors.
No public API change; no functional change.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
attach_audio_mixed_processor_with_user_data + detach helpersFiles:
raylib/src/core/callbacks/stream_processor_with_user_data_wrapper.rsAppend the two new pub(crate) helpers after the existing per-stream attach/detach functions. They share the slot pool but call the mixed-bus FFI functions instead of per-stream ones.
Run: grep -n 'pub fn detach_audio_stream_processor_with_user_data' raylib/src/core/callbacks/stream_processor_with_user_data_wrapper.rs
Expected: one match (the existing per-stream detach helper). Insert the new helpers immediately after its closing }.
After the closing } of detach_audio_stream_processor_with_user_data, append:
/// Attach a closure-driven processor to raylib's global mixed audio
/// bus. Reserves a slot from the shared 30-slot pool and calls the
/// C-side `AttachAudioMixedProcessor` with that slot's trampoline.
///
/// Returns the slot index — pass it to
/// [`detach_audio_mixed_processor_with_user_data`] when the consumer
/// drops.
pub(crate) fn attach_audio_mixed_processor_with_user_data(
callback: AudioCallbackWithUserData,
) -> usize {
let idx = set_context(callback);
// SAFETY: get_callback(idx) returns the trampoline matching the
// slot we just reserved; raylib copies the fn pointer into its
// linked-list entry. We hold the slot until drop calls detach +
// clear_context.
unsafe {
raylib_sys::AttachAudioMixedProcessor(Some(get_callback(idx)));
}
idx
}
/// Detach the closure-driven mixed-bus processor and clear the slot.
///
/// Calls the C-side `DetachAudioMixedProcessor` with the same
/// trampoline pointer registered for `index`, so raylib's internal
/// list-search matches and the entry is removed. The slot is only
/// cleared AFTER the C side has stopped iterating it — without this
/// the closure could still receive one more invocation against freed
/// state (same lifecycle ordering as the per-stream sibling fixed in
/// WS8e).
pub(crate) fn detach_audio_mixed_processor_with_user_data(index: usize) {
let trampoline = get_callback(index);
// SAFETY: trampoline is the same fn pointer raylib stored at
// attach time; the C-side list-search matches and removes the
// entry. clear_context runs AFTER detach completes so raylib has
// stopped iterating before the closure context disappears.
unsafe {
raylib_sys::DetachAudioMixedProcessor(Some(trampoline));
}
clear_context(index);
}
Run: cargo build -p raylib --features full 2>&1 | tail -5
Expected: Finished line, no errors.
If raylib_sys::AttachAudioMixedProcessor / DetachAudioMixedProcessor aren't resolvable, check the FFI binding name (grep -n 'AttachAudioMixedProcessor' raylib-sys/src/lib.rs or the bindgen output). They should be there — bindgen translates RLAPI void AttachAudioMixedProcessor(AudioCallback) directly.
Run: cargo clippy -p raylib --features full -- -D warnings 2>&1 | tail -5
Expected: no warnings.
git add raylib/src/core/callbacks/stream_processor_with_user_data_wrapper.rs
git commit -m "$(cat <<'EOF'
feat(audio-callbacks): add mixed-bus attach/detach helpers (pub(crate))
Two new `pub(crate)` helpers in the slot-pool file that mirror the
per-stream attach/detach pattern but invoke
AttachAudioMixedProcessor / DetachAudioMixedProcessor instead of
the per-stream FFI calls.
Lifecycle correctness mirrors the WS8e per-stream fix: the detach
helper calls DetachAudioMixedProcessor BEFORE clear_context, so
raylib's internal list-walk has stopped iterating the slot before
the closure context disappears.
No public API yet — the `pub` user-facing entry point lands in
the next task.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
MixedAudioProcessorCallback struct + impl + DropFiles:
raylib/src/core/callbacks.rsAppend the new struct after the closing // endregion: -- AudioStreamProcessorCallback -- comment. The shape mirrors AudioStreamProcessorCallback but without the per-stream stream field.
Run: grep -n 'endregion: -- AudioStreamProcessorCallback' raylib/src/core/callbacks.rs
Expected: one match. Insert the new code immediately after that closing comment line, before the pub fn attach_audio_stream_processor_to_music definition.
Run: head -25 raylib/src/core/callbacks.rs
Expected: the file already imports std::pin::Pin, std::os::raw::*, crate::core::audio::Music, raylib_sys. We need crate::core::audio::RaylibAudio (a sibling). Check the existing import line for audio:::
Run: grep -n 'use crate::core::audio' raylib/src/core/callbacks.rs
Expected: an existing line importing Music. Extend it to also import RaylibAudio (or add a new use line if the existing pattern is use ...::Music; only).
RaylibAudio importIf the existing import is use crate::core::audio::Music;, replace it with:
use crate::core::audio::{Music, RaylibAudio};
If it's a multi-line use crate::core::audio::{ ... }, add RaylibAudio to the brace list alphabetically.
If there's no existing import (Music is imported some other way), add a fresh line:
use crate::core::audio::RaylibAudio;
After the // endregion: -- AudioStreamProcessorCallback -- line, append:
// region: -- MixedAudioProcessorCallback --
/// Closure-driven processor attached to raylib's **global mixed audio
/// bus**.
///
/// The processor receives every stereo frame after raylib has mixed
/// all playing streams. Multiple `MixedAudioProcessorCallback`
/// instances can be attached simultaneously; they form a chain in
/// raylib's internal linked list and run in attach-order on each
/// frame.
///
/// This is a guard type — drop it to detach. Users obtain one via
/// [`attach_audio_mixed_processor`]; direct construction is not
/// supported (no public constructor).
pub struct MixedAudioProcessorCallback<'a, F>
where
F: FnMut(&mut [f32], u32) + Send + 'static,
{
rust_callback: &'a mut F,
/// Set after `attach_audio_mixed_processor_with_user_data` succeeds.
/// `None` only during the construction window before attach.
callback_index: Option<usize>,
}
impl<'a, F> MixedAudioProcessorCallback<'a, F>
where
F: FnMut(&mut [f32], u32) + Send + 'static,
{
fn new(closure: &'a mut F) -> Self {
Self {
rust_callback: closure,
callback_index: None,
}
}
fn get_as_user_data(&mut self) -> *mut ::std::os::raw::c_void {
self as *mut Self as *mut ::std::os::raw::c_void
}
fn get_c_callback(
&mut self,
) -> extern "C" fn(
*mut ::std::os::raw::c_void,
*mut ::std::os::raw::c_void,
::std::os::raw::c_uint,
) -> () {
Self::c_callback
}
extern "C" fn c_callback(
user_data: *mut ::std::os::raw::c_void,
data_ptr: *mut ::std::os::raw::c_void,
frame_count: ::std::os::raw::c_uint,
) {
// SAFETY: user_data is `*mut Self` per get_as_user_data; the
// wrapping Pin<Box<Self>> keeps it stable for the guard's
// lifetime, which outlives every callback firing per the
// &'a RaylibAudio borrow. data_ptr is `frame_count * 2 *
// sizeof(f32)` interleaved stereo bytes (mixed bus is always
// stereo per raylib.h:1736).
unsafe {
let cb: &mut Self = user_data.cast::<Self>().as_mut().unwrap();
let data = std::slice::from_raw_parts_mut(
data_ptr as *mut f32,
frame_count as usize * 2,
);
(cb.rust_callback)(data, 2);
}
}
}
impl<F> Drop for MixedAudioProcessorCallback<'_, F>
where
F: FnMut(&mut [f32], u32) + Send + 'static,
{
fn drop(&mut self) {
if let Some(idx) = self.callback_index {
detach_audio_mixed_processor_with_user_data(idx);
}
}
}
// endregion: -- MixedAudioProcessorCallback --
detach_audio_mixed_processor_with_user_data import if missingcallbacks.rs already imports the per-stream helpers from stream_processor_with_user_data_wrapper. Find that import:
Run: grep -nE 'use.*stream_processor_with_user_data_wrapper' raylib/src/core/callbacks.rs
Expected: one line bringing in the per-stream helpers (likely attach_audio_stream_processor_with_user_data + detach_audio_stream_processor_with_user_data + AudioCallbackWithUserData).
Extend the brace list to also include attach_audio_mixed_processor_with_user_data and detach_audio_mixed_processor_with_user_data. The result should be e.g.:
use super::callbacks::stream_processor_with_user_data_wrapper::{
AudioCallbackWithUserData,
attach_audio_mixed_processor_with_user_data,
attach_audio_stream_processor_with_user_data,
detach_audio_mixed_processor_with_user_data,
detach_audio_stream_processor_with_user_data,
};
(Adapt the path to whatever the existing import uses — it might be mod declared in a nearby file rather than via super::.)
Run: cargo build -p raylib --features full 2>&1 | tail -10
Expected: Finished line, no errors. If the trampoline c_callback complains about not being callable from extern "C" context, double-check the function signature matches the per-stream pattern verbatim — they should be identical.
Run: cargo clippy -p raylib --features full -- -D warnings 2>&1 | tail -5
Expected: no warnings.
git add raylib/src/core/callbacks.rs
git commit -m "$(cat <<'EOF'
feat(audio-callbacks): add MixedAudioProcessorCallback struct + impl + Drop
Mirrors AudioStreamProcessorCallback's shape but for raylib's global
mixed audio bus. Differences from the per-stream sibling:
- No `stream` field (mixed bus is global, not per-stream).
- Drop calls detach_audio_mixed_processor_with_user_data with just
the slot index (no stream argument needed by the FFI).
- c_callback hardcodes `nb_channels = 2` (mixed bus is always
stereo per raylib.h:1736).
The struct uses the existing 30-slot trampoline pool, so a
MixedAudioProcessorCallback + a per-stream
AudioStreamProcessorCallback compete for the same 30 slots.
No public attach fn yet — that lands in the next task.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
attach_audio_mixed_processor free functionFiles:
raylib/src/core/callbacks.rsThe user-facing entry point. Mirrors attach_audio_stream_processor_to_music's shape; borrows &'a RaylibAudio so the audio device cannot drop while a processor is attached.
Run: grep -n 'pub fn attach_audio_stream_processor_to_music' raylib/src/core/callbacks.rs
Expected: one match (the existing per-stream public attach fn). Find its closing } and insert the new fn immediately after.
After the closing } of attach_audio_stream_processor_to_music, append:
/// Attach a closure to raylib's **global mixed audio bus**, returning
/// a pinned guard that detaches on drop.
///
/// The callback fires from raylib's internal audio thread, receiving
/// **interleaved stereo** frames (`channels == 2` always — raylib
/// mixes all playing streams down to stereo before invoking the
/// processor). Multiple `MixedAudioProcessorCallback` instances can
/// be attached simultaneously and run in attach-order.
///
/// # Closure constraints
///
/// `F: FnMut(&mut [f32], u32) + Send + 'static`:
/// - `Send + 'static` because the callback runs on raylib's audio
/// thread.
/// - The closure receives a mutable slice of `frame_count * 2`
/// interleaved stereo samples plus the channel count (always `2`).
/// Modifying the slice in-place applies the effect to the mixed
/// bus.
///
/// # Slot pool exhaustion
///
/// The crate supports up to **30 simultaneous** audio-processor
/// closures (shared pool across per-stream and mixed-bus consumers).
/// Attaching a 31st processor panics with `"index out of bounds"`.
/// Detach existing processors (drop their guards) to free slots.
///
/// # Thread safety
///
/// raylib's `AttachAudioMixedProcessor` /
/// `DetachAudioMixedProcessor` are internally mutex-guarded; attach
/// and drop are safe to call from any thread.
pub fn attach_audio_mixed_processor<'a, F>(
_audio: &'a RaylibAudio,
processor: &'a mut F,
) -> Pin<Box<MixedAudioProcessorCallback<'a, F>>>
where
F: FnMut(&mut [f32], u32) + Send + 'static, // static because the function is executed in another thread
{
let mut cb = Box::new(MixedAudioProcessorCallback::<'a, F>::new(processor));
let idx = attach_audio_mixed_processor_with_user_data(AudioCallbackWithUserData::new(
cb.get_as_user_data(),
cb.get_c_callback(),
));
cb.callback_index = Some(idx);
Box::into_pin(cb)
}
Run: cargo build -p raylib --features full 2>&1 | tail -10
Expected: Finished line, no errors.
Run: cargo clippy -p raylib --features full -- -D warnings 2>&1 | tail -5
Expected: no warnings.
Run: RUSTDOCFLAGS="-Dwarnings" cargo doc -p raylib --features full --no-deps 2>&1 | tail -10
Expected: clean build. Intradoc-links to MixedAudioProcessorCallback and RaylibAudio resolve.
git add raylib/src/core/callbacks.rs
git commit -m "$(cat <<'EOF'
feat(audio-callbacks): public attach_audio_mixed_processor free fn
User-facing entry point. Mirrors attach_audio_stream_processor_to_music
but takes `&'a RaylibAudio` instead of `&'a Music` — the mixed bus
is global to the audio device, not tied to a single playing stream.
Returns Pin<Box<MixedAudioProcessorCallback>> as a guard. Drop calls
DetachAudioMixedProcessor + clear_context (same lifecycle ordering
as the WS8e per-stream fix).
Rustdoc covers the closure constraints (Send + 'static; stereo-only
buffer), the 30-slot shared-pool exhaustion ceiling, and the C-side
thread-safety story (mutex-guarded attach/detach).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
Files:
raylib/src/core/callbacks.rs (append a #[cfg(test)] mod mixed_audio_tests block)Two Tier-2 tests gated on feature = "software_renderer". Both open a 1×1 software-renderer window via test_harness::with_headless, init the audio device, attach + drop processors, and verify no panic.
Run: tail -20 raylib/src/core/callbacks.rs
Expected: the file ends after the last public fn (likely attach_audio_mixed_processor from Task 4). Append the test module at the very bottom of the file.
At the end of raylib/src/core/callbacks.rs, append:
#[cfg(test)]
#[cfg(feature = "software_renderer")]
mod mixed_audio_tests {
use super::*;
use crate::core::audio::RaylibAudio;
/// Attaching a mixed-bus processor and immediately dropping the
/// guard must not panic. Proves the slot pool + Drop's
/// DetachAudioMixedProcessor + clear_context all work together.
#[test]
fn attach_then_drop_does_not_panic() {
crate::test_harness::with_headless(1, 1, |_rl, _thread| {
let audio = RaylibAudio::init_audio_device().expect("audio init");
let mut closure = |_samples: &mut [f32], _ch: u32| {};
{
let _guard = attach_audio_mixed_processor(&audio, &mut closure);
// Guard dropped at end of scope — detach + slot clear.
}
// If we got here without panic, attach + drop both worked.
});
}
/// Multiple simultaneous processors plus out-of-order drops must
/// not corrupt the slot pool or raylib's linked list.
#[test]
fn multiple_processors_attach_and_drop_independently() {
crate::test_harness::with_headless(1, 1, |_rl, _thread| {
let audio = RaylibAudio::init_audio_device().expect("audio init");
let mut closure_a = |_s: &mut [f32], _c: u32| {};
let mut closure_b = |_s: &mut [f32], _c: u32| {};
let guard_a = attach_audio_mixed_processor(&audio, &mut closure_a);
let guard_b = attach_audio_mixed_processor(&audio, &mut closure_b);
// Drop in reverse order; both should clean up.
drop(guard_b);
drop(guard_a);
});
}
}
The Tier-2 software_renderer feature is mutually exclusive with the OpenGL backends, so it doesn't run under --features full. Use the same invocation the WS4/WS5 CI uses:
Run:
cargo test -p raylib --lib --no-default-features --features software_renderer,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RAUDIO,SUPPORT_FILEFORMAT_PNG mixed_audio_tests -- --test-threads=1 2>&1 | tail -10
Expected: test result: ok. 2 passed; 0 failed. The --test-threads=1 flag is required because raylib enforces single-init via a lock (raylib/src/core/mod.rs:415).
If RaylibAudio::init_audio_device() fails on the CI runner (no audio output device), the test fails with the .expect("audio init") panic. If this is a problem in practice, the test may need to be gated further (e.g., #[cfg_attr(target_os = "...", ignore)]). Discover during execution; only escalate if it actually fires.
Run: cargo fmt --check 2>&1 | tail -3
Expected: zero output, exit 0. If rustfmt wants any line-collapses, run cargo fmt and re-verify before committing.
git add raylib/src/core/callbacks.rs
git commit -m "$(cat <<'EOF'
test(mixed-audio): Tier-2 lifecycle tests for attach + drop
Two #[test] fns in a new mixed_audio_tests module at the bottom of
callbacks.rs, gated on feature = "software_renderer":
- attach_then_drop_does_not_panic: single attach + scope-exit drop
through with_headless + init_audio_device. Proves slot pool +
Drop's DetachAudioMixedProcessor + clear_context all work together.
- multiple_processors_attach_and_drop_independently: two
simultaneous attaches + out-of-order drops. Proves the slot pool
+ raylib's linked list survive multi-consumer churn.
Tests require `-- --test-threads=1` per raylib's single-init lock —
matches WS4/WS5 conventions in test.yml.
Firing-correctness ("did the closure actually get invoked?") is
deferred to a future window-opening integration suite per spec §11.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
Files: none (verification only)
Run: cargo test -p raylib --lib --features full 2>&1 | tail -10
Expected: every existing test still passes. Mixed-audio's Tier-2 tests are gated on software_renderer, so they don't show up here — expect the same count as before this workstream started (72 from the hashes workstream).
Run:
cargo test -p raylib --lib --no-default-features --features software_renderer,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RAUDIO,SUPPORT_FILEFORMAT_PNG -- --test-threads=1 2>&1 | tail -10
Expected: Tier-2 test count goes up by 2 (attach_then_drop_does_not_panic + multiple_processors_attach_and_drop_independently).
Run: cargo clippy -p raylib --features full -- -D warnings 2>&1 | tail -5
Expected: no warnings.
Also verify under software_renderer feature:
Run: cargo clippy -p raylib --no-default-features --features software_renderer,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RAUDIO,SUPPORT_FILEFORMAT_PNG --tests -- -D warnings 2>&1 | tail -5
Expected: no warnings on the test code either.
Run: cargo fmt --check 2>&1 | tail -3
Expected: zero output, exit 0.
Run: RUSTDOCFLAGS="-Dwarnings" cargo doc -p raylib --features full --no-deps 2>&1 | tail -10
Expected: clean build.
Run: mdbook build book 2>&1 | tail -5
Expected: clean (no errors). The mixed-audio module isn't referenced from any book chapter; this just confirms nothing leaked.
Files:
docs/superpowers/notes/cheatsheet-parity-audit.mdCHANGELOG.mdThe audit doc's §1 and §4 reference AttachAudioMixedProcessor and DetachAudioMixedProcessor as 🟥 GAP entries belonging to the mixed-audio workstream. Flip them to ✅ pointing at the new code paths; decrement §0 counts; remove the workstream entry from §4.
Run: grep -nE 'AttachAudioMixedProcessor|DetachAudioMixedProcessor' docs/superpowers/notes/cheatsheet-parity-audit.md | head -10
Expected: 🟥 GAP entries in §1 (currently pointing at the mixed-audio workstream), plus references in §4 (the workstream entry itself) and possibly the §0 summary or §4 "Done" subsection.
For each Attach/DetachAudioMixedProcessor line in §1, change the marker from 🟥 to ✅ and the action from "GAP. Action: future-workstream-mixed-audio." to a path reference. The pattern matches the just-shipped pixel-pointers + hashes ✅ rows:
- ✅ `AttachAudioMixedProcessor` — wrapped at `raylib/src/core/callbacks.rs::attach_audio_mixed_processor`.
- ✅ `DetachAudioMixedProcessor` — wrapped via `MixedAudioProcessorCallback::Drop` in `raylib/src/core/callbacks.rs`.
The exact original wording may differ; adapt to whatever ✅-style is used elsewhere in §1.
§0 currently shows (post-hashes): "Covered: 492 / Gaps: 7 / Medium workstreams: 2 (mixed-audio + Done section)". Decrement:
Adjust the "X, not 49" headline accordingly: 7 − 2 = 5.
mixed-audio workstream from §4The §4 entry for mixed-audio is done. Match whichever pattern the previous workstreams used (per pixel-pointers/hashes precedent: either remove + renumber OR move to the "Done (this audit)" subsection with a brief commit-range note). Default to "move to Done" since that's what hashes did.
If the §4 entry has a subsection like "Done (this audit)", append a new bullet under it:
- **mixed-audio** ✅ — `MixedAudioProcessorCallback` + `attach_audio_mixed_processor` at `raylib/src/core/callbacks.rs`. RAII guard borrows `&'a RaylibAudio`; Drop detaches before clearing the slot. Tier-2 lifecycle tests under `software_renderer`.
And remove the active mixed-audio workstream entry above.
In CHANGELOG.md, under ## 6.0.0-rc.1 (unreleased) → ### Added, append:
- **Mixed audio bus:** new public API for closure-driven processors on
raylib's global mixed audio bus:
- `attach_audio_mixed_processor(audio: &RaylibAudio, processor: &mut F) -> Pin<Box<MixedAudioProcessorCallback<'_, F>>>`
where `F: FnMut(&mut [f32], u32) + Send + 'static`.
- `MixedAudioProcessorCallback` — the RAII guard type. Dropping it
calls `DetachAudioMixedProcessor` and frees the closure slot,
mirroring the WS8e per-stream-processor soundness fix.
- Multiple mixed-bus processors can be attached simultaneously
(they chain in raylib's internal linked list). They share the
same 30-slot trampoline pool with the per-stream processors.
- Re-exported via `raylib::prelude`. Closes the `mixed-audio`
workstream from the cheatsheet-parity audit.
Also updates the "post-WS8 workstreams" note at the top of the
6.0.0-rc.1 block: mixed-audio drops out of the list.
Then update the lead-paragraph note at the top of the ## 6.0.0-rc.1 (unreleased) block:
Find the line that lists (pixel-pointers, hashes, mixed-audio, WS9 showcase) and remove mixed-audio from it. Result:
> Release candidate 1 for the 6.0 line — the published crate versions are
> `6.0.0-rc.1` while the post-WS8 workstreams (WS9 showcase ...) settle.
Or whatever phrasing keeps the remaining list grammatical (the three earlier workstreams are now all done; the lead note can just point at the remaining WS9 + final-release work).
Run: grep -nE 'AttachAudioMixedProcessor|DetachAudioMixedProcessor' docs/superpowers/notes/cheatsheet-parity-audit.md
Expected: every match is ✅ (in §1 or §4 Done subsection); no 🟥 rows.
Run: grep -n 'attach_audio_mixed_processor\|MixedAudioProcessorCallback' CHANGELOG.md | head -5
Expected: the new bullet shows in the 6.0.0-rc.1 block.
git add docs/superpowers/notes/cheatsheet-parity-audit.md CHANGELOG.md
git commit -m "$(cat <<'EOF'
docs(mixed-audio): mark AttachAudioMixedProcessor / DetachAudioMixedProcessor as covered
The mixed-audio workstream from cheatsheet-parity-audit.md §4 is
done; reconcile the audit + CHANGELOG accordingly.
- cheatsheet-parity-audit.md:
- §1 entries for AttachAudioMixedProcessor + DetachAudioMixedProcessor
flip 🟥 -> ✅ pointing at raylib/src/core/callbacks.rs (the attach
fn + MixedAudioProcessorCallback's Drop respectively).
- §0 totals: Covered 492 -> 494, gaps 7 -> 5, medium workstreams 2 -> 1.
- §4 mixed-audio workstream moved to the "Done (this audit)"
subsection.
- CHANGELOG.md ## 6.0.0-rc.1 (unreleased) ### Added: lists the new
attach_audio_mixed_processor + MixedAudioProcessorCallback with a
reference to the WS8e per-stream-processor soundness pattern.
Lead-paragraph note drops mixed-audio from the post-WS8 workstreams
list (now empty for this group; only WS9 + final-release remain).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
Files:
Modify: CLAUDE.md
Step 1: Confirm clean working tree
Run: git status --short
Expected: only the three permitted untracked files (TODO.md, prompt.md, next-session-prompt.md); no M lines.
Find the status line:
Run: grep -n 'mixed-audio ← NEXT\|hashes ✅' CLAUDE.md
Expected: the current line shows pixel-pointers ✅ → hashes ✅ → mixed-audio ← NEXT → raylib-test → .... Replace with:
pixel-pointers ✅ → hashes ✅ → mixed-audio ✅ → raylib-test ← NEXT → ...
(Preserve the rest of the chain — UBSAN → rustdoc rewrite → safe abstractions → WS9 showcase → final-release.)
git add CLAUDE.md
git commit -m "$(cat <<'EOF'
docs(roadmap): mixed-audio complete; raylib-test is next
CLAUDE.md status line: pre-WS9 queue head flips from mixed-audio to
raylib-test. See docs/superpowers/notes/cheatsheet-parity-audit.md
for the updated cheatsheet coverage (494 / 500 in-scope; 5 gaps
remain in the rtext/rfilesystem out-of-scope sections).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
git push fork 6.0-rc 2>&1 | tail -3
git push fork 6.0-rc:unstable 2>&1 | tail -3
Expected: both pushes succeed with the same SHA range.
raylib/src/core/callbacks/stream_processor_with_user_data_wrapper.rs has pub(crate) on the three slot primitives + the two new pub(crate) mixed-bus helpers + the updated module-level doc comment.raylib/src/core/callbacks.rs has the MixedAudioProcessorCallback struct + impl + Drop + attach_audio_mixed_processor free fn + the two Tier-2 tests in mod mixed_audio_tests.RaylibAudio is imported in callbacks.rs.pub(crate) helpers are imported from the slot-pool file into callbacks.rs.DetachAudioMixedProcessor BEFORE clear_context (lifecycle correctness).cargo test -p raylib --lib --features full.--features software_renderer,... with -- --test-threads=1.cargo build --workspace --features full clean.cargo clippy --workspace --features full -- -D warnings clean.cargo fmt --check clean.RUSTDOCFLAGS="-Dwarnings" cargo doc -p raylib --features full --no-deps clean.mdbook build book clean.cheatsheet-parity-audit.md reconciled: §0 totals (494 covered / 5 gaps), §1 ✅ rows, §4 mixed-audio moved to "Done (this audit)".CHANGELOG.md ## 6.0.0-rc.1 (unreleased) ### Added lists the new APIs + lead-paragraph note updated.CLAUDE.md status line: pre-WS9 queue head flips mixed-audio ✅ → raylib-test ← NEXT.Co-Authored-By: Claude Opus 4.7 <[email protected]> trailer.fork/6.0-rc and fork/unstable.Next workstream: raylib-test delete-or-fix (promoted from long-tail by owner directive 2026-05-29). Decide whether to fix the window-opening integration tests under xvfb or delete the crate entirely. Brainstorm starts when the owner is ready.