有了播放功能的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下的测试效果

java 的录音文件能压缩的吗_缓存


通过demo录音后的文件如下

java 的录音文件能压缩的吗_字节数_02


然后可以通过>>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下测试效果

java 的录音文件能压缩的吗_字节数_03


java 的录音文件能压缩的吗_缓存_04


java 的录音文件能压缩的吗_linux_05


然后是直接在linux下播放20230207.jlm音频:

java 的录音文件能压缩的吗_java 的录音文件能压缩的吗_06


通过测试能在linux下正常播放,而且录音效果不错。