2025-11-01 21:10:57 +01:00
|
|
|
use crate::{
|
2025-11-03 06:55:45 +01:00
|
|
|
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
2025-11-04 20:34:44 +01:00
|
|
|
pipeline::{AudioPipelineNode, Node, NodeLogic},
|
2025-11-01 21:10:57 +01:00
|
|
|
type_constraints::TypeRequirement,
|
|
|
|
|
AudioChunk, AudioChunkData, AudioSegment, I24,
|
|
|
|
|
};
|
|
|
|
|
use pmoflac::{decode_audio_stream, AudioFileMetadata, StreamInfo};
|
|
|
|
|
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
|
|
|
|
|
use std::{path::PathBuf, sync::Arc, time::Duration};
|
|
|
|
|
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
|
2025-11-03 06:55:45 +01:00
|
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
|
use tracing;
|
2025-11-01 21:10:57 +01:00
|
|
|
|
2025-11-04 20:34:44 +01:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// NOUVELLE ARCHITECTURE - FileSourceLogic
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/// Logique pure de lecture de fichier audio
|
|
|
|
|
///
|
|
|
|
|
/// Contient seulement la logique de décodage et d'envoi des segments,
|
|
|
|
|
/// sans la plomberie d'orchestration (gérée par Node<FileSourceLogic>).
|
|
|
|
|
pub struct FileSourceLogic {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
chunk_frames: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FileSourceLogic {
|
|
|
|
|
pub fn new<P: Into<PathBuf>>(path: P, chunk_frames: usize) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
path: path.into(),
|
|
|
|
|
chunk_frames,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait::async_trait]
|
|
|
|
|
impl NodeLogic for FileSourceLogic {
|
|
|
|
|
async fn process(
|
|
|
|
|
&mut self,
|
|
|
|
|
_input: Option<mpsc::Receiver<Arc<AudioSegment>>>,
|
|
|
|
|
output: Vec<mpsc::Sender<Arc<AudioSegment>>>,
|
|
|
|
|
stop_token: CancellationToken,
|
|
|
|
|
) -> Result<(), AudioError> {
|
2025-11-14 10:43:53 +01:00
|
|
|
tracing::debug!(
|
|
|
|
|
"FileSourceLogic::process started, path={:?}, {} children",
|
|
|
|
|
self.path,
|
|
|
|
|
output.len()
|
|
|
|
|
);
|
2025-11-04 20:34:44 +01:00
|
|
|
|
|
|
|
|
// Macro helper pour envoyer à tous les enfants
|
|
|
|
|
macro_rules! send_to_children {
|
|
|
|
|
($segment:expr) => {
|
|
|
|
|
for tx in &output {
|
|
|
|
|
tx.send($segment.clone())
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_| AudioError::ChildDied)?;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ouvrir le fichier
|
2025-11-14 10:43:53 +01:00
|
|
|
let file = File::open(&self.path)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AudioError::IoError(format!("Failed to open {:?}: {}", self.path, e)))?;
|
2025-11-04 20:34:44 +01:00
|
|
|
|
|
|
|
|
// Décoder le flux audio
|
|
|
|
|
let mut stream = decode_audio_stream(file)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?;
|
|
|
|
|
let stream_info = stream.info().clone();
|
|
|
|
|
|
|
|
|
|
validate_stream(&stream_info)?;
|
|
|
|
|
|
|
|
|
|
// Calculer la taille des chunks si non spécifiée (0 = auto)
|
|
|
|
|
let chunk_frames = if self.chunk_frames == 0 {
|
|
|
|
|
let frames =
|
|
|
|
|
(stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize;
|
|
|
|
|
frames.next_power_of_two().max(256)
|
|
|
|
|
} else {
|
|
|
|
|
self.chunk_frames.max(1)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Émettre TopZeroSync
|
|
|
|
|
let top_zero = AudioSegment::new_top_zero_sync();
|
|
|
|
|
send_to_children!(top_zero);
|
|
|
|
|
|
|
|
|
|
// Extraire et émettre les métadonnées du fichier
|
|
|
|
|
if let Ok(file_metadata) = AudioFileMetadata::from_file(&self.path) {
|
|
|
|
|
let mut metadata = MemoryTrackMetadata::new();
|
|
|
|
|
if let Some(title) = file_metadata.title {
|
|
|
|
|
let _ = metadata.set_title(Some(title)).await;
|
|
|
|
|
}
|
|
|
|
|
if let Some(artist) = file_metadata.artist {
|
|
|
|
|
let _ = metadata.set_artist(Some(artist)).await;
|
|
|
|
|
}
|
|
|
|
|
if let Some(album) = file_metadata.album {
|
|
|
|
|
let _ = metadata.set_album(Some(album)).await;
|
|
|
|
|
}
|
|
|
|
|
if let Some(year) = file_metadata.year {
|
|
|
|
|
let _ = metadata.set_year(Some(year)).await;
|
|
|
|
|
}
|
|
|
|
|
if let Some(duration_secs) = file_metadata.duration_secs {
|
|
|
|
|
let _ = metadata
|
|
|
|
|
.set_duration(Some(Duration::from_secs(duration_secs)))
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let track_boundary = AudioSegment::new_track_boundary(
|
|
|
|
|
0,
|
|
|
|
|
0.0,
|
|
|
|
|
Arc::new(tokio::sync::RwLock::new(metadata)),
|
|
|
|
|
);
|
|
|
|
|
send_to_children!(track_boundary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Préparer la lecture des chunks audio
|
|
|
|
|
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
|
|
|
|
|
let chunk_byte_len = chunk_frames * frame_bytes;
|
|
|
|
|
let mut pending = Vec::new();
|
|
|
|
|
let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)];
|
|
|
|
|
let mut chunk_index = 0u64;
|
|
|
|
|
let mut total_frames = 0u64;
|
|
|
|
|
|
|
|
|
|
// Lire et émettre les chunks audio
|
|
|
|
|
loop {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
_ = stop_token.cancelled() => {
|
|
|
|
|
tracing::info!("FileSourceLogic: stop requested");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
read_result = stream.read(&mut read_buf) => {
|
|
|
|
|
// Remplir le buffer
|
|
|
|
|
if pending.len() < chunk_byte_len {
|
|
|
|
|
let read = read_result.map_err(|e| {
|
|
|
|
|
AudioError::IoError(format!("I/O error while decoding: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
if read == 0 && pending.is_empty() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if read > 0 {
|
|
|
|
|
pending.extend_from_slice(&read_buf[..read]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if pending.is_empty() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extraire un chunk
|
|
|
|
|
let frames_in_pending = pending.len() / frame_bytes;
|
|
|
|
|
let frames_to_emit = frames_in_pending.min(chunk_frames);
|
|
|
|
|
if frames_to_emit == 0 {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
let take_bytes = frames_to_emit * frame_bytes;
|
|
|
|
|
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
|
|
|
|
|
|
|
|
|
|
// Calculer le timestamp
|
|
|
|
|
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
|
|
|
|
|
|
|
|
|
|
// Créer et envoyer le segment audio
|
|
|
|
|
let segment = bytes_to_segment(
|
|
|
|
|
&chunk_bytes,
|
|
|
|
|
&stream_info,
|
|
|
|
|
frames_to_emit,
|
|
|
|
|
chunk_index,
|
|
|
|
|
timestamp_sec,
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
send_to_children!(segment);
|
|
|
|
|
|
|
|
|
|
chunk_index += 1;
|
|
|
|
|
total_frames += frames_to_emit as u64;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Traiter le reste éventuel (moins qu'un chunk complet)
|
|
|
|
|
if !pending.is_empty() {
|
|
|
|
|
let frames = pending.len() / frame_bytes;
|
|
|
|
|
if frames > 0 {
|
|
|
|
|
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
|
|
|
|
|
let segment =
|
|
|
|
|
bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?;
|
|
|
|
|
send_to_children!(segment);
|
|
|
|
|
total_frames += frames as u64;
|
|
|
|
|
chunk_index += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Émettre EndOfStream
|
|
|
|
|
let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64;
|
|
|
|
|
let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp);
|
|
|
|
|
send_to_children!(eos);
|
|
|
|
|
|
|
|
|
|
// Attendre la fin du décodage
|
|
|
|
|
stream
|
|
|
|
|
.wait()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// WRAPPER FileSource - Délègue à Node<FileSourceLogic>
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2025-11-01 21:10:57 +01:00
|
|
|
/// FileSource - Lit un fichier audio et publie des `AudioSegment`
|
|
|
|
|
///
|
|
|
|
|
/// Cette source utilise `pmoflac` pour décoder le fichier (FLAC/MP3/OGG/WAV/AIFF)
|
|
|
|
|
/// puis transforme les échantillons PCM en `AudioSegment` stéréo avec le type approprié
|
|
|
|
|
/// (I16, I24, ou I32) selon la profondeur de bit du fichier source.
|
|
|
|
|
///
|
|
|
|
|
/// Le node émet trois types de syncmarkers :
|
|
|
|
|
/// - `TopZeroSync` au début du flux
|
|
|
|
|
/// - `TrackBoundary` avec les métadonnées du fichier
|
|
|
|
|
/// - `EndOfStream` à la fin du flux
|
2025-11-04 20:34:44 +01:00
|
|
|
///
|
|
|
|
|
/// # Architecture
|
|
|
|
|
///
|
|
|
|
|
/// Utilise la nouvelle architecture avec `Node<FileSourceLogic>` pour séparer
|
|
|
|
|
/// la logique métier (décodage) de la plomberie (spawning, monitoring).
|
2025-11-01 21:10:57 +01:00
|
|
|
pub struct FileSource {
|
2025-11-04 20:34:44 +01:00
|
|
|
inner: Node<FileSourceLogic>,
|
2025-11-01 21:10:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FileSource {
|
|
|
|
|
/// Crée une nouvelle source de fichier avec calcul automatique de la taille des chunks.
|
|
|
|
|
///
|
|
|
|
|
/// La taille des chunks sera calculée automatiquement pour obtenir environ 50ms
|
|
|
|
|
/// de latence par chunk, en fonction du sample rate du fichier.
|
|
|
|
|
///
|
|
|
|
|
/// * `path` - chemin du fichier audio à lire
|
|
|
|
|
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
|
|
|
|
Self::with_chunk_size(path, 0) // 0 = auto-calculer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Crée une nouvelle source de fichier avec une taille de chunk spécifique.
|
|
|
|
|
///
|
|
|
|
|
/// * `path` - chemin du fichier audio à lire
|
|
|
|
|
/// * `chunk_frames` - nombre d'échantillons par canal par chunk (0 = auto)
|
|
|
|
|
pub fn with_chunk_size<P: Into<PathBuf>>(path: P, chunk_frames: usize) -> Self {
|
2025-11-04 20:34:44 +01:00
|
|
|
let logic = FileSourceLogic::new(path, chunk_frames);
|
2025-11-01 21:10:57 +01:00
|
|
|
Self {
|
2025-11-04 20:34:44 +01:00
|
|
|
inner: Node::new_source(logic),
|
2025-11-01 21:10:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 20:34:44 +01:00
|
|
|
#[async_trait::async_trait]
|
|
|
|
|
impl AudioPipelineNode for FileSource {
|
|
|
|
|
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
|
|
|
|
|
self.inner.get_tx()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn register(&mut self, child: Box<dyn AudioPipelineNode>) {
|
|
|
|
|
self.inner.register(child)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-14 10:43:53 +01:00
|
|
|
async fn run(self: Box<Self>, stop_token: CancellationToken) -> Result<(), AudioError> {
|
2025-11-04 20:34:44 +01:00
|
|
|
Box::new(self.inner).run(stop_token).await
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 21:10:57 +01:00
|
|
|
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
|
|
|
|
|
if !(1..=2).contains(&info.channels) {
|
|
|
|
|
return Err(AudioError::ProcessingError(format!(
|
|
|
|
|
"Unsupported channel count: {}",
|
|
|
|
|
info.channels
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
match info.bits_per_sample {
|
|
|
|
|
8 | 16 | 24 | 32 => Ok(()),
|
|
|
|
|
other => Err(AudioError::ProcessingError(format!(
|
|
|
|
|
"Unsupported bit depth: {}",
|
|
|
|
|
other
|
|
|
|
|
))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Convertit des bytes PCM en AudioSegment avec le type approprié
|
|
|
|
|
fn bytes_to_segment(
|
|
|
|
|
chunk_bytes: &[u8],
|
|
|
|
|
info: &StreamInfo,
|
|
|
|
|
frames: usize,
|
|
|
|
|
order: u64,
|
|
|
|
|
timestamp_sec: f64,
|
|
|
|
|
) -> Result<Arc<AudioSegment>, AudioError> {
|
|
|
|
|
let bytes_per_sample = info.bytes_per_sample();
|
|
|
|
|
let channels = info.channels as usize;
|
|
|
|
|
let frame_bytes = bytes_per_sample * channels;
|
|
|
|
|
|
|
|
|
|
// Créer le chunk du bon type selon la profondeur de bit
|
|
|
|
|
let chunk = match info.bits_per_sample {
|
|
|
|
|
16 => {
|
|
|
|
|
// Type I16
|
|
|
|
|
let mut stereo = Vec::with_capacity(frames);
|
|
|
|
|
for frame_idx in 0..frames {
|
|
|
|
|
let base = frame_idx * frame_bytes;
|
|
|
|
|
let l = i16::from_le_bytes(
|
|
|
|
|
chunk_bytes[base..base + bytes_per_sample]
|
|
|
|
|
.try_into()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
let r = if channels == 1 {
|
|
|
|
|
l
|
|
|
|
|
} else {
|
|
|
|
|
i16::from_le_bytes(
|
|
|
|
|
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
|
|
|
|
|
.try_into()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
stereo.push([l, r]);
|
|
|
|
|
}
|
|
|
|
|
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
|
|
|
|
AudioChunk::I16(chunk_data)
|
|
|
|
|
}
|
|
|
|
|
24 => {
|
|
|
|
|
// Type I24
|
|
|
|
|
let mut stereo = Vec::with_capacity(frames);
|
|
|
|
|
for frame_idx in 0..frames {
|
|
|
|
|
let base = frame_idx * frame_bytes;
|
|
|
|
|
let l_i32 = {
|
|
|
|
|
let mut buf = [0u8; 4];
|
|
|
|
|
buf[..3].copy_from_slice(&chunk_bytes[base..base + 3]);
|
|
|
|
|
// Sign extend
|
|
|
|
|
if chunk_bytes[base + 2] & 0x80 != 0 {
|
|
|
|
|
buf[3] = 0xFF;
|
|
|
|
|
}
|
|
|
|
|
i32::from_le_bytes(buf)
|
|
|
|
|
};
|
|
|
|
|
let l = I24::new(l_i32).ok_or_else(|| {
|
|
|
|
|
AudioError::ProcessingError(format!("Invalid I24 value: {}", l_i32))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let r = if channels == 1 {
|
|
|
|
|
l
|
|
|
|
|
} else {
|
|
|
|
|
let r_i32 = {
|
|
|
|
|
let mut buf = [0u8; 4];
|
|
|
|
|
buf[..3].copy_from_slice(
|
|
|
|
|
&chunk_bytes[base + bytes_per_sample..base + bytes_per_sample + 3],
|
|
|
|
|
);
|
|
|
|
|
// Sign extend
|
|
|
|
|
if chunk_bytes[base + bytes_per_sample + 2] & 0x80 != 0 {
|
|
|
|
|
buf[3] = 0xFF;
|
|
|
|
|
}
|
|
|
|
|
i32::from_le_bytes(buf)
|
|
|
|
|
};
|
|
|
|
|
I24::new(r_i32).ok_or_else(|| {
|
|
|
|
|
AudioError::ProcessingError(format!("Invalid I24 value: {}", r_i32))
|
|
|
|
|
})?
|
|
|
|
|
};
|
|
|
|
|
stereo.push([l, r]);
|
|
|
|
|
}
|
|
|
|
|
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
|
|
|
|
AudioChunk::I24(chunk_data)
|
|
|
|
|
}
|
|
|
|
|
32 => {
|
|
|
|
|
// Type I32
|
|
|
|
|
let mut stereo = Vec::with_capacity(frames);
|
|
|
|
|
for frame_idx in 0..frames {
|
|
|
|
|
let base = frame_idx * frame_bytes;
|
|
|
|
|
let l = i32::from_le_bytes(
|
|
|
|
|
chunk_bytes[base..base + bytes_per_sample]
|
|
|
|
|
.try_into()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
let r = if channels == 1 {
|
|
|
|
|
l
|
|
|
|
|
} else {
|
|
|
|
|
i32::from_le_bytes(
|
|
|
|
|
chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample]
|
|
|
|
|
.try_into()
|
|
|
|
|
.unwrap(),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
stereo.push([l, r]);
|
|
|
|
|
}
|
|
|
|
|
let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0);
|
|
|
|
|
AudioChunk::I32(chunk_data)
|
|
|
|
|
}
|
|
|
|
|
other => {
|
|
|
|
|
return Err(AudioError::ProcessingError(format!(
|
|
|
|
|
"Unsupported bit depth: {}",
|
|
|
|
|
other
|
|
|
|
|
)))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Créer le segment audio
|
|
|
|
|
Ok(Arc::new(AudioSegment {
|
|
|
|
|
order,
|
|
|
|
|
timestamp_sec,
|
|
|
|
|
segment: crate::_AudioSegment::Chunk(Arc::new(chunk)),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TypedAudioNode for FileSource {
|
|
|
|
|
fn input_type(&self) -> Option<TypeRequirement> {
|
|
|
|
|
// FileSource est une source, elle ne consomme pas d'audio
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn output_type(&self) -> Option<TypeRequirement> {
|
|
|
|
|
// FileSource peut produire n'importe quel type entier (I16, I24, I32)
|
|
|
|
|
// selon la profondeur de bit du fichier source
|
|
|
|
|
Some(TypeRequirement::any_integer())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
|
use tokio::sync::mpsc;
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_file_source_decodes_flac() {
|
|
|
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let flac_path = temp_dir.path().join("test.flac");
|
|
|
|
|
|
|
|
|
|
let sample_rate = 48_000;
|
|
|
|
|
let frames = 256;
|
|
|
|
|
let mut pcm = Vec::with_capacity(frames * 4);
|
|
|
|
|
for i in 0..frames {
|
|
|
|
|
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5; // simple ramp
|
|
|
|
|
let sample_i16 = (sample * 32767.0) as i16;
|
|
|
|
|
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
|
|
|
|
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let format = PcmFormat {
|
|
|
|
|
sample_rate,
|
|
|
|
|
channels: 2,
|
|
|
|
|
bits_per_sample: 16,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut flac_stream =
|
|
|
|
|
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut file = File::create(&flac_path).await.expect("create flac file");
|
|
|
|
|
tokio::io::copy(&mut flac_stream, &mut file)
|
|
|
|
|
.await
|
|
|
|
|
.expect("write flac");
|
|
|
|
|
file.flush().await.expect("flush file");
|
|
|
|
|
flac_stream.wait().await.unwrap();
|
|
|
|
|
|
2025-11-03 06:55:45 +01:00
|
|
|
// Créer un collecteur simple qui transmet les segments à un channel de test
|
|
|
|
|
struct TestCollectorNode {
|
|
|
|
|
tx: mpsc::Sender<Arc<AudioSegment>>,
|
|
|
|
|
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
|
|
|
|
test_tx: mpsc::Sender<Arc<AudioSegment>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TestCollectorNode {
|
|
|
|
|
fn new(test_tx: mpsc::Sender<Arc<AudioSegment>>) -> Self {
|
|
|
|
|
let (tx, rx) = mpsc::channel(16);
|
2025-11-14 10:43:53 +01:00
|
|
|
Self { tx, rx, test_tx }
|
2025-11-03 06:55:45 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait::async_trait]
|
|
|
|
|
impl AudioPipelineNode for TestCollectorNode {
|
|
|
|
|
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
|
|
|
|
|
Some(self.tx.clone())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn register(&mut self, _child: Box<dyn AudioPipelineNode>) {
|
|
|
|
|
panic!("TestCollectorNode is a terminal node");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn run(
|
|
|
|
|
mut self: Box<Self>,
|
|
|
|
|
stop_token: CancellationToken,
|
|
|
|
|
) -> Result<(), AudioError> {
|
|
|
|
|
// Transférer tous les segments au test
|
|
|
|
|
loop {
|
|
|
|
|
tokio::select! {
|
|
|
|
|
_ = stop_token.cancelled() => {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
segment = self.rx.recv() => {
|
|
|
|
|
match segment {
|
|
|
|
|
Some(seg) => {
|
|
|
|
|
if self.test_tx.send(seg).await.is_err() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => break,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (test_tx, mut rx) = mpsc::channel(1024);
|
2025-11-01 21:10:57 +01:00
|
|
|
let mut source = FileSource::with_chunk_size(&flac_path, 64);
|
2025-11-03 06:55:45 +01:00
|
|
|
let collector = TestCollectorNode::new(test_tx);
|
|
|
|
|
source.register(Box::new(collector));
|
2025-11-01 21:10:57 +01:00
|
|
|
|
|
|
|
|
tokio::spawn(async move {
|
2025-11-03 06:55:45 +01:00
|
|
|
let token = CancellationToken::new();
|
|
|
|
|
Box::new(source).run(token).await.unwrap();
|
2025-11-01 21:10:57 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut received_frames = 0usize;
|
|
|
|
|
let mut received_syncmarkers = 0usize;
|
|
|
|
|
let mut seen_top_zero = false;
|
|
|
|
|
let mut seen_eos = false;
|
|
|
|
|
|
|
|
|
|
while let Some(segment) = rx.recv().await {
|
|
|
|
|
if segment.is_audio_chunk() {
|
|
|
|
|
if let Some(chunk) = segment.as_chunk() {
|
|
|
|
|
received_frames += chunk.len();
|
|
|
|
|
assert_eq!(chunk.sample_rate(), sample_rate);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
received_syncmarkers += 1;
|
|
|
|
|
if let Some(marker) = segment.as_sync_marker() {
|
|
|
|
|
match **marker {
|
|
|
|
|
crate::SyncMarker::TopZeroSync => seen_top_zero = true,
|
|
|
|
|
crate::SyncMarker::EndOfStream => seen_eos = true,
|
|
|
|
|
crate::SyncMarker::TrackBoundary { .. } => {}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier que tous les frames ont été reçus
|
|
|
|
|
assert_eq!(received_frames, frames);
|
|
|
|
|
// Vérifier qu'on a bien reçu des syncmarkers
|
|
|
|
|
assert!(received_syncmarkers >= 2); // Au moins TopZeroSync et EndOfStream
|
|
|
|
|
assert!(seen_top_zero, "Should have received TopZeroSync");
|
|
|
|
|
assert!(seen_eos, "Should have received EndOfStream");
|
|
|
|
|
}
|
|
|
|
|
}
|