首页 > 其他分享 >读写wav格式文件

读写wav格式文件

时间:2022-10-06 22:44:48浏览次数:84  
标签:typedef Word 读写 格式文件 wav osc data fmt size

读写wav格式文件

注意:本文代码仅在MinGW-w64 gcc/g++环境下编译测试通过,其余环境不保证。

MinGW环境可以在以下链接下载:

https://github.com/niXman/mingw-builds-binaries/releases
https://sourceforge.net/projects/mingw-w64/files/

以上链接第一个版本较新,第二个版本较老。


上次这篇文章讲到了wav格式的组织存储方式,现在我们根据其格式进行wav文件的读写操作。

在此之前先将上篇文章部分关于wav格式的内容整理成头文件在这里贴出:

types.h

#pragma once

typedef char Int8;
typedef short Int16;
typedef long Int32;
typedef long long Int64;
typedef unsigned char UInt8;
typedef unsigned short UInt16;
typedef unsigned long UInt32;
typedef unsigned long long UInt64;
typedef UInt8 Byte;
typedef UInt16 Word;
typedef UInt32 DWord;
typedef UInt64 QWord;

typedef struct
{
    DWord D1;
    Word D2;
    Word D3;
    Byte D4[8];
} Guid;

typedef union
{
    DWord dw;
    char chr[4];
} FourCC;

wavfmt.h

#pragma once

#include "types.h"

typedef struct
{
    FourCC id; // 区块类型
    DWord size; // 区块大小(不包括id和size字段的大小)
} RIFFChunkHeader;

typedef struct
{
    FourCC id; // 必须是 "RIFF"
    DWord size; // 文件大小(字节数)-8
    FourCC type; // 必须是 "WAVE"
} RIFFHeader;

typedef struct
{
    Word FormatTag;
    Word Channels;
    DWord SampleRate;
    DWord BytesRate;
    Word BlockAlign;
    Word BitsPerSample;
} WaveFormat;

typedef struct
{
    Word FormatTag;
    Word Channels;
    DWord SampleRate;
    DWord BytesRate;
    Word BlockAlign;
    Word BitsPerSample;
    Word ExSize;
} WaveFormatEx;

typedef struct
{
    Word FormatTag;
    Word Channels;
    DWord SampleRate;
    DWord BytesRate;
    Word BlockAlign;
    Word BitsPerSample;
    Word ExSize;
    Word ValidBitsPerSample;
    DWord ChannelMask;
    Guid SubFormat;
} WaveFormatExtensible;

读取wav文件相对麻烦一些,我们先从写入开始吧。

写一个wav文件

一般我们需要写入wav文件的情况就是将PCM数据封装起来,所以我们需要一段原始PCM数据。获得PCM数据的方法有很多,比如可以用麦克风录制一段声音,但是这个要留到后面讲DirectSound的时候,所以这一次,我们自己创建一段PCM数据,并把它写入到文件,用现有的播放器来播放试听效果。

创建一段PCM数据

众所周知,声音是物体震动发出的,记录声音的方式就是把振幅值随时间的变化曲线记录下来,但是由于计算机是以离散的方式存储数据的,所以我们需要每过一定的时间间隔就记录一次振幅并量化,这样存储下来的数据就是PCM数据。这个PCM数据是没有任何信息的,你用不同的速度播放效果是不一样的,所以我们需要同时拥有采样率、量化位数等信息才能正确播放,而wav格式就存储了这些必要数据。

用来生成波形的设备叫振荡器(oscillator),当其生成的频率在20HZ-20kHZ范围内就可以让扬声器播放出声音(能不能听见接近两端频率的声音取决于多种因素),由于奈奎斯特采样定理,这个采样率至少为该频率的两倍。

因为声音记录下来的数据是波形,所以这里我们用sin函数生成一段正弦波数据,作为我们的PCM数据。由于数据存储的方式,为了生成这个数据,我们需要同时设置采样率和频率。

本文我们将以44100HZ的采样率生成一段10秒钟的1000HZ的正弦波,单声道,量化位数16位。

typedef struct
{
    double increase;
    double phase;
    double gain;
} oscillator;

void init_osc(oscillator *osc, int sample_rate, int frequency, double gain)
{
    osc->increase = TWOPI * frequency / sample_rate;
    osc->gain = gain;
    osc->phase = 0;
}

double osc_next(oscillator *osc)
{
    double sample = sin(osc->phase) * osc->gain;
    osc->phase += osc->increase;
    if (osc->phase > TWOPI)
        osc->phase -= TWOPI;
    return sample;
}

这段代码实现了一个简单的正弦波振荡器。

生成PCM采样的方法如下:

oscillator osc;
init_osc(&osc, 44100, 1000, 0.25);
short *buffer = malloc(441000 * sizeof(short));
for (int i = 0; i < 441000; i++)
    buffer[i] = 32767 * osc_next(&osc);
// ...
free(buffer);

这里init_osc第四个参数gain设为0.25(等效于约-12dB)是为了播放的时候声音不要太大,不然1kHZ正弦波的声音还是很刺耳难听的。

写入到文件

