2
\$\begingroup\$

So, here is my code for converting musical notes stored in a text file to WAV. You can see an example of its output here.

#include <iostream>
#include <fstream>
#include <string>
#include <cmath>
#include <map>

std::map<std::string,float> notes;
int sampleRate=44100;

int main(int argc,char **argv) {
    notes["a"]=220;
    notes["as"]=233;
    notes["b"]=247;
    notes["C"]=262;
    notes["Cs"]=277;
    notes["D"]=293;
    notes["Ds"]=311;
    notes["E"]=329;
    notes["F"]=349;
    notes["Fs"]=370;
    notes["G"]=391;
    notes["Gs"]=415;
    notes["A"]=440;
    notes["As"]=466;
    notes["H"]=493;
    notes["C5"]=523;
    notes["Cs5"]=554;
    notes["D5"]=587;
    notes["Ds5"]=622;
    notes["E5"]=659;
    if (argc<2) {
        std::cerr<<"Please supply the text file with notes as an argument." <<std::endl;
        return 1;
    }
    std::ifstream input(argv[1]);
    if (argc>2)
        sampleRate=atoi(argv[2]);
    if (!input)
    {
        std::cerr<<"Can't open \"" <<argv[1] <<"\" for reading!" <<std::endl;
        return 1;
    }
    FILE *wav=std::fopen("output.wav","wb");
    if (!wav) {
        std::cerr <<"Can't open \"output.wav\" for output!" <<std::endl;
        return 1;
    }
    bool isLittleEndian;
    int testNumber=0x10;
    std::fwrite(&testNumber,sizeof(int),1,wav);
    std::fclose(wav);
    wav=std::fopen("output.wav","rb");
    char testCharacter=0;
    std::fread(&testCharacter,1,1,wav);
    std::fclose(wav);
    if (testCharacter==0x10) //The logic is: if the C library uses big endian for writing binary files, now "testCharacter" will still contain 0.
        isLittleEndian=true;
    else
        isLittleEndian=false;
    wav=std::fopen("output.wav","wb");
    if (isLittleEndian)
        std::fprintf(wav,"RIFF"); //ASCII for 0x52494646, the magic number that WAV files start with. 
    else
        std::fprintf(wav,"RIFX"); //Big endian WAV file starts with magic number 0x52494658, or, in ASCII, "RIFX". 
    int32_t ChunkSize=36+8*sampleRate*2;
    std::fwrite(&ChunkSize,4,1,wav);
    std::fprintf(wav,"WAVEfmt "); //The beginning of the header.
    int32_t Subchunk1Size=16; //PCM header is always 16 bytes.
    std::fwrite(&Subchunk1Size,4,1,wav);
    int16_t AudioFormat=1; //PCM format.
    std::fwrite(&AudioFormat,2,1,wav);
    int16_t NumChannels=1; //MONO audio.
    std::fwrite(&NumChannels,2,1,wav);
    int32_t SampleRate=sampleRate;
    std::fwrite(&SampleRate,4,1,wav);
    int32_t ByteRate=2*sampleRate; //Since we are using 16 bits per sample, and "sampleRate" samples per second.
    std::fwrite(&ByteRate,4,1,wav);
    int16_t BlockAlign=2; //Each block is two bytes.
    std::fwrite(&BlockAlign,2,1,wav);
    int16_t BitsPerSample=16;
    std::fwrite(&BitsPerSample,2,1,wav);
    std::fprintf(wav,"data");
    while (!input.eof()) {
        std::string currentNote;
        input >>currentNote;
        if (currentNote.length()==0) break;
        std::string durationString="";
        int i=0;
        while ((currentNote[i]>='0' && currentNote[i]<='9') || currentNote[i]=='.')
        {
            durationString+=currentNote.substr(i,1);
            i++;
        }
        std::cerr <<"Read note name \"" <<currentNote <<"\", the duration string is: " <<durationString <<std::endl;
        int noteDuration=3*sampleRate/std::stof(durationString);
        std::string fullNoteName=currentNote.substr(i);
        std::cerr <<"Playing note \"" <<fullNoteName <<"\" for " <<noteDuration <<" samples." <<std::endl;
        for (int i=0; i<noteDuration; i++) {
            float currentFrequency=notes[fullNoteName];
            float baseFrequency=sin(2*M_PI*currentFrequency*i/sampleRate)*16384;
            float secondHarmony=sin(2*M_PI*2*currentFrequency*i/sampleRate+M_PI/4)*4096;
            float thirdHarmony=sin(2*M_PI*3*currentFrequency*i/sampleRate+M_PI/2)*1024;
            float fourthHarmony=sin(2*M_PI*4*currentFrequency*i/sampleRate+M_PI/2)*512;
            float currentAmplitude=(baseFrequency+secondHarmony+thirdHarmony+fourthHarmony)*std::exp(-(float)(2*i+sampleRate)/(sampleRate)); //Attenuation. 
            int16_t numberToBeWritten=(fullNoteName=="P")?(0):(currentAmplitude);
            numberToBeWritten+=std::rand()%256-128; //A bit of noise makes it sound better.
            std::fwrite(&numberToBeWritten,2,1,wav);
        }
    }
    std::fclose(wav);
}

So, what do you think about it?

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$
  • Don't put everything in main.

  • Prefer an initializer list map constructor:

    std::map<std::string,float> notes {
        {"a", 220},
        {"as", 233},
        ....
    };
    
  • Testing endianness is kinda convoluted. Consider htons: network byte order is big endian.

    bool is_little_endian()
    {
        uint16_t x = 0x0055;
        uint16_t y = htons(x);
        return x != y;
    }
    
  • while (!inpit.eof()) is wrong.

  • It is very unclear how the input file is formatted. In any case, manual testing for numbers and decimal points is likely unnecessary and error prone. std::strtof will do everything you want with the way less effort and better reliability.

  • I am not that versed in music to figure out what is going on with those harmonies. It looks suspicious that the phase shift is \$\dfrac{\pi}{4}\$ for the second harmony, and \$\dfrac{\pi}{2}\$ for the third and fourth. Also, the amplitudes of those harmonies look like magic numbers.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.