Created
April 23, 2016 02:19
-
-
Save pmachapman/d47fa1633505db52a9ecbdbff07deefc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Some code reading notes:
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.
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.
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)
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.
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.
Then you clip it in Line #266.
You write this as a 16 bit value in Line #444; The WAV File header is itself written in Line #356.
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.