Skip to content
tune_build.c 15 KiB
Newer Older
Mario Lang's avatar
Mario Lang committed
/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2018 by The BRLTTY Developers.
Mario Lang's avatar
Mario Lang committed
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
Mario Lang's avatar
Mario Lang committed
 *
 * Web Page: http://brltty.com/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <errno.h>

#include "log.h"
#include "tune_build.h"
#include "notes.h"
#include "charset.h"
typedef unsigned int TuneNumber;

typedef struct {
  const char *name;
  TuneNumber minimum;
  TuneNumber maximum;
  TuneNumber current;
} TuneParameter;

struct TuneBuilderStruct {
  TuneStatus status;

  struct {
    ToneElement *array;
    unsigned int size;
    unsigned int count;
  } tones;

  signed char accidentals[NOTES_PER_SCALE];
  TuneParameter duration;
  TuneParameter note;
  TuneParameter octave;
  TuneParameter percentage;
  TuneParameter tempo;

  struct {
    const wchar_t *text;
    const char *name;
    unsigned int index;
  } source;
};

static const wchar_t *noteLetters = WS_C("cdefgab");
Mario Lang's avatar
Mario Lang committed
static const unsigned char noteOffsets[] = {0, 2, 4, 5, 7, 9, 11};
static const signed char scaleAccidentals[] = {0, 2, 4, -1, 1, 3, 5};
static const unsigned char accidentalTable[] = {3, 0, 4, 1, 5, 2, 6};

typedef struct {
  const wchar_t *name;
Mario Lang's avatar
Mario Lang committed
  signed char accidentals;
} ModeEntry;

static const ModeEntry modeTable[] = {
  {.name=WS_C("major"), .accidentals=0},
  {.name=WS_C("minor"), .accidentals=-3},

  {.name=WS_C("ionian"), .accidentals=0},
  {.name=WS_C("dorian"), .accidentals=-2},
  {.name=WS_C("phrygian"), .accidentals=-4},
  {.name=WS_C("lydian"), .accidentals=1},
  {.name=WS_C("mixolydian"), .accidentals=-1},
  {.name=WS_C("aeolian"), .accidentals=-3},
  {.name=WS_C("locrian"), .accidentals=-5},
Mario Lang's avatar
Mario Lang committed
};
static const unsigned char modeCount = ARRAY_COUNT(modeTable);

