

[蛋疼]windows与linux下播放pcm格式音频(ZWAV)
Posted by watashi in gao, tags: audio, c, linux, windows, 東方蛋疼写了段代码播放东方系列wave格式的背景音乐。随便拿个工具打开thbgm.dat就会发现前四个byte都是ZWAV,这应该是神主自己创造的魔数了。然后后面就全都是正常人理解不能的二进制数据了。其实这些都是pcm格式的音频,可以理解为就是wave格式砍掉了包含音频参数等信息的头。thbgm.dat里音频是16bit, 44.1kHz的立体声。游戏中播放时,每首bgm的播放第一遍都由开始位置播到结束位置,此后就重复播放不含前奏的循环部分。如果要提取wave格式音频只要根据每个bgm的开始位置,前奏长度,循环长度,就可以导出数据并补上头信息写入.wav文件。不过这里的方法不提取音频而是通过api设置音频参数,直接播放pcm格式音频。至于优点,除了省点硬盘外想不到第二个了,所以说了是蛋疼了么……
PS: *nix里everything is file的哲学真是美啊。
ZWAV Player Demo(Windows)
测试环境: Win7 + VS2010
#include <stdio.h> #include <Windows.h> #pragma comment(lib, "winmm.lib") char buf[1 << 20]; int main(int argc, char* argv[]) { FILE* thbgm; int cnt; HWAVEOUT hwo; WAVEHDR wh; WAVEFORMATEX wfx; HANDLE wait; wfx.wFormatTag = WAVE_FORMAT_PCM; wfx.nChannels = 2; wfx.nSamplesPerSec = 44100L; wfx.nAvgBytesPerSec = 176400L; wfx.nBlockAlign = 4; wfx.wBitsPerSample = 16; wfx.cbSize = 0; wait = CreateEvent(NULL, 0, 0, NULL); waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT); thbgm = fopen(argv[1], "rb"); fread(buf, sizeof(char), 4, thbgm); fputs(buf, stderr); while (cnt = fread(buf, sizeof(char), sizeof(buf), thbgm)) { wh.lpData = buf; wh.dwBufferLength = cnt; wh.dwFlags = 0L; wh.dwLoops = 1L; waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR)); waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); WaitForSingleObject(wait, INFINITE); fprintf(stderr, "="); fflush(stderr); } waveOutClose(hwo); fclose(thbgm); return 0; }
waveOutOpen(LPHWAVEOUT phwo, UINT_PTR uDeviceID, LPWAVEFORMATEX pwfx, DWORD_PTR dwCallback, DWORD_PTR dwCallbackInstance, DWORD fdwOpen)
waveOutOpen用于打开用于播放的输出设备。
WAVEFORMATEX的参数说明如下:
|
waveOutPrepareHeader(HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh)
waveOutPrepareHeader用于准备waveOutWrite所需的WAVEHDR。调用前需要设置好WAVEHDR的lpData, dwBufferLength, 和dwFlags。
waveOutWrite(HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh)
waveOutWrite将根据WAVEHDR的信息把数据发到输出设备。waveOutWrite是立即放回的,而播放在后台进行,所以这里需要WaitForSingleObject。不过这样两次waveOutWrite中间将出现明显的停顿,可以通过双缓冲解决。
waveOutClose(HWAVEOUT hwo)
ZWAV Player Demo(Linux)
测试环境: ubuntu8.10 amd64
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/soundcard.h> #define BUFSIZE (1024 * 1024) char buf[BUFSIZE]; int main(int argc, char* argv[]) { FILE* thbgm; int off, cnt; int fd; int ioctl_val; fd = open("/dev/null", O_WRONLY); ioctl_val = AFMT_S16_LE; ioctl(fd, SNDCTL_DSP_SETFMT, &ioctl_val); ioctl_val = 1; ioctl(fd, SNDCTL_DSP_STEREO, &ioctl_val); ioctl_val = 44100; ioctl(fd, SNDCTL_DSP_SPEED, &ioctl_val); thbgm = fopen(argv[1], "rb"); fread(buf, sizeof(char), 4, thbgm); fputs(buf, stderr); while (cnt = fread(buf, sizeof(char), BUFSIZE, thbgm)) { for (off = 0; off < cnt; off += write(fd, buf + off, cnt - off)) { } fprintf(stderr, "="); fflush(stderr); } close(fd); fclose(thbgm); return 0; }
open(const char *pathname, int flags)
以O_WRONLY方式打开/dev/dsp。
ioctl(int d, int request, …)
ioctl用于实现对设备的控制,通过ioctl可以对/dev/dsp设置音频的参数。这里需要设置三个参数就够了。
|
SNDCTL_DSP_SETFMT有AFMT_U8, AFMT_S8, AFMT_S16_LE, AFMT_S16_BE, AFMT_U16_LE, AFMT_U16_BE等多种参数可选。
write(int fd, const void *buf, size_t count)
写文件,注意未必能一次全部写入,返回写入的字节数。
那个…我是小白…我怎么看你的linux都把整个音乐循环了啊,更本没有分出前奏啊…还有跳过前面的zwav用fseek移动文件读写指针不行吗?
fseek当然可以,不过才4个字节就无所谓了
分出前奏得去读音乐数据(就是每首曲子开始位置和循环开始、结束位置),没研究过thxx.dat的逆向,不过网上找得到别人贴出来的结果。
这里就只能fseek或者放在内存里了,或者mmap?
受教了!我在做MP3解码的作业,正纠结这一步呢。linux平台的网上有类似的做法,不过绕了一个大弯子还是回到直接操作硬件上。
(原来ZWAV是神主自己的magic number啊)