【PortAudio】PortAudio 音频处理库Demo

音频处理相关概念

https://zhuanlan.zhihu.com/p/91837880

声音的三个要素

  • 响度: 和声音震动的幅度有关
  • 音调:主要和频率有关,声波的频率越高,音调越高
  • 音色: 有声音产生的材质有关,相同的音调和响度下,不同材质发出的声音是不同的。

数字音频

采样频率:对自然界的连续声音信号进行采样,根据奈奎斯特定理在时间轴上对信号进行数字化信号,即按照一定时间间隔 Δ t \Delta t Δt 在模拟信号 KaTeX parse error: Undefined control sequence: \x at position 1: \̲x̲(t) 上逐点采样其瞬时值。采样频率越高,声音的还原程度越高,质量就越好,同时占用空间会越大。

量化:把模拟信号的连续幅度变为有限数量的一定间隔的离散值。位深度(Bit-depth):表示用多少个二进制位来描述采样数据,一般为 16bit。
编码

编码: 是按照一定的规律,把量化后的值用二进制数字表示,然后转化成二值或多值的数字信号流。上面数字化的过程又叫做脉冲编码调制,通常我们说的音频的裸数据格式就是脉冲编码调制(PCM)数据。描述一段 PCM 数据需要几个量化指标,常用的量化指标是采样率,位深度,字节序,声道数。

声道数(channel number):当前 PCM 文件中包含的声道数,是单声道(mono)、双声道

字节序:表示音频 PCM 数据存储的字节序是大端存储(big-endian)还是小端存储(little-endian),为了数据处理效率的高效,通常为小端存储。

音频编码

以CD音质为例,量化格式是2Byte, 采样频率是 44100, 声道是2, 这些信息就描述了CD音质。那么CD的码率为: 44100 * 16 * 2 = 13783125 kbps, 在一分钟的时间里,占用空间 10.09MB

压缩算法包括有损和无损压缩:

  • MP3, MPEG-1 or MPEG-2 Audio Layer III,是曾经非常流行的一种数字音频编码和有损压缩格式 , 它被设计来大幅降低音频数据量 。
  • AAC,Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony 等公司共同开发, 在 1997 年推出的基于 MPEG-2 的音频编码技术。AAC 比 MP3 有更高的压缩比,同样大小的音频文件,AAC 的音质更高。
  • WMA,Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无 损压缩格式。

1. 介绍

PortAudio是一个免费、跨平台、开源的音频I/O库。看到I/O可能就想到了文件,但是PortAudio操作的I/O不是文件,而是音频设备。它能够简化C/C++的音频程序的设计实现,能够运行在Windows、Macintosh OS X和UNIX之上(Linux的各种版本也不在话下)。使用PortAudio可以在不同的平台上迁移应用程序,比如你可以把你基于PortAudio的应用程序发展一个Android版本啊。

PortAudio的API非常简单,通过一个一个简单的回调函数或者阻塞的读/写接口来录制或者播放声音。PortAudio自带了很多示例程序,比如播放正弦波形的音频信号,处理音频输入,录制回放音频,列举音频设备。

本文以一个录音,播放的例子展示,PortAudio的使用流程。完整代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <memory>
#include "portaudio.h"
using namespace std;
// 回调函数定义
static int audioCallback(const void* inputBuffer, void* outputBuffer,
    unsigned long framesPerBuffer,
    const PaStreamCallbackTimeInfo* timeInfo,
    PaStreamCallbackFlags statusFlags,
    void* userData)
{
    
    
    // 在此处处理音频数据
    // 将输入缓冲区中的数据复制到输出缓冲区以回放声音
    if (inputBuffer != NULL)
        memcpy(outputBuffer, inputBuffer, framesPerBuffer * sizeof(float));

    return paContinue;
}

int main()
{
    
    
    PaStream* stream;
    PaError err;

    // 初始化PortAudio库
    err = Pa_Initialize();
    if (err != paNoError) {
    
    
        printf("初始化PortAudio失败: %s\n", Pa_GetErrorText(err));
        return 1;
    }

    // 打开默认的音频输入和输出设备
    err = Pa_OpenDefaultStream(&stream,
        1,      // 输入通道数
        1,      // 输出通道数
        paFloat32,  // 采样格式
        44100,  // 采样率
        256,    // 缓冲区大小(每个缓冲区的帧数)
        audioCallback,  // 回调函数
        NULL);  // 用户数据

    if (err != paNoError) {
    
    
        printf("打开音频流失败: %s\n", Pa_GetErrorText(err));
        return 1;
    }

    // 启动音频流
    err = Pa_StartStream(stream);
    if (err != paNoError) {
    
    
        printf("启动音频流失败: %s\n", Pa_GetErrorText(err));
        return 1;
    }

    printf("录音已开始,请按 Enter 键停止...\n");
    getchar();

    // 停止音频流
    err = Pa_StopStream(stream);
    if (err != paNoError) {
    
    
        printf("停止音频流失败: %s\n", Pa_GetErrorText(err));
        return 1;
    }

    // 关闭音频流和PortAudio库
    err = Pa_CloseStream(stream);
    if (err != paNoError) {
    
    
        printf("关闭音频流失败: %s\n", Pa_GetErrorText(err));
        return 1;
    }

    Pa_Terminate();
    printf("录音已停止。\n");

    return 0;
}

