请参阅下一个迭代。

三年前,我编写了这个简单的C程序,用于将计算机生成的音乐写入WAV文件。我对其进行了一些重构,但是对我来说看起来并不好。有什么建议吗?

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

/******************************
*  Magic file format strings. *
******************************/
const char fChunkID[]     = {'R', 'I', 'F', 'F'};
const char fFormat[]      = {'W', 'A', 'V', 'E'};
const char fSubchunk1ID[] = {'f', 'm', 't', ' '};
const char fSubchunk2ID[] = {'d', 'a', 't', 'a'};

/********************************
* WriteWavePCM() configuration: *
* - 2 channels,                 *
* - frequency 44100 Hz.         *
********************************/
const unsigned short N_CHANNELS = 2;
const unsigned int SAMPLE_RATE = 44100;
const unsigned short BITS_PER_BYTE = 8;

bool WriteWavePCM(short* sound, size_t pairAmount, char* fileName){
    const static unsigned int fSubchunk1Size = 16;
    const static unsigned short fAudioFormat = 1;
    const static unsigned short fBitsPerSample = 16;

    unsigned int fByteRate = SAMPLE_RATE * N_CHANNELS *
                             fBitsPerSample / BITS_PER_BYTE;

    unsigned short fBlockAlign = N_CHANNELS * fBitsPerSample / BITS_PER_BYTE;
    unsigned int fSubchunk2Size;
    unsigned int fChunkSize;

    FILE* fout;
    size_t ws;

    if (!sound || !fileName || !(fout = fopen( fileName, "w" ))) return false;

    fSubchunk2Size = pairAmount * N_CHANNELS * fBitsPerSample / BITS_PER_BYTE;
    fChunkSize = 36 + fSubchunk2Size;

    // Writing the RIFF header:
    fwrite(&fChunkID, 1, sizeof(fChunkID),      fout);
    fwrite(&fChunkSize,  sizeof(fChunkSize), 1, fout);
    fwrite(&fFormat, 1,  sizeof(fFormat),       fout);

    // "fmt" chunk:
    fwrite(&fSubchunk1ID, 1, sizeof(fSubchunk1ID),      fout);
    fwrite(&fSubchunk1Size,  sizeof(fSubchunk1Size), 1, fout);
    fwrite(&fAudioFormat,    sizeof(fAudioFormat),   1, fout);
    fwrite(&N_CHANNELS,      sizeof(N_CHANNELS),     1, fout);
    fwrite(&SAMPLE_RATE,     sizeof(SAMPLE_RATE),    1, fout);
    fwrite(&fByteRate,       sizeof(fByteRate),      1, fout);
    fwrite(&fBlockAlign,     sizeof(fBlockAlign),    1, fout);
    fwrite(&fBitsPerSample,  sizeof(fBitsPerSample), 1, fout);

    /* "data" chunk: */
    fwrite(&fSubchunk2ID, 1, sizeof(fSubchunk2ID),      fout);
    fwrite(&fSubchunk2Size,  sizeof(fSubchunk2Size), 1, fout);

    /* sound data: */
    ws = fwrite(sound, sizeof(short), pairAmount * N_CHANNELS, fout);
    fclose(fout);
    return true;
}

/************************************
* Around 23 seconds of pure techno! *
************************************/
const unsigned int N_SAMPLE_PAIRS = 1048576;

int main(int argc, char* argv[]){
    short* sound;
    int i;
    int j;
    bool status;
    char* file_name;

    sound = (short*) malloc( sizeof(short) * N_SAMPLE_PAIRS * N_CHANNELS );

    if (!sound)
    {
        puts("Could not allocate space for the sound data.");
        return (EXIT_FAILURE);
    }

    for (i = 0, j = 0; i < N_SAMPLE_PAIRS * N_CHANNELS; i += 2, j++ )
    {
        short datum1 = 450 * ((j >> 9 | j >> 7 | j >> 2) % 128);
        short datum2 = 450 * ((j >> 11 | j >> 8 | j >> 3) % 128);

        sound[i]     = datum1; // One channel.
        sound[i + 1] = datum2; // Another channel.
    }

    file_name = argc > 1 ? argv[1] : "Default.wav";
    status = WriteWavePCM(sound, N_SAMPLE_PAIRS, file_name);
    free(sound);

    if (status)
    {
        printf("Discotheque is ready in \"%s\"\n", file_name);
    }
    else
    {
        puts( "Something seems to have gone wrong." );
        return (EXIT_FAILURE);
    }

    return 0;
}


(播放生成的文件有什么用,VLC和QuickTime Player能够播放它;不确定其他媒体播放器。)

评论

听起来像什么?

某种技术。我不知道为什么,但是生成的音乐保持恒定的节奏。

#1 楼

Endianness
您的程序只能在小型Endian计算机上运行。如果您希望它在任何计算机上都能正常工作,则需要修复所有fwrite()调用以专门以小尾数格式进行输出。文件头。这样,您可以填写所有字段(以字节序正确的方式),然后执行一个fwrite()。从代码看来,您可以将所有字段组合到一个标头中,但是如果更有意义,则可以有多个标头(RIFF标头,格式标头,数据标头等)。

评论


\ $ \ begingroup \ $
是否有可能“填充”该结构(两个连续字段之间的孔)?如果是这样,会破坏事情吗?
\ $ \ endgroup \ $
–coderodde
15年9月22日在5:54

\ $ \ begingroup \ $
另外,我该如何处理字节序?谷歌没有太大帮助。
\ $ \ endgroup \ $
–coderodde
2015年9月22日下午6:27

