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
+115
View File
@@ -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"
+3
View File
@@ -4,3 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
alsa = "0.11"
opus = "0.3"
anyhow = "1.0"
+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;
}
}
}
}
+89 -7
View File
@@ -31,17 +31,99 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN rustup target add armv7-unknown-linux-gnueabihf
# 配置 Cargo
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS="-C link-arg=-Wl,-dynamic-linker,/lib/ld-linux-armhf.so.3"
# 设置 CC
ENV CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc
WORKDIR /app
COPY run.sh /usr/local/bin/run
COPY runtime.sh /usr/local/bin/runtime
RUN chmod +x /usr/local/bin/run /usr/local/bin/runtime
# 解压 root.squashfs
COPY root.squashfs /app/root.squashfs
RUN mkdir -p /opt/sysroot \
&& unsquashfs -d /opt/sysroot -f /app/root.squashfs \
&& rm -rf /app/root.squashfs
# 创建专门针对 armhf 的源文件
# 注意:ubuntu:20.04 默认源里没有 armhf,必须添加 ubuntu-ports 源
RUN echo "deb [arch=armhf] http://mirrors.aliyun.com/ubuntu-ports/ focal main restricted universe multiverse" > /etc/apt/sources.list.d/armhf.list && \
echo "deb [arch=armhf] http://mirrors.aliyun.com/ubuntu-ports/ focal-updates main restricted universe multiverse" >> /etc/apt/sources.list.d/armhf.list && \
# 限制原有的源只针对 amd64,防止冲突
sed -i 's/deb http/deb [arch=amd64] http/g' /etc/apt/sources.list
# 安装 armhf 架构的开发库
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
dpkg --add-architecture armhf && \
apt-get update && apt-get install -y \
crossbuild-essential-armhf \
libasound2-dev:armhf \
libopus-dev:armhf \
libssl-dev:armhf
# --------------------------------------------------------------------------------
# 构建混合 Sysroot
# 目标:结合 OpenWrt 的运行时库 (root.squashfs) 和 Toolchain/Ubuntu 的开发文件
# --------------------------------------------------------------------------------
# 修复 OpenWrt sysroot:自动为所有共享库创建 .so 符号链接
# OpenWrt 只有 libfoo.so.1,编译链接需要 libfoo.so
RUN find /opt/sysroot/lib /opt/sysroot/usr/lib -name "lib*.so.*" | while read src; do \
# 获取库名 (例如 libopus.so.0.8.0 -> libopus.so)
link_name=$(echo "$src" | sed 's/\.so\..*$/.so/'); \
if [ ! -e "$link_name" ]; then \
ln -s "$(basename "$src")" "$link_name"; \
fi \
done
# 注入 Toolchain 的 C 运行时启动文件 (Scrt1.o, crti.o 等)
RUN cp -r /opt/toolchain/arm-linux-gnueabihf/libc/usr/lib/*crt*.o /opt/sysroot/usr/lib/ 2>/dev/null || \
cp -r /opt/toolchain/arm-linux-gnueabihf/libc/lib/*crt*.o /opt/sysroot/usr/lib/ 2>/dev/null || true
# 特殊处理 glibc 的 linker script
# OpenWrt rootfs 中没有开发用的 libc.so (linker script) 和 libc_nonshared.a
# 我们需要从 toolchain 中复制 libc_nonshared.a,并手动创建 libc.so 脚本
# 这样 linker 才能正确处理 libc.so.6 和 ld-linux-armhf.so.3 的依赖关系 (解决 __tls_get_addr 错误)
RUN cp /opt/toolchain/arm-linux-gnueabihf/libc/usr/lib/libc_nonshared.a /opt/sysroot/usr/lib/ && \
# 删除之前步骤可能自动生成的软链接
rm -f /opt/sysroot/usr/lib/libc.so && \
# 创建 linker script
echo 'OUTPUT_FORMAT(elf32-littlearm)' > /opt/sysroot/usr/lib/libc.so && \
echo 'GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux-armhf.so.3 ) )' >> /opt/sysroot/usr/lib/libc.so && \
# 同理处理 libpthread.so (如果存在 libpthread.so.0)
if [ -f /opt/sysroot/lib/libpthread.so.0 ]; then \
cp /opt/toolchain/arm-linux-gnueabihf/libc/usr/lib/libpthread_nonshared.a /opt/sysroot/usr/lib/ 2>/dev/null || true; \
rm -f /opt/sysroot/usr/lib/libpthread.so; \
echo 'OUTPUT_FORMAT(elf32-littlearm)' > /opt/sysroot/usr/lib/libpthread.so; \
echo 'GROUP ( /lib/libpthread.so.0 /usr/lib/libpthread_nonshared.a )' >> /opt/sysroot/usr/lib/libpthread.so; \
fi
# 注入 Ubuntu 的开发头文件和 pkg-config 定义 (用于 alsa, opus 等)
# 我们只取头文件和 .pc 文件,不取库文件,确保链接的是 OpenWrt 的库
RUN mkdir -p /opt/sysroot/usr/include /opt/sysroot/usr/lib/pkgconfig && \
cp -r /usr/include/alsa /opt/sysroot/usr/include/ 2>/dev/null || true && \
cp -r /usr/include/opus /opt/sysroot/usr/include/ 2>/dev/null || true && \
cp -r /usr/include/arm-linux-gnueabihf/* /opt/sysroot/usr/include/ 2>/dev/null || true && \
cp -r /usr/lib/arm-linux-gnueabihf/pkgconfig/* /opt/sysroot/usr/lib/pkgconfig/ 2>/dev/null || true
# 优化 Rust 链接参数
# 1. 优先使用 -L 指定 sysroot 路径
# 2. 使用 -rpath-link 解决库的间接依赖问题
# 3. 确保 dynamic-linker 路径正确 (OpenWrt glibc 常用路径)
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS="\
-C link-arg=--sysroot=/opt/sysroot \
-C link-arg=-Wl,-dynamic-linker,/lib/ld-linux-armhf.so.3 \
-C link-arg=-L/opt/sysroot/usr/lib \
-C link-arg=-L/opt/sysroot/lib \
-C link-arg=-Wl,-rpath-link,/opt/sysroot/usr/lib \
-C link-arg=-Wl,-rpath-link,/opt/sysroot/lib"
# 强制 pkg-config 只在 sysroot 中寻找,忽略宿主机容器路径
ENV PKG_CONFIG_ALLOW_CROSS=1
ENV PKG_CONFIG_SYSROOT_DIR=/opt/sysroot
ENV PKG_CONFIG_LIBDIR=/opt/sysroot/usr/lib/pkgconfig:/opt/sysroot/lib/pkgconfig
# 设置编译器,强制指定 sysroot
ENV CC_armv7_unknown_linux_gnueabihf="arm-linux-gnueabihf-gcc --sysroot=/opt/sysroot"
CMD ["/bin/bash"]
+8 -14
View File
@@ -12,25 +12,19 @@ PROXY="http://host.docker.internal:7890"
# --build-arg https_proxy=$PROXY \
# -t open-xiaoai-runtime .
# # 2. 编译 hello world demo
# 2. 编译 hello world demo
echo "Compiling hello demo..."
rm -rf ../hello/target # 先清理旧的构建产物,防止缓存干扰
docker run --rm -v $(pwd)/../hello:/app/hello open-xiaoai-runtime \
cargo build --manifest-path hello/Cargo.toml --target armv7-unknown-linux-gnueabihf --release
# # 3. 运行编译出的二进制文件 (通过 QEMU 模拟)
echo "Running compiled binary via QEMU..."
docker run --rm -v $(pwd)/../hello/target/armv7-unknown-linux-gnueabihf/release/hello:/app/hello \
-v $(pwd)/root.squashfs:/app/root.squashfs open-xiaoai-runtime \
run /app/hello
# 3. 运行编译出的二进制文件 (通过 QEMU 模拟)
# echo "Running compiled binary via QEMU..."
# docker run --rm -v $(pwd)/../hello/target/armv7-unknown-linux-gnueabihf/release/hello:/app/hello open-xiaoai-runtime \
# run /app/hello
# 4. 交互模式运行
# docker run --rm -it --privileged \
# -v $(pwd)/root.squashfs:/app/root.squashfs \
# open-xiaoai-runtime \
# run
# docker run --rm -it --privileged open-xiaoai-runtime run
# 5. 上传二进制文件到小爱音箱
# dd if=target/armv7-unknown-linux-gnueabihf/release/hello \
# | ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd of=/data/hello"
# ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd if=/data/hello" | hello
# dd if=apps/hello/target/armv7-unknown-linux-gnueabihf/release/hello \
# | ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd of=/data/hello"
-18
View File
@@ -5,24 +5,6 @@ set -e
# 定义 sysroot 目录
SYSROOT_DIR="/opt/sysroot"
# 如果 /opt/sysroot 已经有内容(通过 -v 挂载了 squashfs-root),则跳过解压
if [ -d "$SYSROOT_DIR/bin" ] || [ -d "$SYSROOT_DIR/lib" ]; then
echo "Using existing sysroot at $SYSROOT_DIR"
else
mkdir -p "$SYSROOT_DIR"
# 优先使用环境变量指定的 squashfs 文件,否则使用默认的 root.squashfs
ROOTFS_FILE="${SYSROOT_FILE:-root.squashfs}"
if [ -f "$ROOTFS_FILE" ]; then
echo -e "\n🔥 Unsquashing $ROOTFS_FILE to $SYSROOT_DIR...\n"
unsquashfs -d "$SYSROOT_DIR" -f "$ROOTFS_FILE"
echo -e "\n✅ Unsquashed rootfs\n"
else
echo "Error: Sysroot file $ROOTFS_FILE not found and $SYSROOT_DIR is empty."
exit 1
fi
fi
# 检查第一个参数。如果在 sysroot 下存在该文件,则自动补全路径
# 比如 `run /bin/sh` 会被转换为 `run /opt/sysroot/bin/sh`
if [ -n "$1" ] && [ -f "$SYSROOT_DIR$1" ]; then