2. 下载安装

2.1 源码编译

源码下载路径为http://www.portaudio.com/download.html
VS上编译的步骤可以参考http://portaudio.com/docs/v19-doxydocs/compile_windows.html
ubuntu下直接使用apt-get install portaudio19-dev即可

2.2 Vcpkg 安装

使用 vcpkg c++ 包管理器安装

 vcpkg install portaudio:x64-windows 

3. 使用流程

编写一个PortAudio应用,只需要掌握回调函数即可:

  1. 编写一个回调函数,PortAudio在进行音频处理的时候自动调用
  2. 初始化PA库,并为I/O打开一个流
  3. 启动流,PA会在幕后调用回调函数
  4. 在回调函数中可以从inputBuffer读取音频数据,或者将音频数据写入到outputBuffer
  5. 回调函数返回1, 或者调用相应函数来停止流
  6. 关闭流,然后终止PA

除了回调函数,PA还支持阻塞I/O模型,但并不是所有的功能都得到支持。所以推荐使用回调函数。

流程图
在这里插入图片描述

3.1 编写回调函数

首先引入PA的头文件

#include "portaudio.h"

回调函数会在两种情况下被调用:PA获取音频数据时和PA需要音频数据作为输出时。

回调函数是一个神奇的地方,因为一些系统在一个特殊的线程中处理回调函数,甚至是通过中断来处理,这不同于程序中的其他代码。如果你想音频能够按时到达Speaker,就得保证回调函数能够快速地执行。不同的平台上,什么 样的操作是安全的,什么样的操作是不安全的,是不一样的。一个通用准则就是,不要做内存的分配释放操作、读写文件、printf,或者其他依赖于OS的不能在一定时间内返回的操作,也包括可能导致上下文切换的操作。

回调函数原型:

typedef int PaStreamCallback( 
	const void *input,
	void *output,
	unsigned long frameCount, 
	const PaStreamCallbackTimeInfo* timeInfo, 
	PaStreamCallbackFlags statusFlags, 
	void *userData );

比如我们想把录音的数据,再播放回来就可以这样写这个回调

// 回调函数定义
static int audioCallback(
	const void* inputBuffer, 
	void* outputBuffer,
    unsigned long framesPerBuffer,
    const PaStreamCallbackTimeInfo* timeInfo,
    PaStreamCallbackFlags statusFlags,
    void* userData)
{
    
    
    // 在此处处理音频数据
    // 将输入缓冲区中的数据复制到输出缓冲区以回放声音
    if (inputBuffer != NULL)
        memcpy(outputBuffer, inputBuffer, framesPerBuffer * sizeof(float));

    return paContinue;
}

如果我们想对声音数据做处理,可以这样写

typedef struct
{
    
    
    float left_phase;
    float right_phase;
}   
paTestData;
/* This routine will be called by the PortAudio engine when audio is needed.
 * It may called at interrupt level on some machines so don't do anything
 * that could mess up the system like calling malloc() or free().
*/ 
static int patestCallback( 
	const void  			*inputBuffer, 
	void 					*outputBuffer,
    unsigned long 			framesPerBuffer,
    const 					PaStreamCallbackTimeInfo* timeInfo,
    PaStreamCallbackFlags 	statusFlags,
    void 					*userData )
{
    
    
    /* Cast data passed through stream to our structure. */
    paTestData *data = (paTestData*)userData; 
    float *out = (float*)outputBuffer;
    unsigned int i;
    (void) inputBuffer; /* Prevent unused variable warning. */
    
    for( i=0; i<framesPerBuffer; i++ )
    {
    
    
         out++ = data->left_phase;  /* left */
         out++ = data->right_phase;  /* right */
        /* Generate simple sawtooth phaser that ranges between -1.0 and 1.0. */
        data->left_phase += 0.01f;
        /* When signal reaches top, drop back down. */
        if( data->left_phase >= 1.0f ) data->left_phase -= 2.0f;
        /* higher pitch so we can distinguish left and right. */
        data->right_phase += 0.03f;
        if( data->right_phase >= 1.0f ) data->right_phase -= 2.0f;
    }
    return 0;
}

3.2 Initializing PortAudio

调用 Pa_Initialize() 这将触发对可用设备的扫描,稍后可以查询这些设备。像大部分PA 函数,都将返回paError信息, 如果不是 paNoError 这个表示有错误产生。

auto err = Pa_Terminate();
if( err != paNoError )
   printf(  "PortAudio error: %s\n", Pa_GetErrorText( err ) );

3.3 Opeing Stream Using Defaults