\ $ \ begingroup \ $
@coderodde您确实需要注意结构填充,但是通常这些类型的标头是“正确”打包的,没有间隙。如果没有,则可以使用#pragma pack。为了提高字节序,您需要在littleendian机器上正常填充结构,或者在bigendian机器上以反向字节顺序填充结构。如果包含,则可以使用宏来帮助您执行此操作。
\ $ \ endgroup \ $
– JS1
2015年9月22日下午6:53



#2 楼

fopen在第二个参数中应该有一个b。这主要是防止Windows用\n替换会损坏文件的任何\r\n字节。

fopen( fileName, "wb" )


使fwrite参数的顺序首先与sizeof一致,然后与elements:

// Writing the RIFF header:
fwrite(&fChunkID,    sizeof(fChunkID),   1, fout);
fwrite(&fChunkSize,  sizeof(fChunkSize), 1, fout);
fwrite(&fFormat,     sizeof(fFormat),    1, fout);


说到大小,不能保证标准基元的大小在所有平台上都完全相同(long是一个特殊的难题)。而是使用stdint.h中的整数类型。

#3 楼

实际上看起来还不错。一些较小的nitpicks


您不应该转换malloc的返回值。
fByteRatefBlockAlign可以是const,因为它们仅从const值计算得出。 >我不太确定为什么WriteWavePCM中的几乎所有变量都以f为前缀。我想这是为了表明它们与正在写入文件的数据有关。由于此方法中的大多数代码都与将内容写入文件有关,这对我来说似乎很多余,而且有点分散注意力,但是YMMV。
此处使用了魔术常数fChunkSize = 36 + fSubchunk2Size;-用命名常数替换(计算或定义的-取决于位置) 36来自36。fBitsPerSample定义为16,假设short是16位(因为它与将short* sound数据写入文件有关)。并非在所有平台上都如此。因此,fBitsPerSample应该是sizeof(*sound)或要传入。

使用不一致的支撑样式:

int main(int argc, char* argv[]){


vs

if (!sound)
{



您已经定义了N_CHANNELS,但是主循环被硬编码为以2递增,因此这

for (i = 0, j = 0; i < N_SAMPLE_PAIRS * N_CHANNELS; i += 2, j++ )


应该be

for (i = 0, j = 0; i < N_SAMPLE_PAIRS * N_CHANNELS; i += N_CHANNELS, j++)




#4 楼

除了在其他答案中给出的出色建议之外,我还建议彻底检查此循环:


里面的常量。他们应该有一个明智的名字来描述他们的工作。

每当您看到此内容:您应该定义一个包含2条数据而不是一个数组的结构,在该结构中,您必须知道所有其他数据都是一个通道。诸如此类:

for (i = 0, j = 0; i < N_SAMPLE_PAIRS * N_CHANNELS; i += 2, j++ )
{
    short datum1 = 450 * ((j >> 9 | j >> 7 | j >> 2) % 128);
    short datum2 = 450 * ((j >> 11 | j >> 8 | j >> 3) % 128);

    sound[i]     = datum1; // One channel.
    sound[i + 1] = datum2; // Another channel.
}


或者其他适合您的用例的东西。由于structi现在相等,因此这也允许您只用一个控制变量编写循环。

评论


\ $ \ begingroup \ $
我不知道魔术常数是做什么的。它们影响正在产生的“音乐”,但是我也不知道它们各自如何影响任何事情。
\ $ \ endgroup \ $
–coderodde
2015年9月22日下午6:05

\ $ \ begingroup \ $
“三年前,我编写了这个简单的C程序,用于将计算机生成的音乐写入到WAV文件中” vs.“我不知道魔术常数的作用。它们会影响所产生的“音乐”,但是我不知道他们每个人如何影响任何事情”-这让我感到奇怪,但是如果没有别的事情可以很好地证明评论的重要性...
\ $ \ endgroup \ $
–八位大师
2015年9月22日15:33

\ $ \ begingroup \ $
除了@ Eight-BitGuru所说的之外,我还可以告诉你其中至少有2个做什么。数学:450 *(任何百分比为128)将量化结果并进行缩放。 (任意百分比128)将值限制在0到128之间。由于您有16位样本,并且128个值适合7位,因此它非常安静,因此您需要将其缩放到介于0和2 ^ 16。通常,您会乘以512,但如果要混合使用450,则会为您提供一些余量。当然,通过将8位值缩放为16位,您还将留有空格(因此量化)。
\ $ \ endgroup \ $
–user1118321
2015年9月22日在16:11



#5 楼

没必要在同一条件下打包很多条件:


if (!sound || !fileName || !(fout = fopen( fileName, "w" ))) 



很可能是: >
if (sound == NULL || fileName == NULL) {
    return false;
} 

FILE * fout = fopen(fileName, "wb");
if (fout == NULL) {
    return false
} 


个人喜好,但要避免在有条件的情况下进行作业,它们应该具有很好的可见性。我也更喜欢将ifNULL进行比较,而不是将其几乎不可见的!用作指针,以再次获得最大的可视性。如果参数为null或无法打开文件,则会丢失工作。没什么大不了的,但是仍然不是最理想的。 C99允许您在任何地方声明变量,因此可以将这些检查移至函数顶部。


您在此处分配给ws: >
ws = fwrite(sound, sizeof(short), pairAmount * N_CHANNELS, fout);



但是从未使用过该变量。我想这最初是为了进行错误检查?好吧,您不执行错误检查,这是非常乐观的。 Q4312079q可能会失败。您可以将它们设置为fwrite常数,以确保每次都不会在堆栈上重新分配它们。