随笔十六、音频采集、UDP发送

此功能是远程对讲的一部分,由泰山派实时采集语音,然后UDP发送到远端。泰山派硬件使用RK809-5管理内核电源(PMIC),此IC同时具备音频编解码器(CODEC)功能,接口I2S1。

现在到处都是大模型,编写程序就变得简单多了,向大模型提出需求,程序就写好了。当然,需要修改是难免的,但少了许多码字时间。

为了提升效率,降低CPU占用,需要合理安排对底层接口的频繁调用,一些参数还是需要进一步磨合调试。泰山派这部分程序使用2个线程,分别处理音频的读取和UDP发送。主要是考虑UDP一帧数据控制在512字节,不用拆包。而声卡音频采集一个周期数据太少会浪费CPU。数据交换使用FIFO,参考linux内核函数处理。ALSA库。

cpp 复制代码
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <alsa/asoundlib.h>
#include <kfifo.h>


#define DEST_IP             "192.168.1.44"
#define DEST_PORT           9999

#define UDP_SIZE            512

#define SAMPLE_RATE         44100
#define CHANNELS            2
#define FORMAT              SND_PCM_FORMAT_S16_LE
#define FRAMES_PER_PERIOD   1024
#define PERIODS             2

#define FRAME_SIZE          (CHANNELS*snd_pcm_format_physical_width(FORMAT)/8)
#define BUFF_NUM            (1<<2)
#define BUFF_SIZE           (FRAMES_PER_PERIOD*FRAME_SIZE)

#if 0
// 定义WAV文件头结构体
typedef struct {
  char chunk_id[4];         // "RIFF"
  int chunk_size;           // 文件总大小 - 8
  char format[4];           // "WAVE"
  char subchunk1_id[4];     // "fmt "
  int subchunk1_size;       // 16 for PCM
  short audio_format;       // 1 for PCM
  short num_channels;       // 声道数
  int sample_rate;          // 采样率
  int byte_rate;            // 每秒的字节数
  short block_align;        // 每个样本的字节数
  short bits_per_sample;    // 每个样本的位数
  char subchunk2_id[4];     // "data"
  int subchunk2_size;       // 音频数据大小
} WavHeader;
#endif

void* udpPut(void* arg);    // 线程函数,发送数据
void* pcmGet(void* arg);    // 线程函数,采集数据

Kfifo_st* fifo;

// 主程序
int main()
{
  pthread_t tid[2];

  // 申请FIFO空间
  if ((fifo = Kfifo_Init(BUFF_SIZE * BUFF_NUM)) == NULL) {
    fprintf(stderr, "failed to allocate fifo memory\n");
    exit(1);
  }

  fprintf(stdout, "PCM: %dHz, %dch, %dbits\n", SAMPLE_RATE, CHANNELS, snd_pcm_format_physical_width(FORMAT));

  pthread_create(&tid[0], NULL, pcmGet, NULL);
  pthread_create(&tid[1], NULL, udpPut, NULL);

  pthread_join(tid[0], NULL);
  pthread_join(tid[1], NULL);

  Kfifo_Free(fifo);

  return 0;
}