在这一步将打开一个流,就和打开一个文件一样。你可以指定你想输入/输出音频,多少通道,数据格式,采样率等。打开一个“默认”流意味着打开默认的输入和输出设备,这样可以省去获取设备列表并从列表中选择一个的麻烦。(稍后我们将介绍如何做到这一点。)

#define SAMPLE_RATE (44100)
static paTestData data;
.....
    PaStream *stream;
    PaError err;
    /* Open an audio I/O stream. */
    err = Pa_OpenDefaultStream( &stream,
                                0,          /* no input channels */
                                2,          /* stereo output */
                                paFloat32,  /* 32 bit floating point output */
                                SAMPLE_RATE,
                                256,        /* frames per buffer, i.e. the number
                                                   of sample frames that PortAudio will
                                                   request from the callback. Many apps
                                                   may want to use
                                                   paFramesPerBufferUnspecified, which
                                                   tells PortAudio to pick the best,
                                                   possibly changing, buffer size.*/
                                patestCallback, /* this is your callback function */
                                &data ); /*This is a pointer that will be passed to
                                                   your callback*/
    if( err != paNoError ) goto error;

这里的的 data 对应了 Call back 里面的 userData 参数。

上面的这个示例展示了写的stream, 以满足播放的要求。也可以打开一个用于读取的流,进行录音,或者同时进行读取和写入,以实现同时录制和播放甚至实时音频处理。如果您计划同时进行播放和录制,请只打开一个具有有效输入和输出参数的流。
比如,在文章开头提到得录音,播放得初始化

err = Pa_OpenDefaultStream(&stream,
        1,      // 输入通道数
        1,      // 输出通道数
        paFloat32,  // 采样格式
        44100,  // 采样率
        256,    // 缓冲区大小(每个缓冲区的帧数)
        audioCallback,  // 回调函数
        NULL);  // 用户数据

    if (err != paNoError) {
    
    
        printf("打开音频流失败: %s\n", Pa_GetErrorText(err));
        return 1;
    }

Note:

  • 一些平台得设备可能只读或者只写
  • 尽管多流可以被打开,但是他们很难同步
  • 一些平台的设备不支持打开多流
  • 使用多个流可能没有经过与其他功能一样全面的测试。
  • PortAudio库的调用必须来自同一线程或由用户进行同步。

3.4 Starting, Stoping and Aborting a Stream

PortAudio 当你启动流的将开始播放音频, 当调用 Pa_StartStream() 时, PortAudio 将开始调用你的Callback 函数来执行音频处理。

err = Pa_StartStream( stream );
if( err != paNoError ) goto error;

你可以通过在打开调用时传递的数据结构、全局变量或使用其他进程间通信技术与回调函数进行通信,但请注意,当前台进程最不希望发生中断时可能会调用你的回调函数。因此,避免共享像双向链表这样容易损坏的复杂数据结构,并避免使用诸如互斥锁之类的锁,因为这可能导致你的回调函数阻塞并且丢失音频。这些技术甚至可能在某些平台上导致死锁。

PortAudio将继续调用您的回调函数并处理音频,直到您停止流。这可以通过多种方式完成,但在执行此操作之前,我们希望能看到一些我们的音频经过几秒钟的睡眠进行处理。使用Pa_Sleep()可以轻松实现这一点,许多patests/目录中的示例都是为了这个目的而使用它。请注意,出于各种原因,您不能依赖此函数进行准确的调度,因此您的流可能不会像您期望的那样运行相同的时间,但对于我们的示例来说,这已经足够了。

/* Sleep for several seconds. */
Pa_Sleep(NUM_SECONDS*1000);

现在我们需要停止播放。有几种方法可以做到这一点,其中最简单的方法是调用Pa_StopStream()函数:

err = Pa_StopStream( stream );
if( err != paNoError ) goto error;

Pa_StopStream()函数的设计目的是确保您在回调函数中处理的缓冲区都被播放,这可能会导致一些延迟。或者,您可以调用Pa_AbortStream()函数。在某些平台上,中止流程速度更快,可能会导致部分由回调函数处理的数据不被播放。

停止流的另一种方法是从回调函数返回paComplete或paAbort。paComplete确保最后一个缓冲区被播放,而paAbort尽快停止流。如果您使用此技术停止流程,则需要在再次启动流程之前调用Pa_StopStream()函数。

3.5 Closing a Stream and Terminating PortAudio

当您完成一个流程时,应该关闭它以释放资源:

err = Pa_CloseStream( stream );
if( err != paNoError ) goto error;

在初始化PortAudio时我们已经提到过这一点,但是以防您忘记了,在完成时请确保终止PortAudio:

err = Pa_Terminate( );
if( err != paNoError ) goto error;

4. 参考资料

https://blog.csdn.net/GG_SiMiDa/article/details/77185755
http://files.portaudio.com/docs/v19-doxydocs/terminating_portaudio.html

猜你喜欢

转载自blog.csdn.net/qq_30340349/article/details/131509624
今日推荐