这里写自定义目录标题
若包含一个只支持双声道 (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;
}
1万+

被折叠的 条评论
为什么被折叠?



