Files
upstream-affine/packages/frontend/native/media_capture/src/macos/audio_buffer.rs
upstream-mirror 702cc698a7
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
2026-09-16 04:01:20 +05:00

401 lines
12 KiB
Rust

use std::{cmp::Ordering, ffi::c_void};
use coreaudio::sys::kAudioHardwareBadStreamError;
use objc2::{Encode, Encoding, RefEncode};
use crate::{error::CoreAudioError, utils::process_audio_frame};
pub const AUDIO_MIX_OUTPUT_WEIGHT: f32 = 0.75;
/// Mix audio samples using scalar operations (no SIMD)
///
/// # Arguments
/// * `input_samples` - Samples from the input stream
/// * `output_samples` - Samples from the output stream
/// * `mixed_samples` - Buffer to store the result (must be pre-allocated)
/// * `start_index` - Starting index in the buffers
/// * `end_index` - Ending index in the buffers (exclusive)
pub fn mix_audio_samples_scalar(
input_samples: &[f32],
output_samples: &[f32],
mixed_samples: &mut [f32],
start_index: usize,
end_index: usize,
) {
// performance downgraded 4x if apply suggestion from this lint rule
#[allow(clippy::needless_range_loop)]
for sample_index in start_index..end_index {
let sample_in = input_samples.get(sample_index).unwrap_or(&0.0);
let sample_out = output_samples.get(sample_index).unwrap_or(&0.0);
mixed_samples[sample_index] = sample_in + sample_out * AUDIO_MIX_OUTPUT_WEIGHT;
}
}
/// Mix audio samples from input and output streams with specified weights
/// Uses NEON SIMD acceleration on supported platforms
///
/// # Arguments
/// * `input_samples` - Samples from the input stream
/// * `output_samples` - Samples from the output stream
///
/// # Returns
/// A vector of mixed audio samples
pub fn mix_audio_samples(input_samples: &[f32], output_samples: &[f32]) -> Vec<f32> {
let mixed_samples_length = input_samples.len();
let mut mixed_samples = vec![0.0; mixed_samples_length];
// For very small arrays, use scalar implementation
if mixed_samples_length < 16 {
mix_audio_samples_scalar(
input_samples,
output_samples,
&mut mixed_samples,
0,
mixed_samples_length,
);
return mixed_samples;
}
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
unsafe {
use std::arch::aarch64::{vdupq_n_f32, vld1q_f32, vmlaq_f32, vst1q_f32};
let output_weight_vec = vdupq_n_f32(AUDIO_MIX_OUTPUT_WEIGHT);
// Process the common length where both arrays have data
let common_length = input_samples.len();
// Main SIMD loop - process each block of 4 samples
let input_ptr = input_samples.as_ptr();
let output_ptr = output_samples.as_ptr();
let result_ptr = mixed_samples.as_mut_ptr();
let mut offset: usize = 0;
let mut remaining_offset: Option<usize> = None;
// Process 16 samples at a time (4 SIMD vectors)
while offset < common_length {
// Load 4 vectors of 4 floats each
let in_vec1 = vld1q_f32(input_ptr.add(offset));
let out_vec1 = vld1q_f32(output_ptr.add(offset));
let in_vec2 = vld1q_f32(input_ptr.add(offset + 4));
let out_vec2 = vld1q_f32(output_ptr.add(offset + 4));
let in_vec3 = vld1q_f32(input_ptr.add(offset + 8));
let out_vec3 = vld1q_f32(output_ptr.add(offset + 8));
let in_vec4 = vld1q_f32(input_ptr.add(offset + 12));
let out_vec4 = vld1q_f32(output_ptr.add(offset + 12));
// Using fused multiply-add: (a * b) + c in one operation
// First multiply input by weight
let result1 = vmlaq_f32(in_vec1, out_vec1, output_weight_vec);
let result2 = vmlaq_f32(in_vec2, out_vec2, output_weight_vec);
let result3 = vmlaq_f32(in_vec3, out_vec3, output_weight_vec);
let result4 = vmlaq_f32(in_vec4, out_vec4, output_weight_vec);
// Store results
vst1q_f32(result_ptr.add(offset), result1);
vst1q_f32(result_ptr.add(offset + 4), result2);
vst1q_f32(result_ptr.add(offset + 8), result3);
vst1q_f32(result_ptr.add(offset + 12), result4);
offset += 16;
// accept clippy lint suggestion would downgrade the performance by 15%
#[allow(clippy::comparison_chain)]
// fast path for aligned length
if offset == common_length {
break;
} else if offset > common_length {
remaining_offset = Some(offset - 16);
} else {
let remaining = common_length - offset;
if remaining < 16 {
remaining_offset = Some(offset);
break;
}
}
}
if let Some(remaining_offset) = remaining_offset {
mix_audio_samples_scalar(
input_samples,
output_samples,
&mut mixed_samples,
remaining_offset,
common_length,
);
}
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))]
{
// Fallback for non-ARM architectures
mix_audio_samples_scalar(
input_samples,
output_samples,
&mut mixed_samples,
0,
mixed_samples_length,
);
}
mixed_samples
}
/// [Apple's documentation](https://developer.apple.com/documentation/coreaudiotypes/audiobuffer?language=objc)
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[allow(non_snake_case)]
pub struct AudioBuffer {
pub mNumberChannels: u32,
pub mDataByteSize: u32,
pub mData: *mut c_void,
}
unsafe impl Encode for AudioBuffer {
const ENCODING: Encoding = Encoding::Struct(
"AudioBuffer",
&[<u32>::ENCODING, <u32>::ENCODING, <*mut c_void>::ENCODING],
);
}
unsafe impl RefEncode for AudioBuffer {
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[allow(non_snake_case)]
pub struct AudioBufferList {
pub mNumberBuffers: u32,
pub mBuffers: [AudioBuffer; 2],
}
unsafe impl Encode for AudioBufferList {
const ENCODING: Encoding = Encoding::Struct("AudioBufferList", &[<u32>::ENCODING, <[AudioBuffer; 1]>::ENCODING]);
}
unsafe impl RefEncode for AudioBufferList {
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
}
/// Represent the audio buffer contains input and output PCM buffers
#[repr(transparent)]
pub struct InputAndOutputAudioBufferList(pub AudioBufferList);
impl InputAndOutputAudioBufferList {
/// # Safety
///
/// The caller must ensure that the input data is a valid AudioBufferList
pub unsafe fn from_raw(in_input_data: *mut c_void) -> Result<Self, i32> {
let buffer_list: AudioBufferList = unsafe { *in_input_data.cast() };
if buffer_list.mNumberBuffers != 2 {
return Err(kAudioHardwareBadStreamError as i32);
}
Ok(Self(buffer_list))
}
pub fn mix_input_and_output(
&self,
target_sample_rate: f64,
input_sample_rate: f64,
output_sample_rate: f64,
) -> Result<Vec<f32>, CoreAudioError> {
let mut mixed_samples = Vec::new();
// Directly access buffers from the list
let [input_buffer, output_buffer] = self.0.mBuffers;
if let Some(processed_input) = process_audio_frame(
input_buffer.mData,
input_buffer.mDataByteSize,
input_buffer.mNumberChannels,
input_sample_rate,
target_sample_rate,
) {
mixed_samples = processed_input;
}
if let Some(processed_output) = process_audio_frame(
output_buffer.mData,
output_buffer.mDataByteSize,
output_buffer.mNumberChannels,
output_sample_rate,
target_sample_rate,
) {
if mixed_samples.is_empty() {
mixed_samples = processed_output;
} else {
let len1 = mixed_samples.len();
let len2 = processed_output.len();
match len1.cmp(&len2) {
Ordering::Less => {
mixed_samples.resize(len2, 0.0);
}
Ordering::Greater => {
let mut padded_output = processed_output;
padded_output.resize(len1, 0.0);
for (sample1, sample2) in mixed_samples.iter_mut().zip(padded_output.iter()) {
*sample1 = (*sample1 + *sample2) / 2.0;
}
return Ok(mixed_samples);
}
_ => {}
}
for (sample1, sample2) in mixed_samples.iter_mut().zip(processed_output.iter()) {
*sample1 = (*sample1 + *sample2) / 2.0;
}
}
}
Ok(mixed_samples)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mix_audio_samples_empty() {
let input: Vec<f32> = vec![];
let output: Vec<f32> = vec![];
let mixed = mix_audio_samples(&input, &output);
assert_eq!(mixed.len(), 0);
}
#[test]
fn test_mix_audio_samples_equal_length() {
let input = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let output = vec![0.5, 0.4, 0.3, 0.2, 0.1];
let mixed = mix_audio_samples(&input, &output);
assert_eq!(mixed.len(), 5);
// Verify calculations: (input + output * 0.75)
let expected = [
(0.1 + 0.5 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.2 + 0.4 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.3 + 0.3 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.4 + 0.2 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.5 + 0.1 * AUDIO_MIX_OUTPUT_WEIGHT),
];
for i in 0..mixed.len() {
assert!(
(mixed[i] - expected[i]).abs() < 1e-6,
"Mismatch at index {}: expected {}, got {}",
i,
expected[i],
mixed[i]
);
}
}
#[test]
fn test_mix_audio_samples_input_longer() {
let input = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let output = vec![0.5, 0.4, 0.3];
let mixed = mix_audio_samples(&input, &output);
assert_eq!(mixed.len(), 5);
// Verify calculations
let expected = [
(0.1 + 0.5 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.2 + 0.4 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.3 + 0.3 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.4 + 0.0 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.5 + 0.0 * AUDIO_MIX_OUTPUT_WEIGHT),
];
for i in 0..mixed.len() {
assert!(
(mixed[i] - expected[i]).abs() < 1e-6,
"Mismatch at index {}: expected {}, got {}",
i,
expected[i],
mixed[i]
);
}
}
#[test]
fn test_mix_audio_samples_custom_weights() {
// Note: We're using the constant weights so we can't really test custom
// values directly
let input = vec![0.1, 0.2, 0.3];
let output = vec![0.5, 0.4, 0.3];
let mixed = mix_audio_samples(&input, &output);
// Calculate expected values based on the constants
let expected = [
(0.1 + 0.5 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.2 + 0.4 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.3 + 0.3 * AUDIO_MIX_OUTPUT_WEIGHT),
];
for i in 0..mixed.len() {
assert!(
(mixed[i] - expected[i]).abs() < 1e-6,
"Mismatch at index {}: expected {}, got {}",
i,
expected[i],
mixed[i]
);
}
}
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
#[test]
fn test_simd_implementation_used() {
const BUFFER_SIZES: [usize; 4] = [100, 127, 128, 512];
for size in BUFFER_SIZES {
// Create arrays large enough to trigger SIMD path
let input: Vec<f32> = (0..size).map(|i| i as f32 * 0.01).collect();
let output: Vec<f32> = (0..size).map(|i| (size - i) as f32 * 0.01).collect();
// Mix with standard weights
let mixed = mix_audio_samples(&input, &output);
// Compute the same mix using scalar implementation for comparison
let mut expected = vec![0.0; input.len()];
mix_audio_samples_scalar(&input, &output, &mut expected, 0, input.len());
// Verify results match between SIMD and scalar implementations
for i in 0..mixed.len() {
assert!(
(mixed[i] - expected[i]).abs() < 1e-6,
"SIMD and scalar implementations should produce identical results at index {}",
i
);
}
}
}
#[test]
fn test_small_vector_uses_scalar() {
// Create small arrays that should use scalar path even with SIMD available
let input = vec![0.1, 0.2, 0.3];
let output = vec![0.5, 0.4, 0.3];
// Mix with standard weights
let mixed = mix_audio_samples(&input, &output);
// Calculate expected values manually
let expected = [
(0.1 + 0.5 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.2 + 0.4 * AUDIO_MIX_OUTPUT_WEIGHT),
(0.3 + 0.3 * AUDIO_MIX_OUTPUT_WEIGHT),
];
// Verify results
for i in 0..mixed.len() {
assert!(
(mixed[i] - expected[i]).abs() < 1e-6,
"Small vector mixing should be correct at index {}",
i
);
}
}
}