mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 13:29:02 +08:00
feat(native): record encoding (#14188)
fix #13784 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Start/stop system or meeting recordings with Ogg/Opus artifacts and native start/stop APIs; workspace backup recovery. * **Refactor** * Simplified recording lifecycle and UI flows; native runtime now orchestrates recording/processing and reporting. * **Bug Fixes** * Stronger path validation, safer import/export dialogs, consistent error handling/logging, and retry-safe recording processing. * **Chores** * Added cross-platform native audio capture and Ogg/Opus encoding support. * **Tests** * New unit, integration, and e2e tests for recording, path guards, dialogs, and workspace recovery. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -13,14 +13,12 @@ use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
};
|
||||
use crossbeam_channel::unbounded;
|
||||
use napi::{
|
||||
Error, Status,
|
||||
bindgen_prelude::{Float32Array, Result},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use napi::{Error, Status, bindgen_prelude::Result};
|
||||
use napi_derive::napi;
|
||||
use rubato::{FastFixedIn, PolynomialDegree, Resampler};
|
||||
|
||||
use crate::audio_callback::AudioCallback;
|
||||
|
||||
const RESAMPLER_INPUT_CHUNK: usize = 1024; // samples per channel
|
||||
const TARGET_FRAME_SIZE: usize = 1024; // frame size returned to JS (in mono samples)
|
||||
|
||||
@@ -216,7 +214,10 @@ impl Drop for AudioCaptureSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_recording(audio_buffer_callback: ThreadsafeFunction<Float32Array, ()>) -> Result<AudioCaptureSession> {
|
||||
pub fn start_recording(
|
||||
audio_buffer_callback: AudioCallback,
|
||||
target_sample_rate: Option<SampleRate>,
|
||||
) -> Result<AudioCaptureSession> {
|
||||
let available_hosts = cpal::available_hosts();
|
||||
let host_id = available_hosts
|
||||
.first()
|
||||
@@ -240,7 +241,7 @@ pub fn start_recording(audio_buffer_callback: ThreadsafeFunction<Float32Array, (
|
||||
|
||||
let mic_sample_rate = mic_config.sample_rate();
|
||||
let lb_sample_rate = lb_config.sample_rate();
|
||||
let target_rate = SampleRate(mic_sample_rate.min(lb_sample_rate).0);
|
||||
let target_rate = target_sample_rate.unwrap_or(SampleRate(mic_sample_rate.min(lb_sample_rate).0));
|
||||
|
||||
let mic_channels = mic_config.channels();
|
||||
let lb_channels = lb_config.channels();
|
||||
@@ -332,7 +333,7 @@ pub fn start_recording(audio_buffer_callback: ThreadsafeFunction<Float32Array, (
|
||||
let lb_chunk: Vec<f32> = post_lb.drain(..TARGET_FRAME_SIZE).collect();
|
||||
let mixed = mix(&mic_chunk, &lb_chunk);
|
||||
if !mixed.is_empty() {
|
||||
let _ = audio_buffer_callback.call(Ok(mixed.clone().into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
audio_buffer_callback.call(mixed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use cpal::SampleRate;
|
||||
use napi::{
|
||||
bindgen_prelude::{Buffer, Error, Result, Status},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
@@ -27,6 +28,7 @@ use windows::Win32::System::{
|
||||
};
|
||||
|
||||
// Import the function from microphone_listener
|
||||
use crate::audio_callback::AudioCallback;
|
||||
use crate::windows::microphone_listener::is_process_actively_using_microphone;
|
||||
|
||||
// Type alias to match macOS API
|
||||
@@ -214,6 +216,15 @@ impl ShareableContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tap_audio_with_callback(
|
||||
_process_id: u32,
|
||||
audio_stream_callback: AudioCallback,
|
||||
target_sample_rate: Option<u32>,
|
||||
) -> Result<AudioCaptureSession> {
|
||||
let target = target_sample_rate.map(SampleRate);
|
||||
crate::windows::audio_capture::start_recording(audio_stream_callback, target)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn tap_audio(
|
||||
_process_id: u32, // Currently unused - Windows captures global audio
|
||||
@@ -221,7 +232,18 @@ impl ShareableContent {
|
||||
) -> Result<AudioCaptureSession> {
|
||||
// On Windows with CPAL, we capture global audio (mic + loopback)
|
||||
// since per-application audio tapping isn't supported the same way as macOS
|
||||
crate::windows::audio_capture::start_recording(audio_stream_callback)
|
||||
ShareableContent::tap_audio_with_callback(_process_id, AudioCallback::Js(Arc::new(audio_stream_callback)), None)
|
||||
}
|
||||
|
||||
pub(crate) fn tap_global_audio_with_callback(
|
||||
_excluded_processes: Option<Vec<&ApplicationInfo>>,
|
||||
audio_stream_callback: AudioCallback,
|
||||
target_sample_rate: Option<u32>,
|
||||
) -> Result<AudioCaptureSession> {
|
||||
let target = target_sample_rate.map(SampleRate);
|
||||
// Delegate to audio_capture::start_recording which handles mixing mic +
|
||||
// loopback
|
||||
crate::windows::audio_capture::start_recording(audio_stream_callback, target)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -229,9 +251,11 @@ impl ShareableContent {
|
||||
_excluded_processes: Option<Vec<&ApplicationInfo>>,
|
||||
audio_stream_callback: ThreadsafeFunction<napi::bindgen_prelude::Float32Array, ()>,
|
||||
) -> Result<AudioCaptureSession> {
|
||||
// Delegate to audio_capture::start_recording which handles mixing mic +
|
||||
// loopback
|
||||
crate::windows::audio_capture::start_recording(audio_stream_callback)
|
||||
ShareableContent::tap_global_audio_with_callback(
|
||||
_excluded_processes,
|
||||
AudioCallback::Js(Arc::new(audio_stream_callback)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
Reference in New Issue
Block a user