写入文件的方法就简单了,依次按照RIFF文件头,fmt块,data块的顺序写入文件即可。

完整代码如下,使用gcc直接编译即可,无需链接任何库。

#include "wavfmt.h"
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define TWOPI (2*3.1415926535897932)

typedef struct
{
    double increase;
    double phase;
    double gain;
} oscillator;

void init_osc(oscillator *osc, int sample_rate, int frequency, double gain)
{
    osc->increase = TWOPI * frequency / sample_rate;
    osc->gain = gain;
    osc->phase = 0;
}

double osc_next(oscillator *osc)
{
    double sample = sin(osc->phase) * osc->gain;
    osc->phase += osc->increase;
    if (osc->phase > TWOPI)
        osc->phase -= TWOPI;
    return sample;
}

#define BUFFER_LENGTH 441000

int main()
{
    // RIFF header
    RIFFHeader riff;
    strncpy((char *)&riff.id, "RIFF", 4);
    riff.size = 4 + sizeof(RIFFChunkHeader) * 2 + sizeof(WaveFormat) + BUFFER_LENGTH * sizeof(short);
    strncpy((char *)&riff.type, "WAVE", 4);
    // Format header
    RIFFChunkHeader fmt_header;
    strncpy((char *)&fmt_header.id, "fmt ", 4);
    fmt_header.size = sizeof(WaveFormat);
    // Format
    WaveFormat fmt;
    fmt.FormatTag = 1;
    fmt.Channels = 1;
    fmt.BitsPerSample = 16;
    fmt.SampleRate = 44100;
    fmt.BlockAlign = 2;
    fmt.BytesRate = 44100 * 2;
    // Data header
    RIFFChunkHeader data_header;
    strncpy((char *)&data_header.id, "data", 4);
    data_header.size = BUFFER_LENGTH * sizeof(short);
    // Generate PCM
    oscillator osc;
    init_osc(&osc, 44100, 1000, 0.25);
    short *buffer = malloc(BUFFER_LENGTH * sizeof(short));
    for (int i = 0; i < BUFFER_LENGTH; i++)
        buffer[i] = 32767 * osc_next(&osc);
    // Write to file
    FILE *f = fopen("sin_1khz.wav", "wb");
    fwrite(&riff, sizeof(RIFFHeader), 1, f);
    fwrite(&fmt_header, sizeof(RIFFChunkHeader), 1, f);
    fwrite(&fmt, sizeof(WaveFormat), 1, f);
    fwrite(&data_header, sizeof(RIFFChunkHeader), 1, f);
    fwrite(buffer, 2, BUFFER_LENGTH, f);
    free(buffer);
    fclose(f);
}

运行程序后会在当前工作目录下生成一个"sin_1khz.wav"的文件,用播放器播放就可以听到嘟~~~的声音了。

读取wav文件

实际上读取wav文件也不难,只要按照区块的标准一个个查找就行了,一般fmt块就是第一个块,而data块则有可能夹在中间,所以我们需要循环读取区块,找出fmt和data这两个块就可以了。

当然这样只适合读取标准PCM编码或者IEEE浮点格式的wav文件,对于其他格式的文件并不支持(需要例如fact块),但是一般这样就足够了。

对于这个过程,我们只关心以下几点就可以了

  • fmt块的内容
  • 数据在文件中的位置
  • 数据的大小

为了方便编码以及使用,读取wav文件的代码使用c++实现

也没什么很复杂的,稍微注意一点细节即可,这个前文其实提到过。

wavread.h

#pragma once

#include <stdio.h>
#include <string.h>
#include "wavfmt.h"

