feat: 动态链接到 opus 和 alsa 基础库,完成音频录制播放和编解码

This commit is contained in:
Del Wang
2025-12-31 22:53:33 +08:00
committed by Del
parent c521e274e9
commit fba40393c2
11 changed files with 431 additions and 46 deletions
+31
View File
@@ -0,0 +1,31 @@
use opus::{Encoder, Decoder, Application, Bitrate, Channels};
use anyhow::{Result, Context};
use crate::config::AudioConfig;
pub struct OpusCodec {
encoder: Encoder,
decoder: Decoder,
}
impl OpusCodec {
pub fn new(config: &AudioConfig) -> Result<Self> {
let channels = if config.channels == 1 { Channels::Mono } else { Channels::Stereo };
let mut encoder = Encoder::new(config.sample_rate, channels, Application::Voip)
.context("Failed to create Opus encoder")?;
encoder.set_bitrate(Bitrate::Bits(config.bitrate))?;
let decoder = Decoder::new(config.sample_rate, channels)
.context("Failed to create Opus decoder")?;
Ok(Self { encoder, decoder })
}
pub fn encode(&mut self, pcm: &[i16], out: &mut [u8]) -> Result<usize> {
self.encoder.encode(pcm, out).context("Opus encoding failed")
}
pub fn decode(&mut self, opus: &[u8], out: &mut [i16]) -> Result<usize> {
self.decoder.decode(opus, out, false).context("Opus decoding failed")
}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod recorder;
pub mod player;
pub mod codec;
pub use recorder::AudioRecorder;
pub use player::AudioPlayer;
pub use codec::OpusCodec;
+54
View File
@@ -0,0 +1,54 @@
use crate::config::AudioConfig;
use alsa::pcm::{Access, Format, HwParams, PCM};
use alsa::Direction;
use anyhow::{Context, Result};
pub struct AudioPlayer {
pcm: PCM,
}
impl AudioPlayer {
pub fn new(config: &AudioConfig) -> Result<Self> {
let pcm = PCM::new(&config.playback_device, Direction::Playback, false)
.context("Failed to open playback PCM device")?;
setup_pcm(&pcm, config.sample_rate, config.channels)?;
Ok(Self { pcm })
}
pub fn write(&self, buffer: &[i16]) -> Result<usize> {
let res = self.pcm.io_i16()?.writei(buffer);
match res {
Ok(written) => Ok(written),
Err(e) => {
// Buffer Underrun,即播放缓冲区的数据被耗尽,导致音频流中断
if e.errno() == 32 {
// 恢复音频流状态
self.pcm.prepare()?;
// 重新获取 IO 对象并尝试写入数据
self.pcm
.io_i16()?
.writei(buffer)
.context("Failed to write to playback device after recovery")
} else {
Err(e).context("Failed to write to playback device")
}
}
}
}
}
fn setup_pcm(pcm: &PCM, sample_rate: u32, channels: u16) -> Result<()> {
let hwp = HwParams::any(pcm).context("Failed to get HwParams")?;
hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?;
hwp.set_rate(sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels(channels as u32)?;
pcm.hw_params(&hwp).context("Failed to set HwParams")?;
let swp = pcm.sw_params_current()?;
pcm.sw_params(&swp)?;
pcm.prepare()?;
Ok(())
}
+37
View File
@@ -0,0 +1,37 @@
use alsa::pcm::{PCM, HwParams, Format, Access};
use alsa::Direction;
use anyhow::{Result, Context};
use crate::config::AudioConfig;
pub struct AudioRecorder {
pcm: PCM,
}
impl AudioRecorder {
pub fn new(config: &AudioConfig) -> Result<Self> {
let pcm = PCM::new(&config.capture_device, Direction::Capture, false)
.context("Failed to open capture PCM device")?;
setup_pcm(&pcm, config.sample_rate, config.channels)?;
Ok(Self { pcm })
}
pub fn read(&self, buffer: &mut [i16]) -> Result<usize> {
self.pcm.io_i16()?.readi(buffer).context("Failed to read from capture device")
}
}
fn setup_pcm(pcm: &PCM, sample_rate: u32, channels: u16) -> Result<()> {
let hwp = HwParams::any(pcm).context("Failed to get HwParams")?;
hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?;
hwp.set_rate(sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels(channels as u32)?;
pcm.hw_params(&hwp).context("Failed to set HwParams")?;
let swp = pcm.sw_params_current()?;
pcm.sw_params(&swp)?;
pcm.prepare()?;
Ok(())
}
+25
View File
@@ -0,0 +1,25 @@
pub struct AudioConfig {
pub capture_device: String,
pub playback_device: String,
pub sample_rate: u32,
pub channels: u16,
pub frame_size: usize,
pub bitrate: i32,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
// 根据 apps/runtime/squashfs-root/etc/asound.conf 分析:
// 录音设备对应 pcm.Capture -> hw:0,3 (S32_LE, 48000Hz)
// 播放设备对应 pcm.dmixer -> hw:0,2 (S16_LE, 48000Hz)
// 使用 "default" 或 "plug" 设备可以由 ALSA 自动处理采样率和格式转换
capture_device: "plug:Capture".to_string(),
playback_device: "default".to_string(),
sample_rate: 16000, // Opus 常用 16kALSA plug 会自动从 48k 转换
channels: 1,
frame_size: 320, // 20ms at 16kHz
bitrate: 16000,
}
}
}
+61 -7
View File
@@ -1,9 +1,63 @@
fn main() {
println!("Hello from OpenWrt (ARMhf)!");
println!("System information (via /proc/version):");
if let Ok(version) = std::fs::read_to_string("/proc/version") {
println!("{}", version);
} else {
println!("Could not read /proc/version (running in simulation?)");
mod audio;
mod config;
use crate::audio::{AudioPlayer, AudioRecorder, OpusCodec};
use crate::config::AudioConfig;
use anyhow::Result;
use std::collections::VecDeque;
use std::time::{Duration, Instant};
fn main() -> Result<()> {
let config = AudioConfig::default();
println!("Starting Audio Modular Demo with 1s delay");
println!(
"Config: capture={}, playback={}, rate={}, channels={}, frame_size={}",
config.capture_device,
config.playback_device,
config.sample_rate,
config.channels,
config.frame_size
);
let recorder = AudioRecorder::new(&config)?;
let player = AudioPlayer::new(&config)?;
let mut codec = OpusCodec::new(&config)?;
// 使用队列存储编码后的 Opus 数据块及录制时间
let mut delay_queue: VecDeque<(Instant, Vec<u8>)> = VecDeque::new();
let delay_duration = Duration::from_secs(1);
let mut pcm_in = vec![0i16; config.frame_size * config.channels as usize];
let mut opus_buf = vec![0u8; 1024];
println!("Recording... Delaying playback by 1s. Press Ctrl+C to stop.");
loop {
// 1. 录制原始 PCM
let read = recorder.read(&mut pcm_in)?;
if read == config.frame_size {
// 2. 编码并存入队列
let opus_len = codec.encode(&pcm_in, &mut opus_buf)?;
let opus_data = opus_buf[..opus_len].to_vec();
delay_queue.push_back((Instant::now(), opus_data));
}
// 3. 处理延迟播放:检查队列头部数据是否已等待超过 1s
while let Some((timestamp, _)) = delay_queue.front() {
if timestamp.elapsed() >= delay_duration {
let (_, opus_data) = delay_queue.pop_front().unwrap();
let mut pcm_out = vec![0i16; config.frame_size * config.channels as usize];
let decoded_len = codec.decode(&opus_data, &mut pcm_out)?;
if decoded_len == config.frame_size {
// 4. 播放还原后的 PCM
player.write(&pcm_out)?;
}
} else {
// 头部数据还未到 1s,由于队列是按时间排序的,后面的肯定也没到
break;
}
}
}
}