豆包语音--RK3576播放PCM单声道音频流

若包含一个只支持双声道 (stremo)以上的设备或者板子,那么就需要我们去讲PCM单流按照LRLR的顺序拼接到正常的声道的上。正常来说,需要声卡驱动支持单声道流,将PCM 能够正常的映射到LR两个声道上。

背景

1.1 rk3576 board 播放得音频格式

只能使用 aplay // or aplay -h

root@firefly:~# aplay -h
// 省略
Usage: aplay [OPTION]... [FILE]...
Recognized sample formats are: S8 U8 S16_LE S16_BE U16_LE U16_BE S24_LE S24_BE U24_LE U24_BE S32_LE S32_BE U32_LE U32_BE FLOAT_LE FLOAT_BE FLOAT64_LE FLOAT64_BE IEC958_SUBFRAME_LE IEC958_SUBFRAME_BE MU_LAW A_LAW IMA_ADPCM MPEG GSM S20_LE S20_BE U20_LE U20_BE SPECIAL S24_3LE S24_3BE U24_3LE U24_3BE S20_3LE S20_3BE U20_3LE U20_3BE S18_3LE S18_3BE U18_3LE U18_3BE G723_24 G723_24_1B G723_40 G723_40_1B DSD_U8 DSD_U16_LE DSD_U32_LE DSD_U16_BE DSD_U32_BE
Some of these may not be available on selected hardware
The available format shortcuts are:
-f cd (16 bit little endian, 44100, stereo)
-f cdr (16 bit big endian, 44100, stereo)
-f dat (16 bit little endian, 48000, stereo)

1.2 rk3576 board 录音得音频格式

使用 arecord -D hw:0,0 --dump-hw-params

root@firefly:~# arecord -D hw:0,0 --dump-hw-params
Warning: Some sources (like microphones) may produce inaudiable results
         with 8-bit sampling. Use '-f' argument to increase resolution
         e.g. '-f S16_LE'.
Recording WAVE 'stdin' : Unsigned 8 bit, Rate 8000 Hz, Mono
HW Params of device "hw:0,0":
--------------------
ACCESS:  MMAP_INTERLEAVED RW_INTERLEAVED
FORMAT:  S16_LE S24_LE
SUBFORMAT:  STD
SAMPLE_BITS: [16 32]
FRAME_BITS: [16 64]
CHANNELS: [1 2]
RATE: [8000 96000]
PERIOD_TIME: (41 16384000]
PERIOD_SIZE: [4 131072]
PERIOD_BYTES: [32 1048576]
PERIODS: [2 65536]
BUFFER_TIME: (83 32768000]
BUFFER_SIZE: [8 262144]
BUFFER_BYTES: [32 524288]
TICK_TIME: ALL
--------------------
arecord: set_params:1352: Sample format non available
Available formats:
- S16_LE
- S24_LE

关于播放

2.1 基于PortAudio

Volcengine 使用的是通用的 16bit depth Single channel 的pcm 流; 虽火山引擎才用的是把音频数据挂载到 Websocket 的 payload 上; 虽 websocket 采用的是 std::string 来接收,payload 我们在解析前四个字节的数据, 后面都是音频数据我们才用的是 std::vector<uint8_t> 来接收数据是比较合理的.

2.1.1 很重要, 很重要, 很重要

我们虽然采用uint8_t 来接收音频数据, 但是他是16 bitdepth; 我们复制 pcm 单声道是将每个样本复制到LR 声道.所以我们要做的是, 把每个样本的数据: 先还原,先还原,先还原; 16bitdepth 表明: uint8_t 的音频数据,每两个音频数据表示一个样本, 即两个字节表示一个样本;

2.1.2 样本数据还原

还有一个很重要: Volcengine 的数据是小端字节序

  • 既然是小端字节序, 那么就是高位在后,低位在前了;
  • 还原即高位在高位就行,高位左移8位逻辑或运算低位,那么就成功的还原到了int16_t 的数据了。即一个采样。

Talk is so cheap