class WaveReader
{
private:
    FILE *f;
    WaveFormatExtensible fmtext;
    Int64 data_pos;  // 数据在文件中的位置
    Int64 data_size; // 文件中数据的大小
    Int64 read_size; // 当前读取的数据大小
    // 查找文件中的"fmt "块
    bool find_fmt()
    {
        RIFFChunkHeader hd;
        long size = 0;
        do {
            size = fread(&hd, 1, sizeof(RIFFChunkHeader), f);
            if (hd.size % 2 == 1) // 如果块大小为奇数则需要对齐
                hd.size++;
            if (strncmp((char *)&hd.id, "fmt ", 4) != 0)
                fseek(f, hd.size, SEEK_CUR);
            else
                break;
        } while (size >= 8);
        if (size < 8)
            return false;
        fread(&fmtext, 1, hd.size, f); // 假设文件的format块小于等于sizeof(WaveFormatExtensible)
        // 判断文件格式是否是PCM或者IEEE编码
        if (fmtext.FormatTag == 0xFFFE) {
            if (fmtext.SubFormat.D1 != 1 && fmtext.SubFormat.D1 != 3)
                return false;
        } else if (fmtext.FormatTag != 1 && fmtext.FormatTag != 3) {
            return false;
        }
        return true;
    }
    // 查找文件中的"data"块
    bool find_data()
    {
        RIFFChunkHeader hd;
        long size = 0;
        do {
            size = fread(&hd, 1, sizeof(RIFFChunkHeader), f);
            if (hd.size % 2 == 1) // 查找data块过程中这个更加重要
                hd.size++;
            if (strncmp((char *)&hd.id, "data", 4) != 0)
                fseek(f, hd.size, SEEK_CUR);
            else
                break;
        } while (size >= 8);
        if (size < 8)
            return false;
        fgetpos(f, &data_pos); // 获取实际数据的位置
        data_size = hd.size;   // 该块的大小即为数据的大小
        return true;
    }
public:
    WaveReader()
    {
        memset(&fmtext, 0, sizeof(WaveFormatExtensible));
        f = NULL;
        read_size = 0;
    }
    ~WaveReader()
    {
        if (f)
            fclose(f);
    }
    bool open_file(char *filename)
    {
        f = fopen(filename, "rb");
        if (f) {
            RIFFHeader riff;
            fread(&riff, 1, sizeof(RIFFHeader), f);
            if (strncmp((char *)&riff.id, "RIFF", 4) != 0)
                return false;
            if (strncmp((char *)&riff.type, "WAVE", 4) != 0)
                return false;
            if (!find_fmt())
                return false;
            if (!find_data())
                return false;
            return true;
        }
        return false;
    }
    void close_file()
    {
        if (f)
            fclose(f);
        memset(&fmtext, 0, sizeof(WaveFormatExtensible));
        f = NULL;
        data_pos = 0;
        data_size = 0;
        read_size = 0;
    }
    WaveFormatExtensible *get_fmtext()
    {
        return &fmtext;
    }
    int read_data(void *buffer, DWord size)
    {
        if (read_size >= data_size) {
            memset(buffer, 0, size);
            return -1;
        }
        int result;
        if (read_size + size < data_size) {
            result = fread(buffer, 1, size, f);
            read_size += result;
        } else {
            memset(buffer, 0, size);
            result = fread(buffer, 1, data_size - read_size, f);
            read_size = data_size;
        }
        return result;
    }
    void reset()
    {
        read_size = 0;
        fseek(f, data_pos, SEEK_SET);
    }
};

也可以将文件拆开,将主要实现代码写在wavread.cpp文件中实现,这样更适用于一般的项目,不过本系列内容仅展示基本方法。

这段代码可以读取大部分的PCM和IEEE格式的wav文件,只需调用open_file()打开文件,read_data()读取数据,close_file()关闭文件,其他各种不在赘述。

不过目前只能读取数据,还没有实现播放,播放API有好多,比较老的比如waveXxx系列,DirectSound,最新的是WASAPI。

标签:typedef,Word,读写,格式文件,wav,osc,data,fmt,size
From: https://www.cnblogs.com/PeaZomboss/p/16758738.html

相关文章

  • 122-26-ZooKeeper 读写流程和监听机制_ev
                           ......
  • linux读写一个NTFS分区
    为了读写一个NTFS分区的数据,挂载的时候出现错误提示如下:root@tv:/home/xx#mount-tntfs-3g/dev/sdb1/media/sxx/硬盘B-临时文件Thediskcontainsanuncleanfile......
  • JUC必要掌握(Synchronized,Lock,可重入锁ReentrantLock,可重入锁,读写锁,自旋锁,线程间通信,集
    本文已参与「新人创作礼」活动,一起开启掘金创作之路1.锁(Synchronized和lock)1.1Synchronized(1)Synchronized是Java内置的关键字,是Java内置的锁机制。(2)Synchronized的作......
  • HDFS读写流程
    HDFS读流程客户端通过FileSystem的get方法加载配置获得FileSystem对象。FileSystem向NameNode通过open方法请求读取文件。NameNode进行检查(文件是否存在,是否有相应权......
  • 11_使用SDL播放WAV
    使用命令播放WAV对于WAV文件来说,可以直接使用ffplay命令播放,而且不用像PCM那样增加额外的参数。因为WAV的文件头中已经包含了相关的音频参数信息。ffplayin.wav接下来......
  • 10_PCM转WAV
    播放器是无法直接播放PCM的,因为播放器并不知道PCM的采样率、声道数、位深度等参数。当PCM转成某种特定的音频文件格式后(比如转成WAV),就能够被播放器识别播放了。本文通过2......
  • 【笨方法学python】ex16 - 读写文件
    代码如下:点击查看代码#-*-coding:utf-8--*-#读写文件#close-关闭文件(保存)。#read-读取文件内容,结果可赋值给一个变量。#readline-读取文本文件中的一......
  • C语言读写文件
    1.从键盘输入一些字符,逐个把它们送到磁盘上去,直到用户输入一个'#'#include<stdio.h>#include<stdlib.h>intmain(){FILE*fp;charch,filename[10];......
  • 配置Django实现数据库读写分离
    配置Django实现数据库读写分离django在进行数据库操作的时候,读取数据与写数据(增、删、改)可以分别从不同的数据库进行操作。1.在配置文件中增加slave数据库的配置DATABA......
  • 利用ldt_struct 与 modify_ldt 系统调用实现任意地址读写
    利用ldt_struct与modify_ldt系统调用实现任意地址读写ldt_struct与modify_ldt系统调用的介绍ldt_struct​​ldt​​​是​​局部段描述符表​​​,里面存放的是进程的段描......