代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <sdl/SDL.h>
#include <fstream>

#pragma comment(lib, "SDL2.lib")

#undef main

using namespace std;

// =======(四)=======
void AudioCallBack(void* userdata, Uint8* stream, int len)
{
cout << "AudioCallBack" << endl;
SDL_memset(stream, 0, len);
auto ifs = (ifstream *)userdata;
ifs->read((char*)stream, len);
if (ifs->gcount() <= 0)
{
cout << "end" << endl;
SDL_PauseAudio(1); // 暂停播放
}
}

int main(int argc, char *argv[])
{
// 初始化SDL音频
SDL_Init(SDL_INIT_AUDIO);

// =======(一)=======
// 打开音频设备
SDL_AudioSpec spec;
spec.freq = 44100; // 音频一秒采样率
spec.format = AUDIO_S16SYS; // 音频样本类型,使用系统字节序
spec.channels = 2; // 音频通道数(双声道,立体声)
spec.silence = 0; // 静音的值
spec.samples = 1024; // 样本数量,2的n次方,
// 用于分割平面模式的多通道数据
// 例如 samples = 8
// LLLL RRRR
spec.callback = AudioCallBack; // 音频播放回调函数
ifstream ifs("test_pcm.pcm", ios::binary);

spec.userdata = &ifs;


// =======(二)=======
if (SDL_OpenAudio(&spec, nullptr) < 0)
{
cerr << "SDL_OpenAudio failed!" << SDL_GetError() << endl;
return -1;
}

// =======(三)=======
SDL_PauseAudio(0); // 开始播放

getchar();

SDL_QuitSubSystem(SDL_INIT_AUDIO);

return 0;
}

(一)

SDL_AudioSpec 是 SDL 库中的一个结构体,用于描述音频设备的配置和格式。它包含了音频播放和录制所需的各种参数,包括采样率、音频格式、通道数、样本大小、回调函数等。

以下是 SDL_AudioSpec 结构体的主要成员及其解释:

  1. int freq:音频采样率,表示每秒钟播放或录制的样本数。常见的值是 44100(CD 音质)、48000(DVD 音质)等。

    • 代码中示例:spec.freq = 44100;
  2. SDL_AudioFormat format:音频格式,用于指定每个音频样本的格式(例如 8 位、16 位等)和字节序。常用的值有 AUDIO_U8(无符号 8 位 PCM 数据)、AUDIO_S16SYS(有符号 16 位 PCM 数据,系统字节序)等。

    • 代码中示例:spec.format = AUDIO_S16SYS;
  3. Uint8 channels:音频通道数,1 表示单声道(Mono),2 表示立体声(Stereo)。

    • 代码中示例:spec.channels = 2;
  4. Uint8 silence:静音值。当音频缓冲区为空时,SDL 用于填充静音的值。对于 8 位音频格式,这通常是 128,对于 16 位音频格式,这通常是 0。

    • 代码中示例:spec.silence = 0;
  5. Uint16 samples:每个音频缓冲区的样本数,通常是 2 的幂。这个值决定了音频回调函数的调用频率,值越大,延迟越高,值越小,延迟越低,但会增加回调的调用频率。

    • 代码中示例:spec.samples = 1024;
  6. SDL_AudioCallback callback:音频回调函数指针。当音频设备需要更多数据时,SDL 会调用这个回调函数,以便用户提供数据。

    • 代码中示例:spec.callback = AudioCallBack;
  • 类似于Qt 中的 TimerEvent,会每时每刻调用
  1. void *userdata:用户数据指针。在回调函数中传递给用户的自定义数据,用户可以用它来传递上下文信息或者其他所需的数据。
    • 代码中示例:spec.userdata = &ifs;,其中 ifs 是指向输入文件流的指针。

SDL_AudioSpec 结构体的这些成员允许开发者自定义音频设备的行为,使得 SDL 可以灵活地支持各种音频硬件和格式。通过适当配置这些参数,可以确保音频的播放和录制与期望的音频质量和性能相符。


关于PCM 8位和16位的区别

8位和16位 PCM 数据的主要区别在于 量化精度,即每个样本使用多少位来表示声音的振幅。

  1. 8位 PCM 数据
    • 使用 8 位(1 字节)来表示每个音频样本。
    • 由于每个样本只有 8 位,量化的数值范围是 0 到 255(无符号)或 -128 到 127(有符号)。
    • 8 位音频的精度较低,噪声较大,音质相对较差,通常用于简单的声音效果或需要降低数据量的场景。
    • 文件大小相对较小,因为每个样本的字节数较少。
  2. 16位 PCM 数据
    • 使用 16 位(2 字节)来表示每个音频样本。
    • 量化的数值范围是 -32768 到 32767(有符号整数),这提供了更大的动态范围和更高的音频精度。
    • 16 位音频的音质明显优于 8 位音频,因为它能更精确地表示声音的细节和动态变化,通常用于高质量的音频文件,如音频 CD(44.1 kHz, 16-bit PCM)。
    • 文件大小较大,因为每个样本需要 2 个字节。

总结

  • 8位 PCM:音质较低,适合用于简单音效或数据传输速率较低的场景。
  • 16位 PCM:音质较高,适合用于音乐、视频和其他对音质有较高要求的场合。

(二)

SDL_OpenAudio 是 SDL(Simple DirectMedia Layer)库中的一个函数,用于打开音频设备并进行音频播放的初始化。这个函数配置音频硬件的播放参数,并分配资源,使音频设备能够开始工作。