// 采集音频数据
void* pcmGet(void* arg)
{
  int err;
  snd_pcm_t* handle;
  snd_pcm_hw_params_t* params;
  unsigned int rate = SAMPLE_RATE;
  snd_pcm_uframes_t frames = FRAMES_PER_PERIOD;

  // 申请一个采样周期缓冲区
  int size = BUFF_SIZE;
  char* buffer = (char*)malloc(size);
  if (buffer == NULL) {
    fprintf(stderr, "failed to allocate buffer memory\n");
    exit(1);
  }

#if 0
  // 打开文件进行写入
  FILE* file;
  file = fopen("talk_out.wav", "wb");
  if (file == NULL) {
    fprintf(stderr, "failed to open file for writing\n");
    free(buffer);
    exit(1);
  }

  // 初始化WAV文件头
  WavHeader header;
  int sample_counter = 0;
  int total_samples = 500;
  int data_size = total_samples * BUFF_SIZE;
  int file_size = data_size + sizeof(WavHeader);

  // 填充WAV文件头
  memcpy(header.chunk_id, "RIFF", 4);
  header.chunk_size = file_size - 8;
  memcpy(header.format, "WAVE", 4);
  memcpy(header.subchunk1_id, "fmt ", 4);
  header.subchunk1_size = 16;
  header.audio_format = 1;
  header.num_channels = CHANNELS;
  header.sample_rate = SAMPLE_RATE;
  header.byte_rate = SAMPLE_RATE * FRAME_SIZE;
  header.block_align = FRAME_SIZE;
  header.bits_per_sample = snd_pcm_format_physical_width(FORMAT);
  memcpy(header.subchunk2_id, "data", 4);
  header.subchunk2_size = data_size;

  // 写入WAV文件头
  fwrite(&header, sizeof(WavHeader), 1, file);
#endif

  // 打开音频捕获设备
  if ((err = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, 0)) < 0) {
    fprintf(stderr, "snd_pcm_open: %s\n", snd_strerror(err));
    exit(1);
  }

  // 分配硬件参数结构
  if ((err = snd_pcm_hw_params_malloc(&params)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_malloc: %s\n", snd_strerror(err));
    exit(1);
  }

  // 初始化硬件参数结构
  if ((err = snd_pcm_hw_params_any(handle, params)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_any: %s\n", snd_strerror(err));
    exit(1);
  }

  // 设置访问类型为交错模式
  if ((err = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_set_access: %s\n", snd_strerror(err));
    exit(1);
  }

  // 设置采样格式
  if ((err = snd_pcm_hw_params_set_format(handle, params, FORMAT)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_set_format: %s\n", snd_strerror(err));
    exit(1);
  }

  // 设置声道数
  if ((err = snd_pcm_hw_params_set_channels(handle, params, CHANNELS)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_set_channels: %s\n", snd_strerror(err));
    exit(1);
  }

  // 设置采样率
  if ((err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_set_rate_near: %s\n", snd_strerror(err));
    exit(1);
  }

  // 设置周期数
  if ((err = snd_pcm_hw_params_set_periods(handle, params, PERIODS, 0)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_set_periods: %s\n", snd_strerror(err));
    exit(1);
  }

  // 设置周期大小
  if ((err = snd_pcm_hw_params_set_period_size_near(handle, params, &frames, 0)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params_set_period_size_near: %s\n", snd_strerror(err));
    exit(1);
  }

  // 将硬件参数应用到设备上
  if ((err = snd_pcm_hw_params(handle, params)) < 0) {
    fprintf(stderr, "snd_pcm_hw_params: %s\n", snd_strerror(err));
    exit(1);
  }

  // 释放硬件参数结构
  snd_pcm_hw_params_free(params);

  // 准备音频捕获
  if ((err = snd_pcm_prepare(handle)) < 0) {
    fprintf(stderr, "snd_pcm_prepare: %s\n", snd_strerror(err));
    exit(1);
  }

  // 采样循环
  for (;;)
  {
    // 读取一个周期数据
    err = snd_pcm_readi(handle, buffer, frames);
    if (err != frames) {
      if (err == -EPIPE) {
        snd_pcm_prepare(handle);
      }
      else {
        fprintf(stderr, "error reading audio data: %s\n", snd_strerror(err));
      }
      continue;;
    }

    // 填充数据到FIFO
    Kfifo_In(fifo, buffer, size, 1);
    usleep(1000);

#if 0
    if (++sample_counter == total_samples) {
      fclose(file);
      printf("ok\n");
    }
    else {
      fwrite(buffer, 1, size, file);
    }
#endif
  }

  free(buffer);
  snd_pcm_drain(handle);
  snd_pcm_close(handle);
}

// 发送音频数据
void* udpPut(void* arg)
{
  // UDP一帧大小控制在512字节
  char buff[UDP_SIZE] = { 0 };

  int sockfd;
  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    perror("socket creation failed");
    exit(1);
  }

  // struct sockaddr_in local_addr;
  // socklen_t addrlen = sizeof(local_addr);
  // bzero(&local_addr, sizeof(local_addr));
  // local_addr.sin_family = AF_INET;
  // local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  // local_addr.sin_port = htons(9999);

  // // 绑定套接字到地址
  // if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
  //   perror("bind");
  //   close(sockfd);
  //   exit(1);
  // }

  // // 获取本地地址信息
  // if (getsockname(sockfd, (struct sockaddr*)&local_addr, &addrlen) < 0) {
  //   perror("getsockname failed");
  //   close(sockfd);
  //   exit(1);
  // }
  // // 提取本地端口号
  // unsigned short local_port = ntohs(local_addr.sin_port);
  // printf("UDP发送端口号: %hu\n", local_port);

  struct sockaddr_in dest;
  bzero(&dest, sizeof(dest));
  dest.sin_family = AF_INET;
  dest.sin_port = htons(DEST_PORT);
  dest.sin_addr.s_addr = inet_addr(DEST_IP);

  const struct sockaddr* remote_addr = (struct sockaddr*)(&dest);

  for (;;)
  {
    if (Kfifo_Len(fifo) >= UDP_SIZE) {
      // FIFO存在数据则读取
      Kfifo_Out(fifo, buff, UDP_SIZE);

      // 发送到远端
      sendto(sockfd, buff, sizeof(buff), 0, remote_addr, sizeof(struct sockaddr));
    }

    usleep(1000);
  }
}

远端接收就用电脑来实现,尝试用Rust写个程序(当然也是让大模型帮着写啦)

rust 复制代码
use std::net::UdpSocket;
use rodio::{OutputStream, Sink};
use std::io;
use crossterm::{
    event::{self, Event, KeyCode},
    terminal::{disable_raw_mode, enable_raw_mode},
};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

fn main() -> io::Result<()> {
    // 创建UDP套接字并绑定到本地地址和端口
    let socket = UdpSocket::bind("0.0.0.0:9999")?;
    println!("等待来自UDP的PCM (i16) 数据...");

    // 创建输出流和Sink用于播放声音
    let (_stream, stream_handle) = OutputStream::try_default().unwrap();
    let sink = Sink::try_new(&stream_handle).unwrap();

    // 设置为非阻塞模式以便可以立即开始播放收到的数据
    sink.set_volume(1.0); // 可选:设置音量

    // 准备接收缓冲区
    let mut buf = vec![0u8; 4096]; // 一般4096足够小以保持低延迟,同时足够大以减少系统调用次数

    // 原子布尔值用于控制循环
    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();

    // 启用原始模式以捕获键盘事件
    enable_raw_mode().expect("无法启用raw mode");

    // 在另一个线程中监听键盘事件
    std::thread::spawn(move || {
        while r.load(Ordering::Relaxed) {
            if event::poll(std::time::Duration::from_millis(100)).unwrap() {
                if let Event::Key(key_event) = event::read().unwrap() {
                    match key_event.code {
                        KeyCode::Esc | KeyCode::Char(_) => {
                            println!("检测到按键,程序即将退出...");
                            r.store(false, Ordering::Relaxed);
                            break;
                        },
                        _ => {}
                    }
                }
            }
        }

        // 程序结束时恢复终端模式
        disable_raw_mode().expect("无法禁用raw mode");
    });

    loop {
        if !running.load(Ordering::Relaxed) {
            break;
        }

        // 接收UDP数据报文
        match socket.recv_from(&mut buf) {
            Ok((size, addr)) => {
                //println!("从 {} 收到了 {} 字节的数据", addr, size);

                if size % 2 != 0 {
                    eprintln!("收到的数据大小不是16位样本的整数倍,可能有误!");
                    continue;
                }

                // 将字节数组转换为i16切片,假设是小端字节序
                let samples = unsafe {
                    std::slice::from_raw_parts(
                        buf.as_ptr() as *const i16,
                        size / 2
                    )
                };

                // 将i16样本添加到播放队列中
                let sample_buffer = rodio::buffer::SamplesBuffer::new(
                    2, // 两声道
                    44100, // 采样率为44100Hz
                    samples.to_vec(), // 复制数据
                );
                sink.append(sample_buffer);
            }
            Err(e) => eprintln!("遇到错误: {}", e),
        }
    }

    // 清理工作
    drop(sink);
    drop(stream_handle);
    println!("程序已退出。");

    Ok(())
}

cargo.toml

rust 复制代码
[package]
name = "demo"
version = "0.1.0"
edition = "2021"

[dependencies]
rodio = { version = "0.20.1"}
tokio = { version = "1.25.0", features = ["full"] }
crossterm = "0.28.1"

效果还行

相关推荐
代码游侠5 小时前
ARM开发——阶段问题综述(二)
运维·arm开发·笔记·单片机·嵌入式硬件·学习
DLGXY6 小时前
STM32——旋转编码器计次(七)
stm32·单片机·嵌入式硬件
羽获飞6 小时前
从零开始学嵌入式之STM32——3.使用寄存器点亮一盏LED灯
单片机·嵌入式硬件
浩子智控7 小时前
商业航天计算机抗辐射设计
单片机·嵌入式硬件
独处东汉10 小时前
freertos开发空气检测仪之输入子系统结构体设计
数据结构·人工智能·stm32·单片机·嵌入式硬件·算法
czy878747511 小时前
机智云 MCU OTA可以对MCU程序进行无线远程升级。
单片机·嵌入式硬件
A9better13 小时前
嵌入式开发学习日志52——二值与计数信号量
单片机·嵌入式硬件·学习
给算法爸爸上香15 小时前
yolo目标检测线程池高性能视频tensorrt推理(每秒1000+帧)
yolo·目标检测·音视频·线程池·tensorrt
大学生小郑15 小时前
sensor成像的原理
图像处理·音视频·视频
想放学的刺客15 小时前
单片机嵌入式试题(第25)嵌入式系统可靠性设计与外设驱动异常处理
stm32·单片机·嵌入式硬件·mcu·物联网