MP3 格式的录音和编码。
一篇描述从波形音频输入设备录制声音并将其编码为 MP3 格式的技术文章。
引言
你有没有尝试过编写一个程序,用于从声卡录制声音并将其编码为 MP3 格式?不感兴趣?那么,为了让事情更有趣,你有没有尝试过编写一个 MP3 流媒体、互联网广播服务器?我知道你会说“为什么?有像 Icecast 或 SHOUcast 这样优秀且相当标准的实现。”但是,无论如何,你有没有尝试过,至少,深入研究整个过程,或者为自己编写任何类似的东西?嗯,这就是本文的主旨。当然,我们不可能在一篇文章中涵盖所有主题;最终,这可能会让人感到厌烦。所以,我将整个主题分成几篇文章,本文将涵盖录制和编码过程。
背景
显然,每个人遇到的第一个问题是 MP3 编码本身。尝试编写一个能正常工作的程序并不是一件容易的事。所以,我不会深入太多,将停留在 LAME (Sourceforge) 编码器,它被认为是最好的编码器之一(是之一,不是唯一!)。我使用的是版本 3.97);对源代码感兴趣的人可以从 SourceForge 下载(这是一个开源项目)。相关的“lame_enc.dll”也包含在演示项目中(参见本文顶部的链接)。
下一个问题是从声卡录制声音。嗯,运气好的话,在 Google、MSDN 和 CodeProject 上,你可以找到许多与此主题相关的文章。我应该说我使用的是低级波形音频 API(参见 Windows Media Platform SDK,例如,waveInOpen(...)
,mixerOpen(...)
等)。
那么,现在我们来详细了解一下。
MP3 编码
下载包含源代码的“mp3_stream_src.zip”文件(参见本文顶部源代码的链接)。在其中,你应该找到“mp3_simple.h”文件(解压后参见 INCLUDE 文件夹)。它包含 CMP3Simple
类的定义和实现。这个类是 LAME API 的包装器,我试图通过设计它来让生活变得更轻松一些。我尽可能多地注释了代码,希望这些注释足够好。在这一点上我们需要知道的一切
- 实例化
CMP3Simple
对象时,我们需要定义所需的比特率来编码声音样本、声音样本的预期频率,以及(如果需要重新采样)编码声音的所需频率// Constructor of the class accepts only three parameters. // Feel free to add more constructors with different parameters, // if a better customization is necessary. // // nBitRate - says at what bitrate to encode the raw (PCM) sound // (e.g. 16, 32, 40, 48, ... 64, ... 96, ... 128, etc), see // official LAME documentation for accepted values. // // nInputSampleRate - expected input frequency of the raw (PCM) sound // (e.g. 44100, 32000, 22500, etc), see official LAME documentation // for accepted values. // // nOutSampleRate - requested frequency for the encoded/output // (MP3) sound. If equal with zero, then sound is not // re-sampled (nOutSampleRate = nInputSampleRate). CMP3Simple(unsigned int nBitRate, unsigned int nInputSampleRate = 44100, unsigned int nOutSampleRate = 0);
- 编码本身通过
CMP3Simple::Encode(...)
执行。// This method performs encoding. // // pSamples - pointer to the buffer containing raw (PCM) sound to be // encoded. Mind that buffer must be an array of SHORT (16 bits PCM stereo // sound, for mono 8 bits PCM sound better to double every byte to obtain // 16 bits). // // nSamples - number of elements in "pSamples" (SHORT). Not to be confused // with buffer size which represents (usually) volume in bytes. See // also "MaxInBufferSize" method. // // pOutput - pointer to the buffer that will receive encoded (MP3) sound, // here we have bytes already. LAME says that if pOutput is not // cleaned before call, data in pOutput will be mixed with incoming // data from pSamples. // // pdwOutput - pointer to a variable that will receive the // number of bytes written to "pOutput". See also "MaxOutBufferSize" // method. BE_ERR Encode(PSHORT pSamples, DWORD nSamples, PBYTE pOutput, PDWORD pdwOutput);
从声卡录音
同样,解压“mp3_stream_src.zip”文件后,在 INCLUDE 文件夹中,你应该找到“waveIN_simple.h”文件。它包含 CWaveINSimple
、CMixer
和 CMixerLine
类的定义和实现。这些类是波形音频 API 函数子集的包装器。为什么只是子集?因为(我有时很懒),它们只封装与波形输入设备(录音)相关的功能。所以,波形输出设备(播放)没有被捕获(从“开始->运行”中输入“sndvol32 /r”来查看我的意思)。检查我添加到每个类的注释,以便更好地了解它们在做什么。我们目前需要知道的是
- 一个
CWaveINSimple
设备有一个CMixer
,而CMixer
有零个或多个CMixerLine
。 - 所有这些类的构造函数和析构函数都被声明为“
private
”(由于设计原因)。CWaveINSimple
类的对象不能直接实例化,为此声明了CWaveINSimple::GetDevices()
和CWaveINSimple::GetDevice(...)
静态方法。CMixer
类的对象不能直接实例化,为此声明了CWaveINSimple::OpenMixer()
方法。CMixerLine
类的对象不能直接实例化,为此声明了CMixer::GetLines()
和CMixer::GetLine(...)
方法。
- 为了捕获和进一步处理声音数据,一个类必须继承自
IReceiver
抽象类并实现IReceiver::ReceiveBuffer(...)
方法。此外,IReceiver
派生类的实例通过CWaveINSimple::Start(IReceiver *pReceiver)
传递给CWaveINSimple
。// See CWaveINSimple::Start(IReceiver *pReceiver) below. // Instances of any class extending "IReceiver" will be able // to receive raw (PCM) sound from an instance of the CWaveINSimple // and process sound via own implementation of the "ReceiveBuffer" method. class IReceiver { public: virtual void ReceiveBuffer(LPSTR lpData, DWORD dwBytesRecorded) = 0; }; ... class CWaveINSimple { private: ... // This method starts recording sound from the // WaveIN device. Passed object (derivate from // IReceiver) will be responsible for further // processing of the sound data. void _Start(IReceiver *pReceiver); ... public: ... // Wrapper of the _Start() method, for the multithreading // version. This is the actual starter. void Start(IReceiver *pReceiver); ... };
让我们看一些例子。
示例
- 我们如何列出系统中所有的波形输入设备?
const vector<CWaveINSimple*>& wInDevices = CWaveINSimple::GetDevices(); UINT i; for (i = 0; i < wInDevices.size(); i++) { printf("%s\n", wInDevices[i]->GetName()); }
- 我们如何列出波形输入设备的线路(假设
strDeviceName
= 例如,“SoundMAX Digital Audio”)?CWaveINSimple& WaveInDevice = CWaveINSimple::GetDevice(strDeviceName); CHAR szName[MIXER_LONG_NAME_CHARS]; UINT j; try { CMixer& mixer = WaveInDevice.OpenMixer(); const vector<CMixerLine*>& mLines = mixer.GetLines(); for (j = 0; j < mLines.size(); j++) { // Useful when Line has non proper English name ::CharToOem(mLines[j]->GetName(), szName); printf("%s\n", szName); } mixer.Close(); } catch (const char *err) { printf("%s\n",err); }
- 我们到底如何录制和编码 MP3?
首先,我们定义一个类,例如
class mp3Writer: public IReceiver { private: CMP3Simple m_mp3Enc; FILE *f; public: mp3Writer(unsigned int bitrate = 128, unsigned int finalSimpleRate = 0): m_mp3Enc(bitrate, 44100, finalSimpleRate) { f = fopen("music.mp3", "wb"); if (f == NULL) throw "Can't create MP3 file."; }; ~mp3Writer() { fclose(f); }; virtual void ReceiveBuffer(LPSTR lpData, DWORD dwBytesRecorded) { BYTE mp3Out[44100 * 4]; DWORD dwOut; m_mp3Enc.Encode((PSHORT) lpData, dwBytesRecorded/2, mp3Out, &dwOut); fwrite(mp3Out, dwOut, 1, f); }; };
并且(假设
strLineName
= 例如,“麦克风”)try { CWaveINSimple& device = CWaveINSimple::GetDevice(strDeviceName); CMixer& mixer = device.OpenMixer(); CMixerLine& mixerline = mixer.GetLine(strLineName); mixerline.UnMute(); mixerline.SetVolume(0); mixerline.Select(); mixer.Close(); mp3Writer *mp3Wr = new mp3Writer(); device.Start((IReceiver *) mp3Wr); while( !_kbhit() ) ::Sleep(100); device.Stop(); delete mp3Wr; } catch (const char *err) { printf("%s\n",err); } CWaveINSimple::CleanUp();
备注 1
mixerline.SetVolume(0)
是一个相当棘手的地方。对于某些声卡,SetVolume(0)
会给出原始(良好)音质,而对于其他声卡,SetVolume(100)
也会产生相同的效果。但是,你可能会发现有些声卡在SetVolume(15)
时音质最好。我在这里没有好的建议,只能尝试并检查。备注 2
几乎所有声卡都支持“波形输出混音”或“立体声混音”(列表可扩展)混音器线路。从这样的线路录音(
mixerline.Select()
)实际上会录制所有通过声卡波形输出(即“扬声器”)播放的声音。因此,让 WinAmp 或 Windows Media Player 播放一段时间,然后同时启动应用程序录制声音,你就会看到结果。备注 3
而不是调用
mp3Writer *mp3Wr = new mp3Writer();
也可以按如下方式实例化
mp3Writer
的实例(参见上面的类定义)mp3Writer *mp3Wr = new mp3Writer(64, 32000);
这将生成比特率为 64 Kbps、采样率为 32 Khz 的最终 MP3 文件。
关于使用演示应用程序的注释
演示应用程序(参见本文顶部的链接)是一个支持两个命令行选项的控制台应用程序。不带任何命令行选项执行应用程序将简单地打印用法指南,例如
...>mp3_stream.exe mp3_stream.exe -devices Will list WaveIN devices. mp3_stream.exe -device=<device_name> Will list recording lines of the WaveIN <device_name> device. mp3_stream.exe -device=<device_name> -line=<line_name> [-v=<volume>] [-br=<bitrate>] [-sr=<samplerate>] Will record from the <line_name> at the given voice <volume>, output <bitrate> (in Kbps) and output <samplerate> (in Hz). <volume>, <bitrate> and <samplerate> are optional parameters. <volume> - integer value between (0..100), defaults to 0 if not set. <bitrate> - integer value (16, 24, 32, .., 64, etc.), defaults to 128 if not set. <samplerate> - integer value (44100, 32000, 22050, etc.), defaults to 44100 if not set.
使用“-devices”命令行选项执行应用程序将打印当前系统中安装的波形输入设备的名称,例如
...>mp3_stream.exe -devices Realtek AC97 Audio
使用“-device=<device_name>”命令行选项执行应用程序将列出所选波形输入设备的所有线路,例如
...>mp3_stream.exe "-device=Realtek AC97 Audio" Mono Mix Stereo Mix Aux TV Tuner Audio CD Player Line In Microphone Phone Line
最后,当使用以下命令行选项执行时,应用程序将开始从所选的波形输入设备/线路(本例中为麦克风)录制(并编码)声音
...>mp3_stream.exe "-device=Realtek AC97 Audio" -line=Microphone Recording at 128Kbps, 44100Hz from Microphone (Realtek AC97 Audio). Volume 0%. hit <ENTER> to stop ...
录制和编码的声音保存在与您执行应用程序的同一文件夹中的“music.mp3”文件中。
如果您想录制当前正在播放的声音(例如 AVI 电影或视频 DVD 等)通过声卡波形输出,您可以使用以下选项运行应用程序
...>mp3_stream.exe "-device=Realtek AC97 Audio" "-line=Stereo Mix"
然而,这可能只针对我的配置(在上面的“备注 2”中也有解释)。
您可以指定额外的命令行参数,例如
...>mp3_stream.exe "-device=Realtek AC97 Audio" "-line=Stereo Mix" -v=100 -br=32 -sr=32000
这将把线路音量设置为 100%,并将最终的 MP3 以 32 Kbps 和 32 Khz 的速率生成。
结论
在本文中,我涵盖了几个月的时间,我调查了 MP3 编码 API 和录制(实际是捕获)声卡扬声器播放的声音。我将所有这些技术用于实现一个基于互联网的广播电台(MP3 流媒体服务器)。我发现这个主题非常有趣,并决定分享我的一些代码。在我接下来的文章中,我将尝试涵盖与 MP3 流媒体和 IO 完成端口相关的一些方面,但在此之前,我必须清理现有代码,添加注释,并准备文章:)
- 我们如何列出系统中所有的波形输入设备?