有了播放功能的demo,接下来开源基于JLM音频格式的录音功能的源码。同样提供在windows和linux下的SDK调用方法,录音没有限制文件的后缀名必须是.jlm,因为在录音后的表示信息中包括了“JLMA”的标识信息,所以录音的后缀可以自定义。
代码可能写的不好,但是具有一定的指导意义。
详细函数说明和相关的demo代码以及测试样例请点击下载。
一、windows下JLM格式的录音
1、windows下通过winmm.lib获取麦克风的PCM数据,重点是需要根据JLM格式设定压缩质量、预测窗口以及一些核心的格式参数,比如采样率、位深、分段加密的段大小等参数:
JLM_FORMAT format; // JLM格式,也是音频采集的信息
format.bitsPerSample = 16; // 每个采样值的位深,仅支持8位,16位和24位三种位深参数
format.format = WAVE_FORMAT_PCM; // 默认值为1
format.samplesPerSec = 16000; // 采样率,每秒采样值的个数
format.channels = 1; // 声道数,支持多个声道,一般是1和2
format.byteAlign = 2; // 两个字节为一个联合(左右声道的交织封装),channels * bitsPerSample / 8
format.avgBytesPerSec = 32000; // 每秒字节数(标准播放速度),avgBytesPerSec = samplesPerSec * byteAlign
format.blockSize = 16000; // 设定为每秒一个编码帧(blockSize指采样值的个数,不是字节数),主要是用来设定分段编码(加密)的块大小
format.pcmByteLength = 0; // 初始PCM字节数为0
format.predictsWindowSize = 1; // 设定线性预测窗口大小为1,可以设定的更大,根据不同的音频类型来设定
2、基于vs2019控制台的录音源码
#include "JLMAudioCompression.h"
#include <stdio.h>
#include <Windows.h>
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")
// 定义一个全局实例对象
static JLMAudio jlm;
// 状态,1表示录音,0表示结束
static int status = 0;
static const char* jlm_file_path = nullptr;
static const char* password = nullptr;
static int errsign = 0;
// 音频采集的回调函数,回调的过程中同步完成当前采集数据的编码和保存
void CALLBACK AudioRecodeToJLM(HWAVEIN hwi, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
{
LPWAVEHDR pwh = (LPWAVEHDR)dwParam1;
if (status && WIM_DATA == uMsg) {
// 通过JLM_AUDIO_FILE_REC_ONE_FRAME函数写入文档中
// 因为需要在回调函数中调用JLM_AUDIO_FILE_REC_ONE_FRAME,所以jlm,password,errsign本案例中采用了全局变量
jlm.JLM_AUDIO_FILE_REC_ONE_FRAME((unsigned char*)pwh->lpData, password, &errsign);
// 出现错误则关闭
if (errsign > 0) status = 0;
// 继续添加缓存
waveInAddBuffer(hwi, pwh, sizeof(WAVEHDR));
}
else {
// 等待10毫秒
Sleep(10);
}
}
int main()
{
char pass_str[2000], jlmpath_str[2000];
JLM_FORMAT format; // JLM格式,也是音频采集的信息
WAVEHDR wHdr1[4]; // 采集音频时包括数据缓存的结构体
HWAVEIN hWaveIn; // 输入设备句柄
WAVEFORMATEX waveform; // 采集音频的格式,结构体
int i, err, c_status = 0, q = 0;
// 初始化为0
for (i = 0; i < 2000; ++i) {
jlmpath_str[i] = 0x00;
pass_str[i] = 0x00;
}
hWaveIn = NULL; // 录音设备句柄初始化
// 开始定义format
format.bitsPerSample = 16;
format.format = WAVE_FORMAT_PCM;
format.samplesPerSec = 16000; // 采样率
format.channels = 1; // 声道数
format.byteAlign = 2; // 两个字节为一个联合,channels * bitsPerSample / 8
format.avgBytesPerSec = 32000; // 每秒字节数,avgBytesPerSec = samplesPerSec * byteAlign
format.blockSize = 16000; // 设定为每秒一个编码帧
format.pcmByteLength = 0; // 初始PCM字节数为0
format.predictsWindowSize = 1; // 设定线性预测窗口大小为1
// 设定Windows下的录音参数,注意部分参数必须和format保持一致
waveform.wFormatTag = format.format; // 声音格式为PCM
waveform.nSamplesPerSec = format.samplesPerSec; // 采样率,16000
waveform.wBitsPerSample = format.bitsPerSample; // 采样比特(即采样深度),16bits
waveform.nChannels = format.channels; // 采样声道数,2声道
waveform.nAvgBytesPerSec = format.avgBytesPerSec; // 每秒的数据率,就是每秒能采集多少字节的数据
waveform.nBlockAlign = format.byteAlign; // 一个块的大小,采样bit的字节数*声道数
waveform.cbSize = 0; // 一般为0
printf("请输入JLM录音文件存储路径:\n");
err = scanf_s("%s", jlmpath_str);
printf("请输入加密密码:\n");
err = scanf_s("%s", pass_str);
printf("请输入压缩质量(0无损,1高质量,2次高质量):\n");
err = scanf_s("%d", &q);
// 将char[]转为const char*
password = pass_str;
jlm_file_path = jlmpath_str;
if (q <= 0) {
q = 0;
}
else if (q >= 2) {
q = 2;
}
// 通过输入质量设置
format.quality = q; // 采用0无损,1高质量,2次高质量
// 初始化一个JLM文件供录音数据编码加密后保存
jlm.JLM_AUDIO_FILE_REC(jlm_file_path, &format, NULL, &errsign);
// 开启音频采集,通过AudioRecodeProc回调函数把采集好的数据存储到JLM文件中
waveInOpen(&hWaveIn, WAVE_MAPPER, &waveform, (DWORD_PTR)AudioRecodeToJLM, NULL, CALLBACK_FUNCTION);
// 采用4个缓存数组用以缓存音频数据
for (i = 0; i < 4; ++i) {
wHdr1[i].lpData = new char[format.avgBytesPerSec]; // 以秒为采样块,所以这里开辟的缓存大小为format.avgBytesPerSec
wHdr1[i].dwBufferLength = format.avgBytesPerSec; // 这个是设定缓存字节数
wHdr1[i].dwBytesRecorded = 0;
wHdr1[i].dwUser = NULL;
wHdr1[i].dwFlags = 0;
wHdr1[i].dwLoops = 1;
wHdr1[i].lpNext = NULL;
wHdr1[i].reserved = 0;
//为录音设备准备缓存函数(为音频输入设备准备一个缓冲区):
waveInPrepareHeader(hWaveIn, &wHdr1[i], sizeof(WAVEHDR));
//给输入设备增加一个缓存(将缓冲区发送给设备,若缓冲区填满,则不起作用):
waveInAddBuffer(hWaveIn, &wHdr1[i], sizeof(WAVEHDR));
}
// 开始根据命令启动和结束录音
SELECT_STATUS:
if (status == 0) {
printf("输入1启动录音:\n");
err = scanf_s("%d", &c_status);
if (c_status >= 1) {
status = 1;
// 开始录音
waveInStart(hWaveIn);
// 返回SELECT_STATUS
goto SELECT_STATUS;
}
}
else {
printf("输入0结束录音:\n");
err = scanf_s("%d", &c_status);
if (c_status == 0) {
status = 0;
// 结束录音
waveInClose(hWaveIn);
// 结束录音的同时,也结束JLM的编码
jlm.JLM_AUDIO_FILE_REC_END(password, &errsign);
printf("\n录音完毕\n");
}
}
system("pause");
return 0;
}
3、windows下的测试效果
通过demo录音后的文件如下
然后可以通过>>JLM播放功能<<进行播放,注意播放时密码必须正确!否则将解码失败。
二、linux下JLM格式的录音
1、linux下的录音也需要通过调用alsa相关的库进行,但是本文中linux下的demo的录音demo和windows下的不同,采用先设定要录音的时长,然后再启动录音,因为我没有花太多的时间去弄清楚alsa回调的方法,所以这个问题供大家去研究了。
2、linux下基于alsa录音生成JLM格式的源码
#include "JLMAudioCompression.h"
#include <stdio.h>
#include <alsa/asoundlib.h>
int main()
{
char pass_str[2000], jlmpath_str[2000];
const char* jlm_file_path = nullptr;
const char* password = nullptr;
int errsign = 0;
JLM_FORMAT format; // JLM格式,也是音频采集的信息
JLMAudio jlm;
snd_pcm_t *handle;
snd_pcm_hw_params_t *params;
snd_pcm_uframes_t frames;
int rc, dir;
int i, err, q = 0;
unsigned char* PCM_DATA = NULL;
unsigned int buffer_time = 1000000, status = 0, c_status = 0;
// 初始化为0
for (i = 0; i < 2000; ++i) {
jlmpath_str[i] = 0x00;
pass_str[i] = 0x00;
}
// 开始定义format
format.bitsPerSample = 16;
format.format = 1;
format.samplesPerSec = 16000; // 采样率
format.channels = 1; // 声道数
format.byteAlign = 2; // 两个字节为一个联合,channels * bitsPerSample / 8
format.avgBytesPerSec = 32000; // 每秒字节数,avgBytesPerSec = samplesPerSec * byteAlign
format.blockSize = 16000; // 设定为每秒一个编码帧
format.pcmByteLength = 0; // 初始PCM字节数为0
format.predictsWindowSize = 1; // 设定线性预测窗口大小为1
printf("Please enter the JLM recording file storage path:\n");
err = scanf("%s", jlmpath_str);
printf("Please enter an encrypted password:\n");
err = scanf("%s", pass_str);
printf("Please enter the compression quality (0 nondestructive, 1 high quality, 2 little worse high quality):\n");
err = scanf("%d", &q);
// 将char[]转为const char*
password = pass_str;
jlm_file_path = jlmpath_str;
if (q <= 0) {
q = 0;
}
else if (q >= 2) {
q = 2;
}
// 通过输入质量设置
format.quality = q; // 采用0无损,1高质量,2次高质量
// 初始化一个JLM文件供录音数据编码加密后保存
jlm.JLM_AUDIO_FILE_REC(jlm_file_path, &format, NULL, &errsign);
// 开辟缓存接受数据
PCM_DATA = (unsigned char*)malloc(sizeof(unsigned char) * format.avgBytesPerSec);
// 采用“default”能有效支持自定义帧大小,播放模式SND_PCM_STREAM_PLAYBACK,录音模式SND_PCM_STREAM_CAPTURE
rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_CAPTURE, 0);
if (rc < 0) {
printf("Unable to start the pcm drive!\n");
if (PCM_DATA) free(PCM_DATA);
jlm.JLM_AUDIO_FILE_REC_END(password, &errsign);
exit(1);
}
// 分配一个硬件参数对象
snd_pcm_hw_params_alloca(¶ms);
// 用默认值填入 params
snd_pcm_hw_params_any(handle, params);
// 交错模式
snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
// 根据位深选择不同的模式
if(format.bitsPerSample == 8) snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_U8);
else if(format.bitsPerSample == 16) snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
else if(format.bitsPerSample == 24) snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S24_LE);
// 设置声道数,按照format.channels设置
snd_pcm_hw_params_set_channels(handle, params, format.channels);
// 设置采样率,按照format.samplesPerSec设置
snd_pcm_hw_params_set_rate_near(handle,params,&format.samplesPerSec,&dir);
// 设置帧字节数,按照format.avgBytesPerSec来设置,注意此处必须换算为采样值的个数
if(format.bitsPerSample == 8) frames = format.avgBytesPerSec / format.channels;
else if(format.bitsPerSample == 16) frames = format.avgBytesPerSec / format.channels / 2;
else if(format.bitsPerSample == 24) frames = format.avgBytesPerSec / format.channels / 3;
//设置缓冲时间,buffer_time单位为us,1000000us=1s
rc = snd_pcm_hw_params_set_buffer_time_near(handle, params, &buffer_time, 0);
// 把设置好的参数回填设备
rc = snd_pcm_hw_params(handle, params);
if (rc < 0) {
printf("params is err!\n");
if (PCM_DATA) free(PCM_DATA);
jlm.JLM_AUDIO_FILE_REC_END(password, &errsign);
exit(1);
}
// 设置录音的秒数
printf("Set the number of seconds for the recording:\n");
err = scanf("%d", &c_status);
status = 0;
// 开始录音
while(status < c_status)
{
rc = snd_pcm_readi(handle, PCM_DATA, frames);
if (rc == -EPIPE) {
printf("underrun occurred");
snd_pcm_prepare(handle);
} else if (rc < 0) {
printf("error from writei: %s", snd_strerror(rc));
} else if (rc != (int)frames) {
printf("short write, write %d frames", rc);
}
// 然后把PCM_DATA数据保存到JLM文件中
jlm.JLM_AUDIO_FILE_REC_ONE_FRAME(PCM_DATA, password, &errsign);
status ++;
}
// 关闭录音设备
snd_pcm_drain(handle);
snd_pcm_close(handle);
// 结束录音的同时,也结束JLM的编码
jlm.JLM_AUDIO_FILE_REC_END(password, &errsign);
printf("\nREC end!\n");
if (PCM_DATA) free(PCM_DATA);
return 0;
}
3、ubuntu linux下测试效果
然后是直接在linux下播放20230207.jlm音频:
通过测试能在linux下正常播放,而且录音效果不错。