feat(audio): multitrack stem recording — Stage 2 (wire into the mixer)
Wires MultitrackRecorder into the live audio path, behind a recording_mode.
- config: RecordingMode { Mixed, Multitrack, Both } + AppConfig.recording_mode
(serde-default Mixed, back-compat); CoreCommand::SetRecordingMode, sent at
app startup from config.
- multitrack.rs: mic now arrives async via push_mic into an internal FIFO,
drained one frame per end_cycle (mirrors recorder.rs) so the mic track tracks
the cycle clock; added dir() accessor. mic is a plain WavWriter now.
- core: parallel `multitrack` slot + `is_multitrack` fast-path gate (exactly one
of the mixed/multitrack recorders is active). SetRecording start branches on
mode: Mixed → single-file Recorder (unchanged); Multitrack/Both → a per-session
dir, MultitrackRecorder, and registers everyone already in the room (named,
silence-aligned from t=0). The mixer taps each peer's RAW frame (pre-volume/
mute/limiter) into stems and writes peer stems + mix (Both) + end_cycle per
cycle; the capture thread pushes mic to whichever recorder; PeerJoined adds a
late joiner's stem track. stop_recording finalizes both.
No UI yet to pick the mode (Stage 3) — defaults to Mixed, so behaviour is
unchanged until then; set recording_mode in config.json to exercise stems.
168 lib tests, clippy --all-targets clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,41 @@ impl RoomLayout {
|
||||
[RoomLayout::ThreeColumn, RoomLayout::BottomDock, RoomLayout::Drawer];
|
||||
}
|
||||
|
||||
/// What a call recording captures. `Mixed` is the original single-file behaviour;
|
||||
/// `Multitrack`/`Both` write per-peer stems for post-production (see
|
||||
/// `docs/multitrack-recording-plan.md`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum RecordingMode {
|
||||
/// One mixed WAV (your mic + the incoming mix). Smallest; the default.
|
||||
#[default]
|
||||
Mixed,
|
||||
/// One WAV per peer + your mic, all sample-aligned. Rebuild the mix yourself.
|
||||
Multitrack,
|
||||
/// Per-peer stems + your mic + a convenience mixed track.
|
||||
Both,
|
||||
}
|
||||
|
||||
impl RecordingMode {
|
||||
/// All variants, in picker display order.
|
||||
pub const ALL: [RecordingMode; 3] =
|
||||
[RecordingMode::Mixed, RecordingMode::Multitrack, RecordingMode::Both];
|
||||
|
||||
/// True when this mode writes per-peer stem tracks (Multitrack or Both).
|
||||
pub fn is_multitrack(self) -> bool {
|
||||
matches!(self, RecordingMode::Multitrack | RecordingMode::Both)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RecordingMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
RecordingMode::Mixed => "Mixed (single file)",
|
||||
RecordingMode::Multitrack => "Multitrack (per-peer stems)",
|
||||
RecordingMode::Both => "Both (stems + mixed)",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RoomLayout {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
@@ -141,6 +176,9 @@ pub struct AppConfig {
|
||||
/// Chosen UI colour theme.
|
||||
#[serde(default)]
|
||||
pub theme: AppTheme,
|
||||
/// What a call recording captures (mixed / per-peer stems / both).
|
||||
#[serde(default)]
|
||||
pub recording_mode: RecordingMode,
|
||||
#[serde(default)]
|
||||
pub custom_sound_self_join: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -195,6 +233,7 @@ impl Default for AppConfig {
|
||||
chat_drawer_width: default_chat_drawer_width(),
|
||||
room_layout: RoomLayout::default(),
|
||||
theme: AppTheme::default(),
|
||||
recording_mode: RecordingMode::default(),
|
||||
custom_sound_self_join: None,
|
||||
custom_sound_peer_join: None,
|
||||
custom_sound_peer_leave: None,
|
||||
|
||||
Reference in New Issue
Block a user