Skip to content

Instantly share code, notes, and snippets.

@guilt
Created July 9, 2019 23:55
Show Gist options
  • Save guilt/699d8bc99409a9e5edabd19886b51919 to your computer and use it in GitHub Desktop.
Save guilt/699d8bc99409a9e5edabd19886b51919 to your computer and use it in GitHub Desktop.
// https://stackoverflow.com/questions/11355353/how-can-i-convert-qbasic-play-commands-to-something-more-contemporary
// file: play2wav.c by Alexey Frunze
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358
#endif
#define NOTE_SCALE 46
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 - NOTE_SCALE) / 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 (strcmp("-", pFileInName) == 0)
fileIn = stdin;
else if ((fileIn = fopen(pFileInName, "rb")) == NULL) {
fprintf(stderr, "can't open file \"%s\"\n", pFileInName);
goto End;
}
if (strcmp("-", pFileOutName) == 0)
fileOut = stdout;
else 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], ((unsigned)(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) {
int sampleRate = 44100;
char* inputTextFile = "-";
char* outputWaveFile = "-";
if (argc > 1) {
inputTextFile = argv[1];
if (argc > 2) {
outputWaveFile = argv[2];
}
}
return PlayToFile(inputTextFile, outputWaveFile, sampleRate);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment