You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

345 lines
8.6 KiB

/** @file mmelody.c
@author M. P. Hayes, UCECE
@date 20 April 2007
@brief Play simple melodies.
*/
#include "system.h"
#include "mmelody.h"
enum {MMELODY_SCALE_SIZE = 12};
/* Assume each beat is a quarter note. */
enum {MMELODY_BEAT_FRACTION = 4};
/* Scale factor for fraction of a symbol timing. */
enum {MMELODY_DURATION_FRACTION = 4};
/* How long to release note before sounding the next one. */
#define MMELODY_RELEASE_MS 50
static void
mmelody_ticker_set (mmelody_t mmelody)
{
/* The duration of a beat varies with the time signature:
2/2 : minum (half note)
4/4 : crotchet (quarter note)
6/8, 9/8, 12/8 : dotted crotchet (one and a half quarter notes)
Currently, there is no time signature support so we assume 4/4.
This means a beat is a quarter note and a bar is four quarter
notes.
ticks_per_beat = poll_rate * 60 / speed
symbols_per_beat = symbol_fraction / MMELODY_BEAT_FRACTION
ticks_per_symbol = ticks_per_beat / symbols_per_beat
ticks_per_unit = ticks_per_symbol / MMELODY_DURATION_FRACTION
ticks_per_note = ticks_per_beat * MMELODY_BEAT_FRACTION
= ticks_per_unit * symbol_fraction
* MMELODY_DURATION_FRACTION
The duration is specified as the actual duration scaled by
MMELODY_DURATION_FRACTION so that it can specify half periods.
With symbol_fraction = 4, speed = 50, and poll_rate of 200 this
gives a result of 60. */
mmelody->unit_ticks
= (mmelody->poll_rate * 60 * MMELODY_BEAT_FRACTION)
/ (mmelody->speed * mmelody->symbol_fraction
* MMELODY_DURATION_FRACTION);
}
static void
mmelody_note_on (mmelody_t mmelody, mmelody_note_t note, uint8_t duration)
{
/* The duration is in terms of the half the duration of a symbol. */
mmelody->play_callback (mmelody->play_callback_data, note,
mmelody->volume);
mmelody->note = note;
/* Determine ticks between sounding notes (this needs to be a
minimum of 1). If we assume a release period of 50 ms,
this will give a count of 10 ticks at 200 Hz sampling. */
mmelody->ticks2 = mmelody->release_ticks;
/* Determine ticks before turning the note off. */
mmelody->ticks1 = mmelody->unit_ticks * duration - mmelody->ticks2;
}
static void
mmelody_note_off (mmelody_t mmelody)
{
mmelody->play_callback (mmelody->play_callback_data,
mmelody->note, 0);
mmelody->note = 0;
}
/* Specify the default note length in fractions of a measure (bar).
A value of 4 is the default which makes each note a quarter note. */
static void
mmelody_symbol_fraction_set (mmelody_t mmelody, uint8_t symbol_fraction)
{
mmelody->symbol_fraction = symbol_fraction;
mmelody_ticker_set (mmelody);
}
static mmelody_note_t
mmelody_char_to_note (uint8_t ch)
{
/* A = 9, B = 11, C = 0, D = 2, E = 4, F = 5, G = 7 */
static const mmelody_note_t const lookup[] = {9, 11, 0, 2, 4, 5, 7};
return lookup[ch - 'A'];
}
/* Scan next part of melody until a note or end of melody is found. */
static const char *
mmelody_scan (mmelody_t mmelody, const char *str)
{
while (1)
{
uint8_t num;
char cmd;
char modifier;
bool have_hash;
bool have_num;
mmelody_note_t note = 0;
uint8_t duration = 1;
/* Play rest at end of melody. */
if (! *str)
{
mmelody_note_off (mmelody);
return str;
}
cmd = *str++;
have_hash = *str == '#';
if (have_hash)
str++;
modifier = 0;
if (*str == '+' || *str == '-')
modifier = *str++;
have_num = 0;
num = 0;
while (*str >= '0' && *str <= '9')
{
have_num = 1;
num = num * 10 + *str++ - '0';
}
switch (cmd)
{
/* Repeat sequence from start. */
case ':':
str = mmelody->start;
continue;
/* Define start of loop. */
case '<':
/* We could implement a small stack to allow nested
loops. */
mmelody->loop_start = str;
mmelody->loop_count = 0;
continue;
/* Loop. */
case '>':
mmelody->loop_count++;
if (!num)
num = 2;
if (mmelody->loop_count < num)
{
/* Jump to start of loop. If no start of loop symbol,
jump to start. */
str = mmelody->loop_start;
if (!str)
str = mmelody->start;
}
continue;
/* Alternate endings. */
case '[':
if (mmelody->loop_count == num - 1)
continue;
/* Skip to next alternate ending, the end of loop, or end of
melody. */
while (*str && *str != '[' && *str != '>')
str++;
continue;
case '*':
if (num)
mmelody_symbol_fraction_set (mmelody, num);
continue;
case '@':
if (num)
mmelody_speed_set (mmelody, num);
continue;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
note = mmelody_char_to_note (cmd);
if (have_hash)
note++;
if (have_num)
mmelody->octave = num;
if (modifier == '+')
note += MMELODY_SCALE_SIZE;
if (modifier == '-')
note -= MMELODY_SCALE_SIZE;
/* Convert note to MIDI note number. */
note += (mmelody->octave + 1) * MMELODY_SCALE_SIZE;
/* Fall through. */
case ' ':
while (*str == '/')
{
duration++;
str++;
}
duration = duration * MMELODY_DURATION_FRACTION;
if (*str == '.')
{
/* Dotted quarter note. */
duration += duration / 2;
str++;
}
else if (*str == ',')
{
/* Eighth note. */
str++;
duration = duration / 2;
}
mmelody_note_on (mmelody, note, duration);
return str;
break;
default:
return str;
break;
}
}
return str;
}
void
mmelody_play (mmelody_t mmelody, const char *str)
{
mmelody->cur = mmelody->start = str;
mmelody->loop_start = 0;
mmelody->loop_count = 0;
mmelody->octave = MMELODY_OCTAVE_DEFAULT;
/* Default to quarter notes. */
mmelody_symbol_fraction_set (mmelody, 4);
/* Stop what is currently sounding. */
mmelody_note_off (mmelody);
mmelody->ticks1 = 0;
mmelody->ticks2 = 1;
}
/* Set (base) speed in beats per minute (BPM). */
void
mmelody_speed_set (mmelody_t mmelody, mmelody_speed_t speed)
{
/* With 8 bits for the tempo (in bpm), the max tempo is 255 bpm.
This corresponds to 4.25 beats per second. If the minimum time
to release a note is 1 / 8 of a quarter note, then we need to
poll at a rate of at least 4.25 * 8 = 34 times per second. */
mmelody->speed = speed;
mmelody_ticker_set (mmelody);
}
/* Set volume as percentage of maximum. */
void
mmelody_volume_set (mmelody_t mmelody, mmelody_volume_t volume)
{
mmelody->volume = volume;
}
bool
mmelody_active_p (mmelody_t mmelody)
{
return mmelody->cur != 0;
}
void
mmelody_update (mmelody_t mmelody)
{
if (!mmelody->cur)
return;
if (mmelody->ticks1)
{
mmelody->ticks1--;
if (!mmelody->ticks1)
mmelody_note_off (mmelody);
}
else
{
if (mmelody->ticks2)
{
mmelody->ticks2--;
if (!mmelody->ticks2)
mmelody->cur = mmelody_scan (mmelody, mmelody->cur);
}
}
}
mmelody_t
mmelody_init (mmelody_obj_t *mmelody,
uint16_t poll_rate,
mmelody_callback_t play_callback,
void *play_callback_data)
{
mmelody->poll_rate = poll_rate;
mmelody->play_callback = play_callback;
mmelody->play_callback_data = play_callback_data;
mmelody->volume = 100;
mmelody->note = 0;
mmelody->ticks1 = 0;
mmelody->ticks2 = 0;
mmelody->release_ticks = (poll_rate * MMELODY_RELEASE_MS) / 1000;
mmelody->symbol_fraction = 4;
mmelody->speed = MMELODY_SPEED_DEFAULT;
return mmelody;
}