std::vector<int16_t> ChatStreamEg::convert16bitMonoTo16bitStereo(const std::vector<uint8_t> &monoData) {
    const size_t sampleCount = monoData.size() / 2;
    std::vector<int16_t> stereo(sampleCount * 2);
    for (size_t i = 0; i < sampleCount; ++i) {
        int16_t sample16 = monoData[i * 2] | (monoData[i * 2 + 1] << 8);
        stereo[2 * i] = sample16;
        stereo[2 * i + 1] = sample16;
    }
    return stereo;
}

2.2 PortAudio 回调函数注释和备忘

本人在使用 PortAudio 播放和录音的时候采用的是, 在initialize 函数中打开流, 对PortAudio record/play 来说就是使用的 Pa_OpenStream() 函数; record 设置 inputParameters, play 设置outputParameters. 对rescordStream 和 playStream 具体录音还是播放只需要对recrodStream 和 playStream 状态控制startStream 和 closeStream 内存管理 即可.

2.2.1 OpenStream 定义

PaError Pa_OpenStream( PaStream** stream,
                       const PaStreamParameters *inputParameters,
                       const PaStreamParameters *outputParameters,
                       double sampleRate,
                       unsigned long framesPerBuffer,
                       PaStreamFlags streamFlags,
                       PaStreamCallback *streamCallback,
                       void *userData )

2.2.2 播放回调函数注意事项

  • frameCounts 是帧数
  • totalSampleToWrite 是总样本数
  • 后续计算机操作的单位是字节
int AudioProcess::playCallback(const void *inputBuffer,
                               void *outputBuffer,
                               unsigned long frameCounts,
                               const PaStreamCallbackTimeInfo *timeInfo,
                               PaStreamCallbackFlags statusFlags,
                               void *userData)
{
    auto *audio = static_cast<AudioProcess *>(userData);
    auto *out = static_cast<int16_t *>(outputBuffer);

    // 检查状态标志
    if (statusFlags & paOutputUnderflow)
    {
        std::lock_guard<std::mutex> lock(audio->m_statsMutex);
        audio->m_stats.underruns++;
    }

    size_t channels = audio->m_config.outputChannels;
    // 因为回调中采用的是帧数 * 声道(1个样本,2个样本);请勿被迷惑
    size_t totalSamplesToWrite = frameCounts * channels;
    size_t samplesWritten = 0;

    // 暂停
    if (audio->m_isPaused)
    {
        std::memset(out, 0, totalSamplesToWrite * sizeof(int16_t));
        return paContinue;
    }

    // 填充数据
    while (samplesWritten < totalSamplesToWrite)
    {
        // 当前缓冲区还有数据
        size_t remainingInBuffer = 0;
        if (audio->m_playBufferPos < audio->m_currentPlayBuffer.size())
            remainingInBuffer = audio->m_currentPlayBuffer.size() - audio->m_playBufferPos;

        if (remainingInBuffer > 0)
        {
            size_t samplesToCopy = std::min(totalSamplesToWrite - samplesWritten, remainingInBuffer);
            std::memcpy(out + samplesWritten,
                        audio->m_currentPlayBuffer.data() + audio->m_playBufferPos,
                        samplesToCopy * sizeof(int16_t));
            samplesWritten += samplesToCopy;
            audio->m_playBufferPos += samplesToCopy;
        }
        else
        {
            // 当前缓冲区用完,尝试切换下一个
            std::lock_guard<std::mutex> lock(audio->m_playbackMutex);
            if (!audio->m_playbackQueue.empty())
            {
                audio->m_currentPlayBuffer = std::move(audio->m_playbackQueue.front());
                audio->m_playbackQueue.pop();
                audio->m_playBufferPos = 0;
                continue; // 继续while循环
            }
            else
            {
                // 队列为空,静音填充
                break;
            }
        }
    }
    // 不足部分静音填充
    if (samplesWritten < totalSamplesToWrite)
    {
        std::memset(out + samplesWritten, 0, (totalSamplesToWrite - samplesWritten) * sizeof(int16_t));
        if (audio->m_playbackQueue.empty() &&
            audio->m_playBufferPos >= audio->m_currentPlayBuffer.size())
        {
            audio->onPlaybackFinished();
        }
    }

    // 更新统计信息
    {
        std::lock_guard<std::mutex> lock(audio->m_statsMutex);
        audio->m_stats.playedFrames += audio->m_config.framesPerBuffer;
    }

    return paContinue;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值