static void
logSyntaxError (TuneBuilder *tb, const char *message) {
  tb->status = TUNE_STATUS_SYNTAX;
  logMessage(LOG_ERR, "tune error: %s[%u]: %s: %" PRIws,
             tb->source.name, tb->source.index,
             message, tb->source.text);
addTone (TuneBuilder *tb, const ToneElement *tone) {
  if (tb->tones.count == tb->tones.size) {
    unsigned int newSize = tb->tones.size? (tb->tones.size << 1): 1;
Mario Lang's avatar
Mario Lang committed
    ToneElement *newArray;

    if (!(newArray = realloc(tb->tones.array, ARRAY_SIZE(newArray, newSize)))) {
      tb->status = TUNE_STATUS_FATAL;
Mario Lang's avatar
Mario Lang committed
      logMallocError();
      return 0;
    }

    tb->tones.array = newArray;
    tb->tones.size = newSize;
  tb->tones.array[tb->tones.count++] = *tone;
addNote (TuneBuilder *tb, unsigned char note, int duration) {
Mario Lang's avatar
Mario Lang committed
  if (!duration) return 1;

  ToneElement tone = TONE_PLAY(duration, getNoteFrequency(note));
  return addTone(tb, &tone);
Mario Lang's avatar
Mario Lang committed
}

static int
parseNumber (
  TuneBuilder *tb,
  TuneNumber *number, const wchar_t **operand, int required,
Mario Lang's avatar
Mario Lang committed
  const TuneNumber minimum, const TuneNumber maximum,
  const char *name
) {
  const char *problem = "invalid";

  if (isdigit(**operand)) {
    errno = 0;
    wchar_t *end;
    unsigned long ul = wcstoul(*operand, &end, 10);
Mario Lang's avatar
Mario Lang committed

    if (errno) goto PROBLEM_ENCOUNTERED;
    if (ul > UINT_MAX) goto PROBLEM_ENCOUNTERED;

    if (ul < minimum) goto PROBLEM_ENCOUNTERED;
    if (ul > maximum) goto PROBLEM_ENCOUNTERED;

    *number = ul;
    *operand = end;
    return 1;
  }

  if (!required) return 1;
  problem = "missing";

PROBLEM_ENCOUNTERED:
  if (name) {
    char message[0X80];
    snprintf(message, sizeof(message), "%s %s", problem, name);
    logSyntaxError(tb, message);
Mario Lang's avatar
Mario Lang committed
  }

  return 0;
}

static int
parseParameter (
  TuneBuilder *tb, TuneParameter *parameter,
  const wchar_t **operand, int required
  return parseNumber(tb, &parameter->current, operand, required,
Mario Lang's avatar
Mario Lang committed
                     parameter->minimum, parameter->maximum, parameter->name);
}

static int
parseOptionalParameter (TuneBuilder *tb, TuneParameter *parameter, const wchar_t **operand) {
  return parseParameter(tb, parameter, operand, 0);
Mario Lang's avatar
Mario Lang committed
}

static int
parseRequiredParameter (TuneBuilder *tb, TuneParameter *parameter, const wchar_t **operand) {
  return parseParameter(tb, parameter, operand, 1);
Mario Lang's avatar
Mario Lang committed
}

static int
parsePercentage (TuneBuilder *tb, const wchar_t **operand) {
  return parseRequiredParameter(tb, &tb->percentage, operand);
Mario Lang's avatar
Mario Lang committed
}

static int
parseTempo (TuneBuilder *tb, const wchar_t **operand) {
  return parseRequiredParameter(tb, &tb->tempo, operand);
Mario Lang's avatar
Mario Lang committed
}

static void
setCurrentDuration (TuneBuilder *tb, TuneNumber multiplier, TuneNumber divisor) {
  tb->duration.current = (60000 * multiplier) / (tb->tempo.current * divisor);
Mario Lang's avatar
Mario Lang committed
}

static void
setBaseDuration (TuneBuilder *tb) {
  setCurrentDuration(tb, 1, 1);
Mario Lang's avatar
Mario Lang committed
}

static int
parseDuration (TuneBuilder *tb, const wchar_t **operand, int *duration) {
Mario Lang's avatar
Mario Lang committed
  if (**operand == '@') {
    *operand += 1;

    TuneParameter parameter = tb->duration;
    if (!parseRequiredParameter(tb, &parameter, operand)) return 0;
Mario Lang's avatar
Mario Lang committed
    *duration = parameter.current;
  } else {
    const wchar_t *durationOperand = *operand;
Mario Lang's avatar
Mario Lang committed

    TuneNumber multiplier;
    TuneNumber divisor;

    if (**operand == '*') {
      *operand += 1;

      if (!parseNumber(tb, &multiplier, operand, 1, 1, 16, "duration multiplier")) {
Mario Lang's avatar
Mario Lang committed
        return 0;
      }
    } else {
      multiplier = 1;
    }

    if (**operand == '/') {
      *operand += 1;

      if (!parseNumber(tb, &divisor, operand, 1, 1, 128, "duration divisor")) {
Mario Lang's avatar
Mario Lang committed
        return 0;
      }
    } else {
      divisor = 1;
    }

    if (*operand != durationOperand) setCurrentDuration(tb, multiplier, divisor);
    *duration = tb->duration.current;
  tb->duration.current = *duration;
Mario Lang's avatar
Mario Lang committed

  {
    int increment = *duration;

    while (**operand == '.') {
      *duration += (increment /= 2);
      *operand += 1;
    }
  }

  return 1;
}

static TuneNumber
toOctave (TuneNumber note) {
  return note / NOTES_PER_OCTAVE;
}

static void
setOctave (TuneBuilder *tb) {
  tb->octave.current = toOctave(tb->note.current);
Mario Lang's avatar
Mario Lang committed
}

static void
setAccidentals (TuneBuilder *tb, int accidentals) {
Mario Lang's avatar
Mario Lang committed
  int quotient = accidentals / NOTES_PER_SCALE;
  int remainder = accidentals % NOTES_PER_SCALE;

  for (unsigned int index=0; index<ARRAY_COUNT(tb->accidentals); index+=1) {
    tb->accidentals[index] = quotient;
Mario Lang's avatar
Mario Lang committed
  }

  while (remainder > 0) {
    tb->accidentals[accidentalTable[--remainder]] += 1;
Mario Lang's avatar
Mario Lang committed
  }

  while (remainder < 0) {
    tb->accidentals[accidentalTable[NOTES_PER_SCALE + remainder++]] -= 1;
parseNoteLetter (unsigned char *index, const wchar_t **operand) {
  const wchar_t *letter = wcschr(noteLetters, **operand);
Mario Lang's avatar
Mario Lang committed

  if (!letter) return 0;
  if (!*letter) return 0;

  *index = letter - noteLetters;
  *operand += 1;
  return 1;
}

static int
parseMode (TuneBuilder *tb, int *accidentals, const wchar_t **operand) {
  const wchar_t *from = *operand;
Mario Lang's avatar
Mario Lang committed
  if (!isalpha(*from)) return 1;

  const wchar_t *to = from;
Mario Lang's avatar
Mario Lang committed
  while (isalpha(*++to));
  unsigned int length = to - from;

  const ModeEntry *mode = NULL;
  const ModeEntry *current = modeTable;
  const ModeEntry *end = current + modeCount;

  while (current < end) {
    if (wcsncmp(current->name, from, length) == 0) {
Mario Lang's avatar
Mario Lang committed
      if (mode) {
        logSyntaxError(tb, "ambiguous mode");
Mario Lang's avatar
Mario Lang committed
        return 0;
      }

      mode = current;
    }

    current += 1;
  }

  if (!mode) {
    logSyntaxError(tb, "unrecognized mode");
Mario Lang's avatar
Mario Lang committed
    return 0;
  }

  *accidentals += mode->accidentals;
  *operand = to;
  return 1;
}

static int
parseKeySignature (TuneBuilder *tb, const wchar_t **operand) {
Mario Lang's avatar
Mario Lang committed
  int accidentals;
  int increment;

  {
    unsigned char index;

    if (parseNoteLetter(&index, operand)) {
      accidentals = scaleAccidentals[index];
      increment = NOTES_PER_SCALE;
      if (!parseMode(tb, &accidentals, operand)) return 0;
Mario Lang's avatar
Mario Lang committed
    } else {
      accidentals = 0;
      increment = 1;
    }
  }

  TuneNumber count = 0;
  if (!parseNumber(tb, &count, operand, 0, 1, NOTES_PER_OCTAVE-1, "accidental count")) {
Mario Lang's avatar
Mario Lang committed
    return 0;
  }

  int haveCount = count != 0;
  wchar_t accidental = **operand;
Mario Lang's avatar
Mario Lang committed

  switch (accidental) {
    case '-':
      increment = -increment;
    case '+':
      if (haveCount) {
        *operand += 1;
      } else {
        do {
          count += 1;
        } while (*++*operand == accidental);
      }
      break;

    default:
      if (!haveCount) break;
      logSyntaxError(tb, "accidental not specified");
Mario Lang's avatar
Mario Lang committed
      return 0;
  }

  accidentals += increment * count;
  setAccidentals(tb, accidentals);
Mario Lang's avatar
Mario Lang committed
  return 1;
}

static int
parseNote (TuneBuilder *tb, const wchar_t **operand, unsigned char *note) {
Mario Lang's avatar
Mario Lang committed
  int noteNumber;

  if (**operand == 'r') {
    *operand += 1;
    noteNumber = 0;
  } else {
    int defaultAccidentals = 0;

    if (**operand == 'n') {
      *operand += 1;
      TuneParameter parameter = tb->note;
      if (!parseRequiredParameter(tb, &parameter, operand)) return 0;
Mario Lang's avatar
Mario Lang committed
      noteNumber = parameter.current;
    } else {
      unsigned char noteIndex;
      if (!parseNoteLetter(&noteIndex, operand)) return 0;

      const wchar_t *octaveOperand = *operand;
      TuneParameter octave = tb->octave;
      if (!parseOptionalParameter(tb, &octave, operand)) return 0;
Mario Lang's avatar
Mario Lang committed

      noteNumber = (octave.current * NOTES_PER_OCTAVE) + noteOffsets[noteIndex];
      defaultAccidentals = tb->accidentals[noteIndex];
Mario Lang's avatar
Mario Lang committed

      if (*operand == octaveOperand) {
        int adjustOctave = 0;
        TuneNumber previousNote = tb->note.current;
Mario Lang's avatar
Mario Lang committed
        TuneNumber currentNote = noteNumber;

        if (currentNote < previousNote) {
          currentNote += NOTES_PER_OCTAVE;
          if ((currentNote - previousNote) <= 3) adjustOctave = 1;
        } else if (currentNote > previousNote) {
          currentNote -= NOTES_PER_OCTAVE;
          if ((previousNote - currentNote) <= 3) adjustOctave = 1;
        }

        if (adjustOctave) noteNumber = currentNote;
      }
    }

    tb->note.current = noteNumber;
    setOctave(tb);
      wchar_t accidental = **operand;
Mario Lang's avatar
Mario Lang committed

      switch (accidental) {
        {
          int increment;

        case '+':
          increment = 1;
          goto doAccidental;

        case '-':
          increment = -1;
          goto doAccidental;

        doAccidental:
          do {
            noteNumber += increment;
          } while (*++*operand == accidental);

          break;
        }

        case '=':
          *operand += 1;
          break;

        default:
          noteNumber += defaultAccidentals;
          break;
      }
    }

    {
      const unsigned char lowestNote = getLowestNote();
      const unsigned char highestNote = getHighestNote();

      if (noteNumber < lowestNote) {
        logSyntaxError(tb, "note too low");
Mario Lang's avatar
Mario Lang committed
        return 0;
      }

      if (noteNumber > highestNote) {
        logSyntaxError(tb, "note too high");
Mario Lang's avatar
Mario Lang committed
        return 0;
      }
    }
  }

  *note = noteNumber;
  return 1;
}

static int
parseTone (TuneBuilder *tb, const wchar_t **operand) {
Mario Lang's avatar
Mario Lang committed
  while (1) {
    tb->source.text = *operand;
Mario Lang's avatar
Mario Lang committed
    unsigned char note;

    {
      const wchar_t *noteOperand = *operand;
      if (!parseNote(tb, operand, &note)) return *operand == noteOperand;
Mario Lang's avatar
Mario Lang committed
    }

    int duration;
    if (!parseDuration(tb, operand, &duration)) return 0;
Mario Lang's avatar
Mario Lang committed

    if (note) {
      int onDuration = (duration * tb->percentage.current) / 100;
      if (!addNote(tb, note, onDuration)) return 0;
Mario Lang's avatar
Mario Lang committed
      duration -= onDuration;
    }

    if (!addNote(tb, 0, duration)) return 0;
parseTuneOperand (TuneBuilder *tb, const wchar_t *operand) {
  tb->source.text = operand;
Mario Lang's avatar
Mario Lang committed

  switch (*operand) {
    case 'k':
      operand += 1;
      if (!parseKeySignature(tb, &operand)) return 0;
Mario Lang's avatar
Mario Lang committed
      break;

    case 'p':
      operand += 1;
      if (!parsePercentage(tb, &operand)) return 0;
Mario Lang's avatar
Mario Lang committed
      break;

    case 't':
      operand += 1;
      if (!parseTempo(tb, &operand)) return 0;
      setBaseDuration(tb);
Mario Lang's avatar
Mario Lang committed
      break;

    default:
      if (!parseTone(tb, &operand)) return 0;
Mario Lang's avatar
Mario Lang committed
      break;
  }

  if (*operand) {
    logSyntaxError(tb, "extra data");
parseTuneText (TuneBuilder *tb, const wchar_t *text) {
  tb->source.text = text;

  wchar_t buffer[wcslen(text) + 1];
  wcscpy(buffer, text);
  static const wchar_t *delimiters = WS_C(" \t\r\n");
  wchar_t *string = buffer;
  wchar_t *operand;
#if !defined(__MINGW32__) && !defined(__MSDOS__)
  wchar_t *next;
#endif /* __MINGW32__ */
  while ((operand = wcstok(string, delimiters
#ifndef __MINGW32__
                           , &next
#endif /* __MINGW32__ */
                          ))) {
Mario Lang's avatar
Mario Lang committed
    if (*operand == '#') break;
    if (!parseTuneOperand(tb, operand)) return 0;
Mario Lang's avatar
Mario Lang committed
    string = NULL;
  }

  return 1;
}

int
parseTuneString (TuneBuilder *tb, const char *string) {
  const size_t size = strlen(string) + 1;
  wchar_t characters[size];

  const char *byte = string;
  wchar_t *character = characters;

  convertUtf8ToWchars(&byte, &character, size);

  return parseTuneText(tb, characters);
}

ToneElement *
getTune (TuneBuilder *tb) {
  if (tb->status == TUNE_STATUS_OK) {
    unsigned int count = tb->tones.count;
    ToneElement *tune;

    if ((tune = malloc(ARRAY_SIZE(tune, (count + 1))))) {
      memcpy(tune, tb->tones.array, ARRAY_SIZE(tune, count));

      static const ToneElement tone = TONE_STOP();
      tune[count] = tone;

      return tune;
    } else {
      logMallocError();
    }
  }

  return NULL;
}

TuneStatus
getTuneStatus (TuneBuilder *tb) {
  return tb->status;
}

void
setTuneSourceName (TuneBuilder *tb, const char *name) {
  tb->source.name = name;
}

void
setTuneSourceIndex (TuneBuilder *tb, unsigned int index) {
  tb->source.index = index;
}

void
incrementTuneSourceIndex (TuneBuilder *tb) {
  tb->source.index += 1;
Mario Lang's avatar
Mario Lang committed
}

static inline void
setParameter (
  TuneParameter *parameter, const char *name,
  TuneNumber minimum, TuneNumber maximum, TuneNumber current
) {
  parameter->name = name;
  parameter->minimum = minimum;
  parameter->maximum = maximum;
  parameter->current = current;
}

void
resetTuneBuilder (TuneBuilder *tb) {
  tb->status = TUNE_STATUS_OK;

  tb->tones.count = 0;

  setParameter(&tb->duration, "note duration", 1, UINT16_MAX, 0);
  setParameter(&tb->note, "MIDI note number", getLowestNote(), getHighestNote(), NOTE_MIDDLE_C+noteOffsets[2]);
  setParameter(&tb->octave, "octave number", 0, 10, 0);
  setParameter(&tb->percentage, "percentage", 1, 100, 80);
  setParameter(&tb->tempo, "tempo", 40, UINT8_MAX, (60 * 2));
  setAccidentals(tb, 0);
  setBaseDuration(tb);
  setOctave(tb);

  tb->source.text = WS_C("");
  tb->source.name = "";
  tb->source.index = 0;
}
TuneBuilder *
newTuneBuilder (void) {
  TuneBuilder *tb;
  if ((tb = malloc(sizeof(*tb)))) {
    memset(tb, 0, sizeof(*tb));

    tb->tones.array = NULL;
    tb->tones.size = 0;

    resetTuneBuilder(tb);
    return tb;
  } else {
    logMallocError();
  }
  return NULL;
destroyTuneBuilder (TuneBuilder *tb) {
  if (tb->tones.array) free(tb->tones.array);
  free(tb);