Skip to content

Instantly share code, notes, and snippets.

@pmachapman
Created April 23, 2016 02:19
Show Gist options
  • Save pmachapman/d47fa1633505db52a9ecbdbff07deefc to your computer and use it in GitHub Desktop.
Save pmachapman/d47fa1633505db52a9ecbdbff07deefc to your computer and use it in GitHub Desktop.
// file: play2wav.c
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358
#endif
double Note2Freq(int Note) // Note=1 = C1 (32.7032 Hz), Note=84 = B7 (3951.07 Hz)
{
double f = 0;
if (Note > 0)
f = 440 * exp(log(2) * (Note - 46) / 12);
return f;
}
int Name2SemitonesFromC(char c)
{
static const int semitonesFromC[7] = { 9, 11, 0, 2, 4, 5, 7 }; // A,B,C,D,E,F,G
if (c < 'A' && c > 'G') return -1;
return semitonesFromC[c - 'A'];
}
typedef struct tPlayer
{
enum
{
StateParsing,
StateGenerating,
} State;
int Tempo;
int Duration;
int Octave;
enum
{
ModeNormal,
ModeLegato,
ModeStaccato,
} Mode;
int Note;
double NoteDuration;
double NoteTime;
unsigned SampleRate;
} tPlayer;
void PlayerInit(tPlayer* pPlayer, unsigned SampleRate)
{
pPlayer->State = StateParsing;
pPlayer->Tempo = 120; // [32,255] quarter notes per minute
pPlayer->Duration = 4; // [1,64]
pPlayer->Octave = 4; // [0,6]
pPlayer->Mode = ModeNormal;
pPlayer->Note = 0;
pPlayer->SampleRate = SampleRate;
}
int PlayerGetSample(tPlayer* pPlayer, const char** ppMusicString, short* pSample)
{
int number;
int note = 0;
int duration = 0;
int dotCnt = 0;
double sample;
double freq;
*pSample = 0;
while (pPlayer->State == StateParsing)
{
char c = **ppMusicString;
if (c == '\0') return 0;
++*ppMusicString;
if (isspace(c)) continue;
c = toupper(c);
switch (c)
{
case 'O':
c = **ppMusicString;
if (c < '0' || c > '6') return 0;
pPlayer->Octave = c - '0';
++*ppMusicString;
break;
case '<':
if (pPlayer->Octave > 0) pPlayer->Octave--;
break;
case '>':
if (pPlayer->Octave < 6) pPlayer->Octave++;
break;
case 'M':
c = toupper(**ppMusicString);
switch (c)
{
case 'L':
pPlayer->Mode = ModeLegato;
break;
case 'N':
pPlayer->Mode = ModeNormal;
break;
case 'S':
pPlayer->Mode = ModeStaccato;
break;
case 'B':
case 'F':
// skip MB and MF
break;
default:
return 0;
}
++*ppMusicString;
break; // ML/MN/MS, MB/MF
case 'L':
case 'T':
number = 0;
for (;;)
{
char c2 = **ppMusicString;
if (isdigit(c2))
{
number = number * 10 + c2 - '0';
++*ppMusicString;
}
else break;
}
switch (c)
{
case 'L':
if (number < 1 || number > 64) return 0;
pPlayer->Duration = number;
break;
case 'T':
if (number < 32 || number > 255) return 0;
pPlayer->Tempo = number;
break;
}
break; // Ln/Tn
case 'A': case 'B': case 'C': case 'D':
case 'E': case 'F': case 'G':
case 'N':
case 'P':
switch (c)
{
case 'A': case 'B': case 'C': case 'D':
case 'E': case 'F': case 'G':
note = 1 + pPlayer->Octave * 12 + Name2SemitonesFromC(c);
break; // A...G
case 'P':
note = 0;
break; // P
case 'N':
number = 0;
for (;;)
{
char c2 = **ppMusicString;
if (isdigit(c2))
{
number = number * 10 + c2 - '0';
++*ppMusicString;
}
else break;
}
if (number < 0 || number > 84) return 0;
note = number;
break; // N
} // got note #
if (c >= 'A' && c <= 'G')
{
char c2 = **ppMusicString;
if (c2 == '+' || c2 == '#')
{
if (note < 84) note++;
++*ppMusicString;
}
else if (c2 == '-')
{
if (note > 1) note--;
++*ppMusicString;
}
} // applied sharps and flats
duration = pPlayer->Duration;
if (c != 'N')
{
number = 0;
for (;;)
{
char c2 = **ppMusicString;
if (isdigit(c2))
{
number = number * 10 + c2 - '0';
++*ppMusicString;
}
else break;
}
if (number < 0 || number > 64) return 0;
if (number > 0) duration = number;
} // got note duration
while (**ppMusicString == '.')
{
dotCnt++;
++*ppMusicString;
} // got dots
pPlayer->Note = note;
pPlayer->NoteDuration = 1.0 / duration;
while (dotCnt--)
{
duration *= 2;
pPlayer->NoteDuration += 1.0 / duration;
}
pPlayer->NoteDuration *= 60 * 4. / pPlayer->Tempo; // in seconds now
pPlayer->NoteTime = 0;
pPlayer->State = StateGenerating;
break; // A...G/N/P
default:
return 0;
} // switch (c)
}
// pPlayer->State == StateGenerating
// Calculate the next sample for the current note
sample = 0;
// QuickBasic Play() frequencies appear to be 1 octave higher than
// on the piano.
freq = Note2Freq(pPlayer->Note) * 2;
if (freq > 0)
{
double f = freq;
while (f < pPlayer->SampleRate / 2 && f < 8000) // Cap max frequency at 8 KHz
{
sample += exp(-0.125 * f / freq) * sin(2 * M_PI * f * pPlayer->NoteTime);
f += 2 * freq; // Use only odd harmonics
}
sample *= 15000;
sample *= exp(-pPlayer->NoteTime / 0.5); // Slow decay
}
if ((pPlayer->Mode == ModeNormal && pPlayer->NoteTime >= pPlayer->NoteDuration * 7 / 8) ||
(pPlayer->Mode == ModeStaccato && pPlayer->NoteTime >= pPlayer->NoteDuration * 3 / 4))
sample = 0;
if (sample > 32767) sample = 32767;
if (sample < -32767) sample = -32767;
*pSample = (short)sample;
pPlayer->NoteTime += 1.0 / pPlayer->SampleRate;
if (pPlayer->NoteTime >= pPlayer->NoteDuration)
pPlayer->State = StateParsing;
return 1;
}
int PlayToFile(const char* pFileInName, const char* pFileOutName, unsigned SampleRate)
{
int err = EXIT_FAILURE;
FILE *fileIn = NULL, *fileOut = NULL;
tPlayer player;
short sample;
char* pMusicString = NULL;
const char* p;
size_t sz = 1, len = 0;
char c;
unsigned char uc;
unsigned long sampleCnt = 0, us;
if ((fileIn = fopen(pFileInName, "rb")) == NULL)
{
fprintf(stderr, "can't open file \"%s\"\n", pFileInName);
goto End;
}
if ((fileOut = fopen(pFileOutName, "wb")) == NULL)
{
fprintf(stderr, "can't create file \"%s\"\n", pFileOutName);
goto End;
}
if ((pMusicString = malloc(sz)) == NULL)
{
NoMemory:
fprintf(stderr, "can't allocate memory\n");
goto End;
}
// Load the input file into pMusicString[]
while (fread(&c, 1, 1, fileIn))
{
pMusicString[len++] = c;
if (len == sz)
{
char* p;
sz *= 2;
if (sz < len)
goto NoMemory;
p = realloc(pMusicString, sz);
if (p == NULL)
goto NoMemory;
pMusicString = p;
}
}
pMusicString[len] = '\0'; // Make pMusicString[] an ASCIIZ string
// First, a dry run to simply count samples (needed for the WAV header)
PlayerInit(&player, SampleRate);
p = pMusicString;
while (PlayerGetSample(&player, &p, &sample))
sampleCnt++;
if (p != pMusicString + len)
{
fprintf(stderr,
"Parsing error near byte %u: \"%c%c%c\"\n",
(unsigned)(p - pMusicString),
(p > pMusicString) ? p[-1] : ' ',
p[0],
(p - pMusicString + 1 < len) ? p[1] : ' ');
goto End;
}
// Write the output file
// ChunkID
fwrite("RIFF", 1, 4, fileOut);
// ChunkSize
us = 36 + 2 * sampleCnt;
uc = us % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 / 256 / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
// Format + Subchunk1ID
fwrite("WAVEfmt ", 1, 8, fileOut);
// Subchunk1Size
uc = 16;
fwrite(&uc, 1, 1, fileOut);
uc = 0;
fwrite(&uc, 1, 1, fileOut);
fwrite(&uc, 1, 1, fileOut);
fwrite(&uc, 1, 1, fileOut);
// AudioFormat
uc = 1;
fwrite(&uc, 1, 1, fileOut);
uc = 0;
fwrite(&uc, 1, 1, fileOut);
// NumChannels
uc = 1;
fwrite(&uc, 1, 1, fileOut);
uc = 0;
fwrite(&uc, 1, 1, fileOut);
// SampleRate
uc = SampleRate % 256;
fwrite(&uc, 1, 1, fileOut);
uc = SampleRate / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
uc = 0;
fwrite(&uc, 1, 1, fileOut);
fwrite(&uc, 1, 1, fileOut);
// ByteRate
us = (unsigned long)SampleRate * 2;
uc = us % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 / 256 / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
// BlockAlign
uc = 2;
fwrite(&uc, 1, 1, fileOut);
uc = 0;
fwrite(&uc, 1, 1, fileOut);
// BitsPerSample
uc = 16;
fwrite(&uc, 1, 1, fileOut);
uc = 0;
fwrite(&uc, 1, 1, fileOut);
// Subchunk2ID
fwrite("data", 1, 4, fileOut);
// Subchunk2Size
us = sampleCnt * 2;
uc = us % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
uc = us / 256 / 256 / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
// Data
PlayerInit(&player, SampleRate);
p = pMusicString;
while (PlayerGetSample(&player, &p, &sample))
{
uc = (unsigned)sample % 256;
fwrite(&uc, 1, 1, fileOut);
uc = (unsigned)sample / 256 % 256;
fwrite(&uc, 1, 1, fileOut);
}
err = EXIT_SUCCESS;
End:
if (pMusicString != NULL) free(pMusicString);
if (fileOut != NULL) fclose(fileOut);
if (fileIn != NULL) fclose(fileIn);
return err;
}
int main(int argc, char** argv)
{
if (argc == 3)
// return PlayToFile(argv[1], argv[2], 44100); // Use this for 44100 sample rate
return PlayToFile(argv[1], argv[2], 16000);
printf("Usage:\n play2wav <Input-QBASIC-Play-String-file> <Output-Wav-file>\n");
return EXIT_FAILURE;
}
@guilt
Copy link

