学习笔记-C语言关于waveout接口的使用(终?)
好,介绍下这段时间写的东西,从何讲起呢?
从最直观的开始吧,文件数量,对文件数量,这个项目共有5
个文件,分别是
WaveOut.c
file_info.h
onWindows.h
file_info.c
onWindows.c
这五个文件各有神通,支撑起了本项目主要的两个功能,一个是将任意位深的音乐以16bit播放,另一个是将任意位深的音乐以16bit文件的形式输出。头文件中装的是对C文件的函数声明,没什么好讲的,就先从主函数所在的文件WaveOut.c
开始介绍吧
1 WaveOut.c
WaveOut.c
中主要包含了主函数main
和监听作用的线程函数Listening
。其实一开始是打算在这个文件中增加个中间层,为后续移植做准备,但由于完全没这方面的概念,就不了了之了,但还是保留了一部分内容,所以WaveOut.c
开头为:
#ifdef _WIN32
#include <windows.h> // Windows平台的头文件和定义
#include <mmsystem.h>
#include "onWindows.h"
#pragma comment(lib,"Winmm.lib")
#else
#include <alsa/asoundlib.h>
#endif
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include "file_info.h"
这里会对运行的系统环境进行判断然后选择合适的头文件。其中file_info.h
和onWindows.h
后续会提到,这里暂且按下不表。我们讲讲最开始运行的函数——主函数main
。
1.1 main
主函数main
的功能为:
- 创建线程函数
- 提供选择不同功能的选项
main
的内容为:
int main()
{
HANDLE pthread; //线程的句柄
int num = 0;
if ((pthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Listening, 0, 0, NULL)) == 0)
printf("线程建立失败\n");
while (1)
{
num = 0;
printf("\n1、将音乐转成16位并输出为文件\n2、播放音乐\nQ/ESC 退出\n");
printf("\n选择操作: ");
kb = 0; //重置并监听输入
while (kb < 48 || kb >50)
{
if (back_flag == 1)break;
Sleep(100);
}
if (kb == '1')
{
system("CLS");
printf("\n将音乐转成16位并输出为文件\n\n");
Output16bitFile(16);
}
else if (kb == '2')
{
system("CLS");
printf("\n播放音乐\n");
WaveOnWin();
}
else if (back_flag == 1)
{
printf("退出程序\n");
break;
}
}
if (pthread != 0)CloseHandle(pthread);
return 0;
}
1.2 Listening
而线程函数Listening
的内容就多了,除了监听键盘输入kb
外,还有一些在非线程函数中不方便进行的判断,主要为
- 任何模式下
- 按
Q/ESC
键时将返回标识符back_flag
置1,表示返回到上个界面 - 按
X
可以改变音乐播放顺序,即loop_mode
- 按
W/↑
、S/↓
键时可以控制播放音量vol
- 按
- 音乐播放模式下(当
device != NULL
时)- 按
空格
键时可以暂停/继续播放音乐 - 暂停播放时按下任意控制音乐的按键解除暂停,例如
Q,A,Z
等
- 按
线程函数Listening
的代码为:
int back_flag = 0; //返回标识符,当为1时程序从当前界面返回
int loop_mode; //播放时的循环方向
int music_switch; //判断切歌方向,1切下一首,-1切上一首
HWAVEOUT device; //设备的句柄
char kb; //键盘输入
//用来监听键盘输入的线程,若有符合要求的按键输入
DWORD WINAPI Listening(lpParam)
{
int music_pause = 0; //暂停标识符
unsigned int vol = 0x7FFF7FFF;
waveOutGetVolume(device, &vol);
while (1)
{
kb = _getch(); //非阻塞无回显获取键盘输入
//如果对音乐进行操作,例如切歌,判断是否处于暂停中,若是,则取消暂停
if (kb == 'd' || kb == 'D' || kb == 77 ||
kb == 'a' || kb == 'A' || kb == 75 ||
kb == 'q' || kb == 'Q' || kb == 27 ||
kb == 'z' || kb == 'Z' ||
kb == 'c' || kb == 'C' ||
(kb > 48 && kb < 53))
{
if(music_pause = 1 && device != NULL)
{
waveOutRestart(device);
music_pause = 0;
}
}
else if (kb == 'w' || kb == 'W' || kb == 72) // '↑',增加音量
{
if (vol + 0x19991999 < vol)
{
printf("音量已达最大值 ");
vol = 0xFFFFFFFF;
}
else vol += 0x19991999;
printf("音量 : %d\n", (vol >> 16) * 100 / 0xFFFF );
waveOutSetVolume(device, vol);
}
else if (kb == 's' || kb == 'S' || kb == 80) // '↓',降低音量
{
if (vol - 0x19991999 > vol)
{
printf("已静音 ");
vol = 0x00000000;
}
else vol -= 0x19991999;
printf("音量 : %d\n", (vol >> 16) * 100 / 0xFFFF);
waveOutSetVolume(device, vol);
}
else if (kb == ' ' && device != NULL) // ' '暂停/播放音乐
{
if (music_pause == 0)
{
waveOutPause(device);
printf("PAUSE\n");
music_pause = 1;
}
else
{
waveOutRestart(device);
printf("RESTART\n");
music_pause = 0;
}
}
else if (kb == 'x' || kb == 'X') //设置循环模式
{
if (device != NULL)
{
printf("当前循环模式为:");
if (loop_mode == -1)
{
printf("单曲循环\n");
music_switch = 0;
loop_mode = 0;
}
else if (loop_mode == 0)
{
printf("顺序循环\n");
music_switch = 1;
loop_mode = 1;
}
else
{
printf("倒序循环\n");
music_switch = -1;
loop_mode = -1;
}
}
}
//退出程序,虽然目前只用来退出播放音乐和输出文件,但相信未来肯定大有用处
if (kb == 'q' || kb == 'Q' || kb == 27)
{
back_flag = 1;
}
}
}
代码中有个变量music_swtich
,你可能会想不是有个loop_mode
来控制切歌方向了吗?没错,我也是那么想的,但是这样按左右键切歌就会无比麻烦,所以只要把music_swtich
当做是由左右键切歌和播放顺序loop_mode
共同控制就好了。
2 file_info.c
好,介绍完了WaveOut.c
,现在来讲file_info.c
,不过在此之前,要先讲讲对应头文件file_info.h
中定义的一个结构体,该结构体作用是存放.wav
格式文件的文件头信息,这个wav结构体后面会频繁用到,代码为:
struct
{
unsigned char identification[5]; //头文件文档标识
unsigned int file_size; //该数据+8为文件大小
unsigned char format[5]; //文件类型
unsigned char fmt[5]; //格式块标识
unsigned int fmt_size; //格式块长度
unsigned short ecodeing_format; //编码格式
unsigned short channel; //声道数量
unsigned int Sampling_frequency; //采样频率
unsigned int transfer_speed; //数据传输速度
unsigned short Sample_size; //采样帧大小
unsigned short bit_depth; //采样位深
unsigned int music_size; //音乐大小
}wav;
既然有了这个结构体存放文件头,那么肯定就需要函数来使用这个结构体了,接下来堂堂登场的是函数File_Info
!
2.1 File_Info(char filename[])
File_Info
函数的作用就是读取.wav
文件的文件头,然后存进上面提到的结构体当中,同时打印一部分较为有用的信息,代码为:
//显示文件信息
int File_Info(char filename[])
{
FILE* fp = fopen(filename, "rb"); //只读打开文件
if (fp == NULL)
{
printf("file open fail\n");
return 0;
}
//读取文件头文件数据
fread(wav.identification, 4,1, fp);
fread(&wav.file_size, 4, 1, fp);
fread(wav.format, 4,1, fp);
fread(wav.fmt, 4, 1,fp);
fread(&wav.fmt_size, 4, 1, fp);
fread(&wav.ecodeing_format, 2, 1, fp);
fread(&wav.channel, 2, 1, fp);
fread(&wav.Sampling_frequency, 4, 1, fp);
fread(&wav.transfer_speed, 4, 1, fp);
fread(&wav.Sample_size, 2, 1, fp);
fread(&wav.bit_depth, 2, 1, fp);
fseek(fp, 40, SEEK_SET);
fread(&wav.music_size, 4, 1, fp);
//输出文件信息
printf("\n音乐名称 : %s\n", filename);
printf("文件大小 = %.3f Mb\n", wav.file_size / 1024.0 / 1024.0);
printf("歌曲长度 = %dm%ds\n", (wav.music_size / wav.Sampling_frequency / wav.Sample_size) / 60,
(wav.music_size / wav.Sampling_frequency / wav.Sample_size) % 60);
printf("声道数量 = %d\n", wav.channel);
printf("采样频率 = %d\n", wav.Sampling_frequency);
printf("位深大小 = %d bit\n", wav.bit_depth);
printf("采样帧 = %d\n", wav.Sample_size);
printf("传输速率 = %d\n", wav.transfer_speed);
/*fseek(fp, 0, SEEK_END);
wav.music_size = ftell(fp) - 44;
printf("实际歌曲长度 = %dm%ds\n", (wav.music_size / wav.Sampling_frequency / wav.Sample_size) / 60,
(wav.music_size / wav.Sampling_frequency / wav.Sample_size) % 60);*/
//关闭文件指针
if (fclose(fp) == EOF)
{
puts("Fail to close");
exit(0);
}
return 0;
}
细心的人已经注意到有一段注释,内容是打印出文件的实际长度,这个是为了和文件头中的文件/数据大小信息进行一个相互印证,一般来讲是没必要的,但是我在实际读取过程中发现许多.wav
文件头中这部分信息是错误的,所以留了这么一串代码在这。
2.2 Bit_Conversion(unsigned long buffsize,char* data,char* data_out,int bit,int file_bit)
Bit_Conversion
函数的功能是转换位深。它可以接收两块内存,一块为读取到的文件信息,一块则为空,它通过获取的目标位深(16bit)和文件读取的位深,以及传递过来的文件信息内存长度来计算,将计算后的数据填入空内存中,然后返回空内存(虽然经过填充已经不空了)的头指针。
具体计算过程为
8bit->16bit
:此时空内存长度为文件信息内存长度的两倍,将文件信息中的每一字节按隔一个字节的形式放入空内存中,因为8位文件中的信息是无符号的,而16位文件是有符号的,所以还需要再将复制来的字节高位取反16bit->16bit
:这个大伙都知道,就不赘述了24bit->16bit
:将文件中三个字节看作是一个单位,取后面两个放入空内存中32bit->16bit
:将文件中每两个字节,取后面一个放入空内存中- 额外注意的是,
.wav
格式文件字节都是小端方式存储,比如16位中的某一个数据7E 11
,这里其实11
是高8位,而7E
是低8位,所以在上述的数据操作中,总是要取一个单位(16位中就是两个字节)中后面的字节
那么来看看代码是怎么写的吧
//位深转换
char* Bit_Conversion(unsigned long buffsize, char* data, char* data_out, int bit, int file_bit)
{
if (file_bit == 8)
{
for (unsigned int i = 0; i < buffsize/2; i += 1)
{
data_out[i * 2] = 0x00;
data_out[i * 2 + 1] = data[i] ^ 0x7F;
}
}
else if (file_bit == 24) //每三个字节,只取后两位,即高位的那两位
{
for (unsigned int i = 0; i < buffsize; i += 2)
{
data_out[i] = data[i / 2 * 3 + 1];
data_out[i + 1] = data[i / 2 * 3 + 2];
}
}
else if (file_bit == 32) //缩小数据,每两位只取高位
{
for (unsigned int i = 0; i < buffsize; i += 1)
{
data_out[i] = data[i * 2 + 1];
}
}
else
{
for (unsigned int i = 0; i < buffsize; i += 1)
{
data_out[i] = data[i];
}
}
return (char*)data_out;
}
写的真简洁明了是不是?好,接下来介绍的是单独的一个功能模块,在程序开始时按1
就可以进入这个函数了
2.3 Output16bitFile(int bit)
Output16bitFile(int bit)
函数的功能是将文件转换为16位文件并输出为new_file.wav
文件,这个逻辑其实挺简单的,但是实现却比较繁琐,毕竟转换完还要对文件头进行疯狂修改,实现逻辑为:
- 读取键盘输入:若没有输入则等待,有数字输入进入下一步,有
back_flag
为1
则退出 - 显示原文件信息:调用
File_Info
显示信息,顺便填充wav
结构体 - 将原文件文件头写入新文件:创建新文件,同时写入读到的文件头信息
- 读取原文件数据进行转换:调用
Bit_Conversion
函数完成 - 将转换好的数据写入新文件
- 持续
步骤4
和步骤5
,直到读完:事前获得文件尾位置,当读到文件尾时修改读取大小,争取把文件全部读完 - 读完后修改新文件文件头:主要改文件/数据大小,读文件尾位置完成,而非计算得到,防止原文件文件头是错误的
- 显示新文件信息
- 完成一次循环,返回到
步骤1
:这里关闭文件指针,释放读取文件信息的内存,但不释放空内存,因为文件位深不一样,但是转换后的位深是一样的,只有退出时才清理空内存
看吧,说了想着简单,结果写着挺麻烦的,特别是还得按键控制,那就更繁琐了,不说了直接看代码:
char kb;
int bcak_flag;
int Output16bitFile(int bit) //将文件转换为16bit并以文件的形式输出
{
unsigned char header[44]; //存放头文件的数组
char* data_in = NULL;
char* data_out = NULL;
int output_size = 10000; //一次转换的数据大小
int data_size = 0; //文件大小
int cycle_flag = 1; //循环标识符
char file_name[100];
bit = 16;
data_out = (char*)malloc(output_size); //写入新文件的数据
if (data_out == NULL)return 0;
printf("1. 红色高跟鞋_8bit.wav\n");
printf("2. 红色高跟鞋_16bit.wav\n");
printf("3. 最伟大的作品_24bit_48000hz.wav\n");
printf("4. Dark side of the moon_192K_32bit.wav\n");
printf("Q/ESC返回\n");
while (1)
{
cycle_flag = 1; //为下次循环做准备
output_size = 10000;
memset(file_name, 0, sizeof(file_name));
printf("\n选择音乐:\n");
kb = 0;
while (kb < 49 || kb>52)
{
if (back_flag == 1)
break;
Sleep(100); //不加的话一直点'↑'键会跳到下面的程序
}
system("CLS");
printf("\n将音乐转成16位并输出为文件\n\n");
printf("1. 红色高跟鞋_8bit.wav\n");
printf("2. 红色高跟鞋_16bit.wav\n");
printf("3. 最伟大的作品_24bit_48000hz.wav\n");
printf("4. Dark side of the moon_192K_32bit.wav\n");
printf("Q/ESC返回\n");
if (kb == 49)strcpy(file_name, "red_8bit.wav");
else if (kb == 50)strcpy(file_name, "red.wav");
else if (kb == 51)strcpy(file_name, "44K_24bit.wav");
else if (kb == 52)strcpy(file_name, "192K_32bit.wav");
else if (back_flag == 1)
{
back_flag = 0;
printf("退出音乐输出\n");
system("CLS");
break;
}
printf("\n音乐:%s 转换16bit输出完成\n", file_name);
printf("\n转换前音乐数据");
File_Info(file_name); //获取文件相关信息,大小,位深等
FILE* file_read = fopen(file_name, "rb"); //只读打开文件
//char new_file = strcat("new_",filename);
FILE* file_write = fopen("new_file.wav", "wb"); //创建新文件
if (file_read == NULL)
{
printf("file read fail\n");
return 0;
}
if (file_write == NULL)
{
printf("file write fail\n");
return 0;
}
fseek(file_read, 0, SEEK_END);
data_size = ftell(file_read); //文件尾大小
fseek(file_read, 0, SEEK_SET); //先复制头文件到新文件中,等转换完再修改里面各项内容
fread(header, sizeof(char), 44, file_read);
fseek(file_write, 0, SEEK_SET);
fwrite(header, sizeof(char), 44, file_write);
fseek(file_write, 44, SEEK_SET);
data_in = (char*)malloc(output_size * wav.bit_depth / bit); //读取原本文件的数据
if (data_in == NULL)return 0;
while (cycle_flag)
{
//当读到文件尾时,剩余数据不足一次读取,故对读取大小进行修改
if (ftell(file_read) + output_size >= data_size)
{
output_size = data_size - ftell(file_read);
cycle_flag = 0;
}
//从原本文件中读取文件,然后用转换函数转换完后写入新文件
fread(data_in, sizeof(char), output_size * wav.bit_depth / bit, file_read);
Bit_Conversion(output_size, data_in, data_out, bit, wav.bit_depth);
fwrite(data_out, sizeof(char), output_size, file_write);
}
fseek(file_write, 0, SEEK_END); //计算新的文件大小、数据传输速度、采样帧大小
data_size = ftell(file_write);
wav.transfer_speed = wav.transfer_speed * bit / wav.bit_depth;
wav.Sample_size = wav.Sample_size * bit / wav.bit_depth;
wav.bit_depth = bit;
fseek(file_write, 4, SEEK_SET); //写入新的头文件数据
data_size -= 8;
fwrite(&data_size, 4, 1, file_write); //文件数据长度
fseek(file_write, 28, SEEK_SET);
fwrite(&wav.transfer_speed, 4, 1, file_write); //数据传输速率
fwrite(&wav.Sample_size, 2, 1, file_write); //采样帧大小
fwrite(&wav.bit_depth, 2, 1, file_write); //位深
fseek(file_write, 40, SEEK_SET);
data_size = data_size + 8 - 44;
fwrite(&data_size, 4, 1, file_write); //数据块长度
free(data_in);
fclose(file_read);
fclose(file_write);
printf("\n转换后音乐数据");
File_Info("new_file.wav");
}
free(data_out);
return 0;
}
3. onWindows.c
讲完了File_Inof.c
这个开胃菜后,现在来到了重量级的onWindows.c
,这个我都不知道从何讲起,甚至名字都不知道为什么要这么取的(其实是因为那时候要搞中间层,就把这个当作是Windows下的播放代码了)。算了,还是老规矩,从主函数main
的2
选项之后讲起吧。
3.1 WaveOnWin()
当你在主界面选择想听点歌时,程序就进入了WaveOnWin()
当中。相比于待会要介绍的writeAudioBlock
,这个函数可以说是比较简单了。它的运行逻辑为:
- 初始化设置:比如建立
WAVEFORMATEX
,初始化临界区变量,初始化数组等 - 监听键盘输入
kb
,如果有数字输入就放对应音乐,没有就等待,如果有退出输入就退出 - 进入循环,监听键盘输入
kb
,并显示音乐信息:音乐之间的循环,这里其实也有监听,但是由于kb
没有清零,所以这里第一次进入循环,和后续播放过程中按了数字键后能直接播放对应音乐,在使用完kb
后才会清零,调用File_Info
显示信息,顺便填充wav
结构体 - 还有
music_size
,一般是1
,也就是顺序播放,但是如果是左右按键切歌则置1
或-1
,在赋值给num
(音乐序号)后才会等于loop_mode
,保证后续的循环正常 - 将
wav
结构体内信息赋给WAVEFORMATEX
结构体成员 - 打开播放设备并进入
writeAudioBlock
中:这里就留着下一部分再讲吧 - 等待程序从
writeAudioBlock
退出,从writeAudioBlock
退出后关闭设备,清理device
设备句柄。如果不是因为back_flag
退出则再次进入循环步骤3
,否则则退出 - 退出时
back_flag
置0
代码为:
int WaveOnWin(void)
{
WAVEFORMATEX wave; //初始化wave设置
InitializeCriticalSection (&KEY); //初始化临界区关键字
int bit = 16;
int num = 0;
char file_name[100];
memset(file_name, 0, sizeof(file_name));
music_switch = 1; //默认切歌方向
printf("\n1. 红色高跟鞋_8bit.wav\n");
printf("2. 红色高跟鞋_16bit.wav\n");
printf("3. 最伟大的作品_24bit_48000hz.wav\n");
printf("4. Dark side of the moon_192K_32bit.wav\n");
printf("Q/ESC返回主界面\n");
printf("\n选择音乐:\n");
kb = 0; //重置并监听输入
while (1)
{
if (kb > 48 && kb < 53)break;
else if (back_flag == 1)
{
back_flag = 0;
system("CLS");
return 0;
}
}
while (!back_flag)
{
system("CLS");
printf("\n播放音乐\n");
printf("\n1. 红色高跟鞋_8bit.wav\n");
printf("2. 红色高跟鞋_16bit.wav\n");
printf("3. 最伟大的作品_24bit_48000hz.wav\n");
printf("4. Dark side of the moon_192K_32bit.wav\n");
printf("Q/ESC返回主界面 W/↑增加音量 S/↓降低音量 A/←上一首 D/→下一首 Z/C快进/倒退5s 空格暂停/播放\n");
if (kb > 48 && kb < 53) //如果是通过数字按键切歌,则不参与loop_mode
{
num = (int)kb - 48 - 1;
kb = 0;
}
else //否则按照循环模式或A/D按键切换音乐
num = (num + music_switch + 4) % 4;
if (num == 0)strcpy(file_name, "red_8bit.wav");
else if (num == 1)strcpy(file_name, "red.wav");
else if (num == 2)strcpy(file_name, "44K_24bit.wav");
else if (num == 3)strcpy(file_name, "192K_32bit.wav");
//由于music_switch由切歌按键和循环模式共同影响,在此重置则不管之前进行了什么操作,下一首播放的音乐依旧可以遵循循环模式
music_switch = loop_mode;
File_Info(file_name);
//填写WAVEFORMATEX
wave.nSamplesPerSec = wav.Sampling_frequency; //采样频率
wave.wBitsPerSample = bit; //采样位深
wave.nChannels = wav.channel; //音道
wave.cbSize = 0; //附加信息
wave.wFormatTag = WAVE_FORMAT_PCM; //PCM编码格式,赋1
wave.nBlockAlign = wave.wBitsPerSample * wave.nChannels / 8; //帧大小
wave.nAvgBytesPerSec = wave.nBlockAlign * wave.nSamplesPerSec; //传输速率
//输出文件信息
printf("\n以16bit时播放信息为: \n");
printf("采样频率 = %d\n", wave.nSamplesPerSec);
printf("位深 = %d bit\n", wave.wBitsPerSample);
printf("采样帧大小 = %d\n", wave.nBlockAlign);
printf("数据传输速率 = %d\n", wave.nAvgBytesPerSec);
if (loop_mode == 1)printf("当前循环模式为(按‘X’切换):顺序循环\n");
else if (loop_mode == 0)printf("当前循环模式为(按‘X’切换):单曲循环\n");
else printf("当前循环模式为(按‘X’切换):倒序循环\n");
//WAVE_MAPPER 是 mmsystem.h 中定义的常量,它始终指向系统上的默认波形设备
if (waveOutOpen(&device, WAVE_MAPPER, &wave, (DWORD_PTR)waveOutProc, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
{
fprintf(stderr, "unable to open WAVE_MAPPER device\n");
return 0;
}
writeAudioBlock(device, file_name, file_lrc, bit);
waveOutClose(device); //播放完音频后关闭设备,清理句柄
printf("Close device successful\n");
device = NULL;
}
system("CLS");
back_flag = 0;
return 0;
}
细心的人会发现调用waveOutOpen
时启动了回调函数,没错,接下来是waveOutProc
。
3.2 CALLBACK waveOutProc(HWAVEOUT device, UINT uMsg, DWORD dwInstance, WAVEHDR dwParam1, DWORD dwParam2)
这个没什么好说的,就是当有缓冲区输出完时,设置函数中的uMsg
为WOM_DONE
,所以就用这个判断是否有缓冲区输出完,有的话就free_buff
+1,free_buff
是实时的可用缓冲区数量,并不等于SUM_BUFF
。你是不是想为什么是+1不是-1,因为-1是有缓冲区开始进行输出,而这里是缓冲区输出完了,可以使用它了,所以+1。这里用临界区防止与writeAudioBlock
共享资源时冲突。代码为:
//回调函数,检测输出完的缓冲区,当有缓冲区输出完会调用该函数
static void CALLBACK waveOutProc(HWAVEOUT device, UINT uMsg, DWORD dwInstance, WAVEHDR dwParam1, DWORD dwParam2)
{
if (uMsg != WOM_DONE)
{
return;
}
EnterCriticalSection(&KEY); //使用临界区防止与主函数冲突,同时可用缓冲区加一
free_buff++;
LeaveCriticalSection(&KEY);
}
不多说了,直接进入writeAudioBlock
,要准备下班了,而且是2月3号的下班。
3.3 writeAudioBlock(HWAVEOUT device,char filename[], char file_lrc[], int bit)
我的确把过多的功能和判断集成在了这个函数中,导致这个函数过于臃肿。其实这个函数的功能就是播放音乐,然后在播放的同时搞点判断,没了,判断的话具体参考:
Q/ESC
返回主界面
W/↑
增加音量S/↓
降低音量
A/←
上一首D/→
下一首
Z/C
快进/倒退5s
空格
暂停/播放
数字
直接切到该序号的音乐
writeAudioBlock
的运行逻辑为:
- 初始化,初始化各种东西:定义了一个
WAVEHDR
结构体数组wave_buff[SUM_BUFF]
,和一个指针数组out_data[SUM_BUFF]
,SUM_BUFF
为自己定义的缓冲区数量。指针数组是为了待会申请内存,理论上来讲两块内存就够了,但是那样有点麻烦,就使用指针数组了,不过也问题不大,只需要把SUM_BUFF
设置为2
,那么就是两块内存了 - 打开音乐文件,获得文件大小,申请
file_data
内存存放文件信息,读取BUFFER_LENGTH * file_bit / bit
大小的信息,BUFFER_LENGTH
为一次转换的大小,BUFFER_LENGTH * file_bit / bit
的意义是控制读取时的信息大小,比如32位转16位播放,那么要读2倍的BUFFER_LENGTH
才不会出问题,而输出只需要正常的BUFFER_LENGTH
就行 - 进入循环,调用
Bit_Conversion
后获得转换后数据的内存的指针,然后调用waveOutPrepareHeader
清理缓冲区(也就是刚刚获得的内存),再调用waveOutWrite
输出音频数据。因为输出了一块缓冲区,所以让free_buff
减一很合理吧,这里用临界区防止与线程共享资源时冲突。 - 检测是否有上一块缓冲区,有的话进行清理,当从第一块缓冲区开始时,上一块缓冲区指针为
NULL
- 等待
free_buff
+1,也就是缓冲区输出完,毕竟播放音乐是需要时间的 - 进入判断阶段,因为一次循环极快,所以当有键盘输入时也可以反应过来,有哪些判断就看上面功能介绍就行了
- 一通判断后,如果是:
- 快进/倒退,则改变文件指针位置
- 切歌,则退出循环,将
music_size
置1或-1 - 有数字按键也退出循环,保留
kb
到WaveOnWin
中,这样就跟第一次进入播放模式时的逻辑一样了 - 读完文件退出循环,
music_size
为loop_mode
- 有退出标识符则退出循环
- 如果有幸没有退出循环,则回到
步骤3
,持续读取、输出文件内容到设备中 - 结束循环后,释放这个函数里使用的内存,毕竟下一次又是全新的音乐了
代码如下:
//向设备写入缓冲区数据
void writeAudioBlock(HWAVEOUT device,char filename[], char file_lrc[], int bit)
{
WAVEHDR wave_buff[SUM_BUFF]; //设置缓冲区结构体数组
int file_bit = wav.bit_depth; //文件位深,默认16位
FILE* file = NULL;
char* file_data = NULL; //读取的文件数据
char* out_data[SUM_BUFF]; //位深转换后要输出的数据
file = fopen(filename, "rb+");
if (file == NULL)
{
printf("无法打开文件\n");
return;
}
//初始化缓存区
for (int i = 0;i < SUM_BUFF;i++)
{
ZeroMemory(&wave_buff[i], sizeof(WAVEHDR));
wave_buff[i].dwBufferLength = BUFFER_LENGTH;
wave_buff[i].lpData = NULL;
wave_buff[i].dwFlags = 0;
wave_buff[i].dwLoops = 1;
out_data[i] = (char*)malloc(BUFFER_LENGTH); //申请data缓存区读取文件
if (out_data[i] == 0)return;
}
fseek(file, 0, SEEK_END); //读取文件结尾位置,用来判断文件是否结束
long file_tile = ftell(file);
file_data = (char*)malloc(BUFFER_LENGTH * file_bit / bit); //申请data缓存区读取文件
if (file_data == 0)return;
fseek(file, 44, SEEK_SET);
//BUFFER_LENGTH * file_bit/bit指文件数据要读取的大小,如16bit文件转8bit输出
//假设要输出44100字节数据,则要读取文件44100*2字节数据,将其中一半数据去除再输出。
fread(file_data, sizeof(char), BUFFER_LENGTH * file_bit / bit, file);
int now = 0; //标识当前在播放的缓冲区
int next = 0; //标识下一个缓冲区
int last = 0; //标识上一个缓冲区
kb = 0; //重置输入
while (1)
{
next = (now + 1) % SUM_BUFF; //直接 +1 固然美好,但是加个 % 就可以循环了
last = (now + SUM_BUFF - 1) % SUM_BUFF; //直接 -1 会出现负数
wave_buff[now].lpData = Bit_Conversion(BUFFER_LENGTH, file_data, out_data[now], bit, file_bit);
waveOutPrepareHeader(device, &wave_buff[now], sizeof(WAVEHDR));
waveOutWrite(device, &wave_buff[now], sizeof(WAVEHDR));
EnterCriticalSection(&KEY); //可用缓冲区数量减一,同时使用临界区防止与回调函数冲突
free_buff--;
LeaveCriticalSection(&KEY);
if (wave_buff[last].lpData != NULL) //当前的缓冲区在输出时,偷偷释放上一块缓冲区
{
waveOutUnprepareHeader(device, &wave_buff[last], sizeof(WAVEHDR));
}
//当未输出完\还没有缓冲区时等待,一般来讲,输出时间远大于运行时间,除非每次输出长度小的可怜
while (free_buff <= 0)
{
Sleep(10);
}
if (kb == 'c' || kb == 'C') //快进5秒
{
//注释内容为:如果到结尾继续快进,则返回到结尾前5s,而非直接跳到下一个文件,注释后是快进到文件结尾时切进下一首歌
/*if (ftell(file) >= (file_tile - BUFFER_LENGTH * file_bit / bit * 20))
fseek(file, -(BUFFER_LENGTH * file_bit / bit * 20), SEEK_END);
else*/
fseek(file, BUFFER_LENGTH * file_bit / 8 * 10, SEEK_CUR);
printf("快进5秒\n");
kb = 0;
}
else if (kb == 'z' || kb == 'Z') //倒退5秒
{
if (ftell(file) < BUFFER_LENGTH * file_bit / 8 * 10) //在文件开头倒退时重置到文件头
fseek(file, 44, SEEK_SET);
else
fseek(file, -(BUFFER_LENGTH * file_bit / 8 * 10), SEEK_CUR);
printf("倒退5秒\n");
kb = 0;
}
//读取下一次循环要用的数据
fread(file_data, sizeof(char), BUFFER_LENGTH * file_bit / bit, file);
//读完文件、结束标识符为1或输入数字,结束循环
if (ftell(file) >= file_tile || back_flag == 1 || (kb > 48 && kb < 53))
{
while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
Sleep(10);
printf("\nMusic End?\n");
break;
}
//如果输入了‘d’或者→按键,切歌标识符置1,结束循环
else if (kb == 'd' || kb == 'D' || kb == 77)
{
music_switch = 1;
while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
Sleep(10);
printf("\nThe Next Song\n");
break;
}
//如果输入了‘a’或者←按键,切歌标识符置-1,结束循环
else if (kb == 'a' || kb == 'A' || kb == 75)
{
music_switch = -1;
while (waveOutUnprepareHeader(device, &wave_buff[now], sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
Sleep(10);
printf("\nThe Last Song\n");
break;
}
now = next;
}
//结束播放后释放内存
Sleep(500);
for (int i = 0;i < SUM_BUFF;i++)
{
free(out_data[i]);
}
free(file_data);
fclose(file);
}
不得不说,这一块写的的确有点过于繁杂臃肿了,本想将判断全部移入线程,但是线程本身必须要判断的就已经够多了,且还得用全局变量区传递信息,就又想将线程中的判断全部判断移到这里面来,但是有些判断完全不知道怎么移(比如暂停),就只好将大部分判断放在这,必要的就留在线程里了。感觉还是可以改进改进的,不过功能加的是真的爽
4. 总结
还需要总结吗?应该不用了吧。那就讲讲整个项目的运行过程吧
- 运行程序
- 创建并启动线程,开启全程监听键盘输入
- 主界面,选择功能,等待输入
- 如果选
1
则进入文件输出模式,可以疯狂转换文件,按Q/ESC
退回到主界面 - 如果选
2
则进入音乐播放模式,可以疯狂地听音乐,按Q/ESC
退回到主界面 - 回到主界面,即回到
步骤3
- 主界面按
Q/ESC
退出程序
好了,上面就是整个运行过程了,感觉内容不多,但是为什么我写了那么多代码,真是奇怪啊