SDL_OpenAudio 函数的作用

  1. 配置音频设备:它使用一个 SDL_AudioSpec 结构体来设置音频设备的参数,比如采样率、音频格式、通道数和缓冲区大小等。
  2. 初始化音频设备:根据提供的配置参数,SDL 会初始化音频硬件或虚拟音频设备,使其准备好播放音频。
  3. 注册回调函数:在初始化过程中,SDL 注册一个回调函数(SDL_AudioSpec 中的 callback 成员),当音频设备需要新的音频数据时,SDL 会调用这个回调函数。

函数签名

1
int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);

参数说明

  1. desired(指针):指向 SDL_AudioSpec 结构体,该结构体描述了你希望音频设备采用的音频参数(如采样率、格式、通道数、缓冲区大小等)。
    • 你在这个结构体中指定你所期望的音频设置,包括回调函数。
    • 这是一个输入参数,SDL 使用这些信息来尝试匹配最接近的硬件配置。
  2. obtained(指针):指向一个 SDL_AudioSpec 结构体,用于接收实际音频设备支持的参数。
    • 如果你不关心实际的音频参数,可以传入 nullptr
    • 这是一个输出参数,SDL 将音频设备实际使用的参数写入该结构体中。通常用于检测音频设备的实际配置与期望配置之间的差异。

(三)

关于callback函数什么时候调用

  1. 初始化 SDL 音频子系统SDL_Init(SDL_INIT_AUDIO);)。
  2. 配置并打开音频设备SDL_OpenAudio(&spec, nullptr);)。
  3. 开始音频播放SDL_PauseAudio(0);):这是启动音频播放的关键点。此时,SDL 开始工作,音频设备开始请求数据,触发 AudioCallBack 回调函数。

(四)

这个回调函数 AudioCallBack 是在 SDL 音频设备需要新的音频数据时调用的。它的主要任务是从音频文件中读取数据并填充到音频缓冲区(stream),以确保音频播放的连续性。以下是这个回调函数的详细解释:

函数参数解释

1
void AudioCallBack(void* userdata, Uint8* stream, int len)
  • void* userdata:用户自定义的数据指针。在设置音频设备时(SDL_AudioSpecuserdata 成员),你可以传递任何需要在回调函数中使用的数据。在这个例子中,它是一个指向 ifstream 的指针,用于读取音频文件数据。
  • Uint8* stream:指向音频缓冲区的指针。这个缓冲区是 SDL 用来播放音频的。当 SDL 需要更多数据时,它会调用回调函数并传递这个缓冲区指针。回调函数的任务是将新的音频数据填充到这个缓冲区。
  • int len:需要填充到缓冲区 stream 中的音频数据长度(字节数)。这个长度由 SDL 提供,通常由 SDL_AudioSpec 结构体中的 samples 字段决定。

函数执行流程

  1. 清零音频缓冲区

    1
    SDL_memset(stream, 0, len);

    SDL_memset 函数将音频缓冲区 stream 的所有字节设置为 0。这个步骤是为了确保缓冲区在填充新数据之前是清零的,防止之前的数据残留。对于一些应用场景,这可以避免噪声或不期望的声音出现。

  2. 读取音频数据

    1
    2
    auto ifs = (ifstream *)userdata;
    ifs->read((char*)stream, len);
    • auto ifs = (ifstream *)userdata;:将 userdata 转换为 ifstream 指针。这里 userdata 是在初始化 SDL_AudioSpec 时传入的,指向一个打开的音频文件流(ifstream)。
    • ifs->read((char*)stream, len);:从音频文件中读取 len 字节的数据并填充到 stream 缓冲区。这个操作将音频文件中的 PCM 数据读入到 SDL 的播放缓冲区中。
  3. 检查文件是否读完

    1
    2
    3
    4
    5
    if (ifs->gcount() <= 0)
    {
    cout << "end" << endl;
    SDL_PauseAudio(1); // 暂停播放
    }
    • ifs->gcount():返回 read 操作实际读取的字节数。如果返回值小于或等于 0,表示文件已经读完或者没有更多数据可以读取。
    • 如果文件读取完毕(ifs->gcount() <= 0),打印 “end” 表示音频文件已结束,然后调用 SDL_PauseAudio(1); 暂停音频播放。暂停播放是为了避免播放空的缓冲区,产生噪音或无效的音频输出。

总结

  • 回调函数的主要任务是从音频文件读取数据,并填充 SDL 的音频缓冲区,确保音频的连续播放。
  • userdata 用于传递自定义数据(如音频文件流),以便在回调函数中使用。
  • 当音频数据用尽时,回调函数暂停音频播放,防止播放空数据。

这确保了音频播放的稳定性和数据的准确性。

(五)

播放过程的详细解释

  1. 音频设备开始工作:调用 SDL_PauseAudio(0) 后,SDL 的音频子系统开始工作,音频设备会开始播放缓冲区中的数据。
  2. 音频数据的填充:当音频设备播放时,它会消耗缓冲区中的音频数据。当缓冲区中剩余的数据不足时,SDL 会调用你在 SDL_AudioSpec 中指定的回调函数 AudioCallBack
  3. 音频数据的提供AudioCallBack 函数从音频文件中读取新的音频数据并填充到音频缓冲区中。这些数据就是音频设备播放的内容。
  4. 持续播放:只要有新的音频数据可用,并且音频设备没有被暂停(调用 SDL_PauseAudio(1)),音频设备就会持续播放数据。

总结

声音的实际播放是由 SDL_PauseAudio(0) 启动的,它解除音频设备的暂停状态,让音频设备开始请求和播放数据。回调函数 AudioCallBack 在音频数据不足时被调用,用于填充更多的音频数据,从而实现连续播放。