guilt commented Aug 12, 2017

Some code reading notes:

  1. Player->State is a way to combine iteration (do we have more music to parse) with the data, and it could be refactored outside the while loop on Line #339.

  2. The file reading part is a little bit more complicated than required; I'd have used fseek, ftell, malloc to read the contents into a buffer; Line #311; Read all bytes into a char array, basically.

  3. The command parsing/dispatch code in Line #87 is explained as follows:

O? - Set Octave to ?
< - Reduce Octave
> - Increase Octave

L? - Length/Duration to Play Note as Specified by ? (1, 1/4 etc.),
M? - Set Mode based on ?, one of:
L - Legato (Full Length of Note set by L)
N - Normal (7/8th of the Length of Note set by L)
S - Staccato (3/4th of the Length of Note set by L)
B - Play music in Background (Another Thread) - Hence Ignored
F - Play music in foreground (Same Thread) - Hence Ignored

. - Note Length setter. The more dots, the Length of the note goes up with each element in the series 1/2 + 1/4 + 1/8

T? - Set Tempo to Tempo specified by ?

P? - Pause for the duration Specified by ? (no music played)

A?
B?
C?
D?
E?
F?
G? - Actual Note in currently set Octave ? = + # (Sharp) - (Flat), a half note higher or lower
N? - Set Note to ? between [0, 84)

  1. Now, for each note, you map it to a number based on Octave, Sharp, then to the frequency based on an exponential scale on Line #12.

  2. You generate the waveform at every point in this Line #254; You add a exponential dampening to it in Line #259; Finally, you take into account the Staccato/Legato and truncate as required in Line #262.

  3. Then you clip it in Line #266.

  4. You write this as a 16 bit value in Line #444; The WAV File header is itself written in Line #356.

  5. You keep repeating this thanks to the while loop. There are two passes of this data done; One to simply get the time/length in Line #339 and the second to export out the sample itself into the WAV file in Line #441.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment