JUCE (Jules’ Utility Class Extensions)是由Raw MaterialSoftware发布的一套基于C++的跨平台应用程序框架类库(Windows, Mac,Linux)。JUCE的特殊之处在于其友好的用户界面以及强大的音频、图像处理能力。JUCE适合那些想使用干净、快捷、高层的API,而不想 把时间浪费在使用不同类库,面向不同平台上的开发者。JUCE能够胜任大型、复杂的应用程序(C++)的开发。
与其他应用程序框架类似,JUCE有众多覆盖音频、图像、XML分析、网络等方面的类。JUCE的开发者就是被JAVA的JDK所启发,然后想做出基于C++的对等物。
JUCE最突出的特点是其对音频的特殊支持。JUCE原本是作为Tracktion audiosequencer的一部分而开发的,但后来脱离出来成为了一个独立的工程。JUCE支持音频和MIDI回放,复音合成器,对多种音频格式文件 的读取。同时,JUCE还封装了诸如VST、RTAS、AU等技术的代码,能够开发各种音源、效果插件。众多音频厂商如Imageline、M-AUDIO、cycling74、KORG、Presonus、TC Group等等都是JUCE的使用者。
JUCE 对音频编解码做了很好的支持,我们关心如何利用 JUCE 进行音频文件在读取与写入。这篇文章中,通过丰富的例子来说明如何使用 JUCE 中的编解码工具。
在介绍编解码前,让我们先来看看基本的文件操作。
在 JUCE 中,File 类用来表示一个文件或者目录,它提供了文件相关的基础操作,例如:
exitst()
,判断文件是否存在isDirectory()
,是否为文件夹getFileExtension()
,获取文件扩展名create()
,创建文件deleteFile()
,删除文件createInputStream()
创建可读文件流createOutputStream()
创建可写文件流详细的接口说明请查阅 Class File,下面举个例子,读入 txt 文件并打印
const string path_to_file;
File file(path_to_file);
if(file.existsAsFile()){
auto content = file.loadFileAsString();
cout << content << endl;
}
在 JUCE 中负责解码音频文件类都继承了 AudioFormat
,你可以在 juce_audio_formats/codecs
找到相应的代码,从文件目录来看,在 Windows 下用的是 Windows Media 系统 API,在 Mac/iOS 下用的是 Core Audio,此外还支持以下格式:
AudioFormat 中有几个接口我们需要熟悉
getPossibleSampleRates()
返回该格式支持的采样率getPossibleBitDepths()
返回该格式支持的位深getQualityOptions()
返回一组字符串,用于描述写入音频的质量信息createReaderFor()
创建 AudioFormatReader
用于读取音频数据createWriterFor()
创建 AudioFormatWriter
用于写入音频数据我们可以打印 AudioFormat
的基本信息
void printfAudioFormatInfo(AudioFormat* format)
{
auto format_name = format->getFormatName();
auto possible_extensions = format->getFileExtensions();
auto possible_sample_rates = format->getPossibleSampleRates();
auto possible_bit_depths = format->getPossibleBitDepths();
auto quality_opts = format->getQualityOptions();
cout << "format name:" << format->getFormatName() << endl;
cout << "possible extensions:\n";
for(const auto& item : possible_extensions){
cout << "\t" << item << endl;
}
cout << "possible sample rates:\n";
for(const auto& item : possible_sample_rates){
cout << "\t" << item << endl;
}
cout << "possible bit depth:\n";
for(const auto& item : possible_bit_depths){
cout << "\t" << item << endl;
}
cout << "quality options:\n";
for(const auto& item : quality_opts){
cout << "\t" << item << endl;
}
cout << "is compressed: " << (format->isCompressed() ? "YES" : "NO");
}
WavAudioFormat 负责 wave 读取与写入。它的基本信息包括:
format name:WAV file
possible extensions:
.wav
.bwf
possible sample rates:
8000
11025
12000
16000
22050
32000
44100
48000
88200
96000
176400
192000
352800
384000
possible bit depth:
8
16
24
32
quality options:
is compressed: NO
void readFromAudioFormat(const std::string& path_to_file, AudioFormat* format)
{
File audio_file {path_to_file};
if(audio_file.existsAsFile()){
auto in = audio_file.createInputStream();
AudioFormatReader* reader = format->createReaderFor(in.release(), true);
if(!reader){
cerr << "cannot create AudioFormatReader" << endl;
return;
}
// print wave file information
cout << "num channels:" << reader->numChannels << endl;
cout << "sample rate:" << reader->sampleRate << endl;
cout << "length:" << reader->lengthInSamples << endl;
// read left channel data
const auto num_read = reader->lengthInSamples;
std::vector<float> left_buffer(num_read);
float* data_refer_to[1] = {left_buffer.data()};
reader->read(data_refer_to, 1, 0, num_read);
delete reader;
}
}
WavAudioFormat wav;
readFromAudioFormat("path_to_wave_file", &wav);
在读取 wav 文件时,
File::createInputStream()
创建文件流WavAudioFormat::createReaderFor()
通过文件流创建 AudioFormatReader
,接下来,所有音频文件解码、读取的操作都委托给它。注意一个细节:in.release()
将文件流所有权交给 AudioFormatReader
进行管理,AudioFormatReader
在析构时会同时释放流AudioFormatReader
可以获取音频文件信息,例如采样率、声道数、音频长度等read
方法读取音频数据delete reader
来释放资源,同时关闭文件流对 wav 此类没有压缩的格式,通过一些技术手段进行加速读取速度,其中 mmap
是最方便的。
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
关于 mmap
详细介绍请参考 认真分析mmap:是什么 为什么 怎么用,或者参考 mapread.c 学习如何使用 mmap
。
下面的代码展示了如何用 MemoryMappedReader
读取数据。注意,需要调用 reader->mapEntireFile()
主动进行内存映射。
void readFromAudioFormatMapReader(const std::string& path_to_file, AudioFormat* format)
{
File audio_file {path_to_file};
if(audio_file.existsAsFile()){
auto in = audio_file.createInputStream();
auto* reader = format->createMemoryMappedReader(audio_file);
if(!reader){
cerr << "cannot create AudioFormatReader" << endl;
return;
}
// map entire file into memory
reader->mapEntireFile();
// print wave file information
cout << "num channels:" << reader->numChannels << endl;
cout << "sample rate:" << reader->sampleRate << endl;
cout << "length:" << reader->lengthInSamples << endl;
// read left channel data
const auto num_read = reader->lengthInSamples;
std::vector<float> left_buffer(num_read);
float* data_refer_to[1] = {left_buffer.data()};
reader->read(data_refer_to, 1, 0, num_read);
delete reader;
}
}
与普通的读取性能上有什么区别呢?在读取 4min 音频上,在我的电脑上,两者性能差异约为 25 倍。这提升还是相当可观的,但是对于较短的音频,性能提升就不太明显,例如 10s 音频,提升大概 10% 不到。
在 JUCE 中,可以使用 MemoryMappedReader
进行读取数据的格式只有两种,wav 和后面会提到的 aiff。
void writeByAudioFormat(const std::string& output_path, AudioFormat* format, int bits_per_sample, const StringPairArray& metadataValues, int quality_index)
{
const double sample_rate_out = 44100;
const unsigned int num_channels_out = 2;
File audio_file{output_path};
auto out = audio_file.createOutputStream();
AudioFormatWriter* writer = format->createWriterFor(out.release(),
sample_rate_out,
num_channels_out,
bits_per_sample,
metadataValues,
quality_index);
// write 440 sine wave
const float hz = 440.0f;
const float tincr = 2 * M_PI * hz / sample_rate_out;
const int num_output_frame = 200;
const int num_buffer = 1024;
std::vector<float> left_buffer(num_buffer);
std::vector<float> right_buffer(num_buffer);
float* data_refer_to[2] = {left_buffer.data(), right_buffer.data()};
float t = 0;
for(int i = 0; i < num_output_frame; ++i){
for(int j = 0; j < num_buffer; ++j){
left_buffer[j] = sin(t);
right_buffer[j] = left_buffer[j];
t += tincr;
}
writer->writeFromFloatArrays(data_refer_to, num_channels_out, num_buffer);
}
delete writer;
}
WavAudioFormat wav;
writeByAudioFormat("./sin440.wav", &wav, 16, {}, 0);
在写入 wave 文件时,
File
,并通过 createOutputStream
创建文件的输出流。createWriterFor
创建 AudioFormatWritter
,注意一个细节:out.release()
将文件流所有权交给 AudioFormatWritter
进行管理。AudioFormatWritter
在析构时会同时释放流writeFromFloatArrays
将音频数据写入文件中delete writer
进行释放MP3AudioFormat 负责 MP3 读取。它的基本信息包括:
format name:MP3 file
possible extensions:
.mp3
possible sample rates:
possible bit depth:
quality options:
is compressed: YES
MP3AudioFormat::getPossibleSampleRates
返回空列表,似乎表明它能够支持所有采样率。
想要使用 MP3AudioFormat
需要定义宏 JUCE_USE_MP3AUDIOFORMAT
。具体操作,可以在 Projucer 的工程设置 -> Preprocessor Definitions 中添加 JUCE_USE_MP3AUDIOFORMAT
。
流程与读取 wav 数据一致,不再赘述
MP3AudioFormat mp3;
readFromAudioFormat("path_to_mp3_file", &mp3);
很遗憾,MP3AudioFormat
不支持写入数据,其 createWriterFor
的实现为空
AudioFormatWriter* MP3AudioFormat::createWriterFor (OutputStream*, double,
unsigned int, int
const StringPairArray&, int)
{
jassertfalse; // not yet implemented!
return nullptr;
}
LAMEEncoderAudioFormat 它不能读取 MP3 数据,只能写入。内部实现中,它先将数据写入到 WAVE 文件中,接着通过 lame
程序将 WAVE 转换成 MP3。这操作我也是看傻了,似乎是因为 license 的原因。
它的基本操作包括:
format name:MP3 file
possible extensions:
.mp3
possible sample rates:
32000
44100
48000
possible bit depth:
16
quality options:
VBR quality 0 (best)
VBR quality 1
VBR quality 2
VBR quality 3
VBR quality 4 (normal)
VBR quality 5
VBR quality 6
VBR quality 7
VBR quality 8
VBR quality 9 (smallest)
32 Kb/s CBR
40 Kb/s CBR
48 Kb/s CBR
56 Kb/s CBR
64 Kb/s CBR
80 Kb/s CBR
96 Kb/s CBR
112 Kb/s CBR
128 Kb/s CBR
160 Kb/s CBR
192 Kb/s CBR
224 Kb/s CBR
256 Kb/s CBR
320 Kb/s CBR
is compressed: YES
想要使用 LAMEEncoderAudioFormat
需要定义宏 JUCE_USE_LAME_AUDIO_FORMAT
。
File lame_exe_file{"/usr/local/bin/lame"};
if(lame_exe_file.existsAsFile()){
LAMEEncoderAudioFormat lame(lame_exe_file);
writeByAudioFormat("./sin440.mp3", &lame, 16, {}, 0);
}
使用 LAMEEncoderAudioFormat
需要搭配 lame
使用,在 MacOS 下你可以使用 brew install lame
来安装它。
在使用的过程中,JUCE 打印的 log 可以让你清楚的了解它是如何调用 lame
的,例如:
/usr/local/bin/lame --quiet --vbr-new -V 0 /Users/admin/Library/Caches/LAMEENcoderAudioFormatTest/temp_757173c.wav /Users/admin/Library/Caches/LAMEENcoderAudioFormatTest/temp_a5bd89c.mp3
OggVorbisAudioFormat 负责 ogg 读取与写入。它的基本信息包括:
format name:Ogg-Vorbis file
possible extensions:
.ogg
possible sample rates:
8000
11025
12000
16000
22050
32000
44100
48000
88200
96000
176400
192000
possible bit depth:
32
quality options:
64 kbps
80 kbps
96 kbps
112 kbps
128 kbps
160 kbps
192 kbps
224 kbps
256 kbps
320 kbps
500 kbps
is compressed: YES
想要使用 OggVorbisAudioFormat
需要定义宏 JUCE_USE_OGGVORBIS
。
OggVorbisAudioFormat 的读取与写入流程与 WavAudioFormat 一致,没有其他惊喜,不再赘述。
OggVorbisAudioFormat ogg;
readFromAudioFormat(“path_to_ogg_file.ogg”, &ogg);
writeByAudioFormat("./sin440.ogg", &ogg, 32, {}, 0);
FlacAudioFormat 负责 flac 读取与写入。它的基本信息包括:
format name:FLAC file
possible extensions:
.flac
possible sample rates:
8000
11025
12000
16000
22050
32000
44100
48000
88200
96000
176400
192000
352800
384000
possible bit depth:
16
24
quality options:
0 (Fastest)
1
2
3
4
5 (Default)
6
7
8 (Highest quality)
is compressed: YES
想要使用 FlacAudioFormat
需要定义宏 JUCE_USE_FLAC
。
FlacAudioFormat 的读取与写入流程与 WavAudioFormat 一致,没有其他惊喜,不再赘述。
FlacAudioFormat flac;
readFromAudioFormat("path_to_flac_file.flac", &flac);
writeByAudioFormat("./sin440.flac", &flac, 16, {}, 0);
AiffAudioFormat 负责 aiff 读取与写入。它的基本信息包括:
format name:AIFF file
possible extensions:
.aiff
.aif
possible sample rates:
22050
32000
44100
48000
88200
96000
176400
192000
possible bit depth:
8
16
24
quality options:
is compressed: NO
AiffAudioFormat 的读取与写入流程与 WavAudioFormat 一致,没有其他惊喜,不再赘述。
AiffAudioFormat aiff;
readFromAudioFormat("path_to_aiff_file.aif", &aiff);
writeByAudioFormat("./sin440.aif", &aiff, 16, {}, 0);
CoreAudioFormat 仅在 OSX 和 iOS 系统中可用,它使用 AudioToolbox
框架来读取文件。在笔者的 mac 上,它的基本信息包括:
format name:CoreAudio supported file
possible extensions:
.mp2
.xhe
.amr
.ac3
.loas
.qt
.adts
.mp3
.m4r
.wav
.3gp
.mpa
.sd2
.mpeg
.ec3
.m4a
.mp4
.mov
.snd
.aifc
.caf
.flac
.m4b
.3g2
.mp1
.aac
.aiff
.aif
.au
.latm
possible sample rates:
possible bit depth:
quality options:
is compressed: NO
可以看到 CoreAudioFormat
支持解码的格式非常丰富,但是很遗憾,CoreAudioFormat
不支持写入文件,createWriterFor
实现为空
AudioFormatWriter* CoreAudioFormat::createWriterFor (OutputStream*,
double /*sampleRateToUse*/,
unsigned int /*numberOfChannels*/,
int /*bitsPerSample*/,
const StringPairArray& /*metadataValues*/,
int /*qualityOptionIndex*/)
{
jassertfalse; // not yet implemented!
return nullptr;
}
CoreAudioFormat 的读取与写入流程与 WavAudioFormat 一致,没有其他惊喜,不再赘述。
CoreAudioFormat core_audio;
readFromAudioFormat("path_to_audio_file", &core_audio);
与 CoreAudioFormat 类似,WindowsMediaAudioFormat 仅在 Windows 下可用,它使用 “Windows Media codecs” 来读取文件。由于手头没有 Windows 机器,没法进行测试了。但是通过源码,以及结合上述的 AudioFormat
,聪明的你肯定能够摸清它套路。
同样的,WindowsMediaAudioFormat 只支持读取,不支持写入,其 createWriterFor
实现为空
AudioFormatWriter* WindowsMediaAudioFormat::createWriterFor (OutputStream* /*streamToWriteTo*/, double /*sampleRateToUse*/,
unsigned int /*numberOfChannels*/, int /*bitsPerSample*/,
const StringPairArray& /*metadataValues*/, int /*qualityOptionIndex*/)
{
jassertfalse; // not yet implemented!
return nullptr;
}
通过一张表格总结上述 AudioFormat
类名 | 支持解码 | 支持编码 | 备注 |
---|---|---|---|
WavAudioFormat | ✅ | ✅ | |
MP3AudioFormat | ✅ | ❌ | |
LAMEEncoderAudioFormat | ❌ | ✅ | 需配合 lame 使用 |
OggVorbisAudioFormat | ✅ | ✅ | |
FlacAudioFormat | ✅ | ✅ | |
AiffAudioFormat | ✅ | ✅ | |
CoreAudioFormat | ✅ | ❌ | 仅在 OSX 和 iOS 下使用 |
WindowsMediaAudioFormat | ✅ | ❌ | 仅在 Windows 下使用 |