feat: 动态链接到 opus 和 alsa 基础库,完成音频录制播放和编解码
This commit is contained in:
Generated
+115
@@ -2,6 +2,121 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "audiopus_sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651"
|
||||
dependencies = [
|
||||
"cmake",
|
||||
"log",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
||||
|
||||
[[package]]
|
||||
name = "hello"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"anyhow",
|
||||
"opus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.178"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "opus"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6526409b274a7e98e55ff59d96aafd38e6cd34d46b7dbbc32ce126dffcd75e8e"
|
||||
dependencies = [
|
||||
"audiopus_sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
@@ -4,3 +4,6 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
alsa = "0.11"
|
||||
opus = "0.3"
|
||||
anyhow = "1.0"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 常用 16k,ALSA plug 会自动从 48k 转换
|
||||
channels: 1,
|
||||
frame_size: 320, // 20ms at 16kHz
|
||||
bitrate: 16000,
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user