蛋疼写了段代码播放东方系列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的参数说明如下:

wFormatTag WAVE_FORMAT_PCM wave的格式是pcm格式
nChannels 2 声道,这里是立体声,即2
nSamplesPerSec 44100L 采样率可选的有8.0k, 11.025k, 22.05k和44.1k,这里是44100
nAvgBytesPerSec 176400L nAvgBytesPerSec = nSamplesPerSec * nBlockAlign
nBlockAlign 4 nBlockAlign = nChannels * wBitsPerSample
wBitsPerSample 16 采样位数可选的有8和16,这里是16,也就是两个byte
cbSize 0 对于pcm格式,这个参数不需要

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_S16_LE 数据格式为signed int16 little endian
SNDCTL_DSP_STEREO 1 立体声(stereo)
SNDCTL_DSP_SPEED 44100 采样率为44.1kHz

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)

写文件,注意未必能一次全部写入,返回写入的字节数。

close(int fd)

3 Responses to “[蛋疼]windows与linux下播放pcm格式音频(ZWAV)”
  1. xtricman says:

    那个…我是小白…我怎么看你的linux都把整个音乐循环了啊,更本没有分出前奏啊…还有跳过前面的zwav用fseek移动文件读写指针不行吗?

    • watashi says:

      fseek当然可以,不过才4个字节就无所谓了
      分出前奏得去读音乐数据(就是每首曲子开始位置和循环开始、结束位置),没研究过thxx.dat的逆向,不过网上找得到别人贴出来的结果。
      这里就只能fseek或者放在内存里了,或者mmap?

  2. viktor says:

    受教了!我在做MP3解码的作业,正纠结这一步呢。linux平台的网上有类似的做法,不过绕了一个大弯子还是回到直接操作硬件上。
    (原来ZWAV是神主自己的magic number啊)

  3.  
Leave a Reply