I have play commands in my QB application like this:
PLAY "MSe8f#4f#8f#8g8a8b4.a4.g4.f#4.o0b8o1e8e8e4d8e2."
I'd like to convert these somehow into something modern applications could use. Any thoughts? I'm currently messing around with the application in FreeBasic.
The PLAY command is for playing musical notes,octave. It can only play one note at a time. More than that the play command can play a complex stored "song" This is accomplished through the use of a song string. For string command detail, see QBasic/Appendix#PLAY.
Mode commands: MF Plays music in foreground. MB Plays music in background.
You can convert your Play strings into WAV files with a tool like this (C code):
// 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;
}
Compile with gcc:
gcc play2wav.c -o play2wav.exe
Test file, JingleBells.txt:
t200l4o2mneel2el4eel2el4egl3cl8dl1el4ffl3fl8fl4fel2el8eel4edde
l2dgl4eel2el4eel2el4egl3cl8dl1el4ffl3fl8fl4fel2el8efl4ggfdl2c
Run:
play2wav.exe JingleBells.txt JingleBells.wav
Enjoy listening to JingleBells.wav!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With