How can I convert QBASIC PLAY Commands to Something More Contemporary?

蹲街弑〆低调 提交于 2019-12-01 04:28:34

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!

This QB play formatted string contains musical notes and duration symbols that could be converted into MIDI commands and then packaged in a midi file format. You may have to add more detailed timing and relative volume information based on some defaults.

MIDI is still considered a current format with zillions of tools and devices currently supporting it.

The "modern applications" way to play music would be to use .mid files I guess. FreeBasic includes support for music through fmod library. So you could convert the music to .MID files format, using MIDI Tracker or something like that.

There's not an easy way to do this in Qbasic. You would basically need to write a modern sound driver. You'll need to do something more hackish, like using Audio Hijack (or similar PC app) or even this $0.85 cable.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!