当前位置: 首页 > article >正文

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

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

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

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

#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写个程序(当然也是让大模型帮着写啦)

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

[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"

效果还行


http://www.kler.cn/a/520190.html

相关文章:

  • 基于OSAL的嵌入式裸机事件驱动框架——消息队列osal_msg
  • HTML常见文本标签解析:从基础到进阶的全面指南
  • 攻防世界bad_python
  • 网络安全 | F5-Attack Signatures详解
  • MongoDB 备份与恢复综述
  • Redis实战(黑马点评)——涉及session、redis存储验证码,双拦截器处理请求
  • 【设计模式-行为型】命令模式
  • 智能门铃开发与面试准备:技术解析与常见问题解答
  • SSH 的登录命令常见用法
  • 人工智能的应用领域和未来发展趋势
  • π0:仅有3B数据模型打通Franka等7种机器人形态适配,实现0样本的完全由模型自主控制方法
  • ESP32 I2S音频总线学习笔记(二):I2S读取INMP441音频数据
  • Java面试题2025-设计模式
  • Redis事务机制详解与Springboot项目中的使用
  • C++ 包装器与绑定器的应用之回调函数的实现
  • 美国主要的科技大公司及其产品介绍
  • 算子--数学与人工智能中的运算力量
  • C# 提取PDF表单数据
  • 旋转的错觉
  • Java Web-Tomcat Servlet
  • 运用python进行多任务学习过程中,手动调整权重时,如何选择项目并确定合适的权重值?
  • 【四川乡镇界面】图层shp格式arcgis数据乡镇名称和编码2020年wgs84无偏移内容测评
  • Spring Boot 支持的日志框架解析与最佳实践
  • day6手机摄影社区,可以去苹果摄影社区学习拍摄技巧
  • 力扣动态规划-10【算法学习day.104】
  • Couchbase UI: Logs