Class to play tones, various sounds and music in MML format using a PWM channel.
play.cpp
- Committer:
- paulg
- Date:
- 2014-05-06
- Revision:
- 1:67056b9df9ff
File content as of revision 1:67056b9df9ff:
/****************************************************************************** * File: play.cpp * Author: Paul Griffith * Created: 28 Mar 2014 * Last Edit: see below * Version: see below * * Description: * Play melody from music data written in Music Macro Language (MML) format. * Part of PwmSound class. * * Copyright (c) 2014 Paul Griffith, MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * * Modifications: * Ver Date By Details * 0.00 28Mar14 PG File created. * 1.00 06May14 PG Initial release. * ******************************************************************************/ /* * Music Macro Language (MML) is a music description language used in sequencing * music on computer and video game systems. For further details refer to the * Wikipedia article of the same name. * * There are many dialects of MML. The one used here is essentially that of * the PLAY statement from Microsoft GW-BASIC. The original Microsoft * documentation is available online by following the References from the * Wikipedia GW-BASIC article. The MML data format is described below: * * A-G Play note using the letter names of the scale. The letter may be * followed by either a # or + for a sharp, or a - for a flat. Any note * letter followed by a #, + or - must refer to a black key on a piano. * * K Keyboard. Stops play() polling the PC keyboard during playback. * If K is not found, any PC keystroke will stop playback. * * Ln Sets the length of each note. n is a decimal number between 1 and 64 * L1 is a whole note (semi-breve), L4 is a quarter note (crotchet) and * so on. The L value persists until the next L command is encountered. * * A length number may also follow a note letter name to change the length * for that note only. For example, D8 is equivalent to L8D. * * ML Music Legato. The note is played the for the full length of its * specified time. There is no rest between notes. * * MN Music Normal. The note is played for 7/8 of its specified time, and * 1/8 of the specified time is a rest between notes. This is the default. * * MS Music Staccato. The note is played for 3/4 of its specified time, and * 1/4 of the specified time is a rest between notes. * * Nn Play note. n is a decimal number between 0 and 84. n = 1 is the lowest * note and n = 84 is the highest note. n set to 0 indicates a rest. N can * be used as an alternative to defining a note by an octave and a letter. * For example, N37 = middle C. * * On Sets the current octave. n is a decimal number between 0 and 6. The * default octave is 4. Middle C starts octave 3, i.e. O3C = middle C. * * Note: The above convention differs from standard piano octave numbering * where middle C starts octave 4, i.e. O4C = middle C. * * Pn Pause, or rest, for a length defined by n. P works in the same way as * the L command above. For example, P2 = a half rest. * * Qn Sets the tonal quality (timbre). n is a decimal number between 1 and 4. * It sets the PWM duty cycle to n / 8, (i.e. 12.5%, 25%, 37.5% or 50%). * The default is 4 (50% duty cycle). * * Rn Rest, for a length defined by n. Alternative form of the P command. * * Tn Sets the tempo (the pace at which the music plays) in beats per minute. * n is a decimal number between 32 and 255. The default tempo is 120. One * beat corresponds to a quarter note (L4). * * . Dot. A dot can follow a letter note or a pause. It extends the duration * of the note or pause by half (to 150%). More than one dot may be used. * * Note: The Microsoft documentation states that multiple dots extend the * duration as follows: * 2 dots = 1.5 ^ 2 = 225%, 3 dots = 1.5 ^ 3 = 337.5%. * This differs from standard musical notation where multiple dots * provide successively smaller extensions as follows: * 2 dots = 1 + 1/2 + 1/4 = 175%. * 3 dots = 1 + 1/2 + 1/4 + 1/8 = 187.5%. * * The standard notation extensions are used here. * * > Play the following note in the next higher octave. For example, * O3C >D E is equivalent to O3C O4D O3E. * * < Play the following note in the next lower octave. For example, * O3C <D E is equivalent to O3C O2D O3E. * * Note: Some dialects of MML appear to treat < and > as persistent * commands that affect all following notes. * * Note: The Microsoft documentation does not say whether or not < and > * should act on notes specified by number, such as N37. In this * implementation it does, so >N37 = N49. * * : # Treat remainder of line as a comment. Note: line must end with '\n'. * * Note: Some dialects of MML use # to start a comment. We accept either. * * White space and line endings (CR, LF) are ignored (except in comments). * * Note: The play() function has a second parameter which supports some MML * dialect alternatives. Refer to the comments below for details. */ #include "mbed.h" #include "PwmSound.h" #include "ctype.h" extern Serial pc; //for debug, comment out of not needed // Standard note pitches in Hz // From Wikipedia: http://en.wikipedia.org/wiki/Scientific_pitch_notation // First entry is a dummy, real note numbers start at 1 // Seven octaves, twelve notes per octave // C, C#, D, D#, E, F, F#, G, G#, A, A#, B // Middle C (261.63Hz) is element 37 float notePitches[1+84] = { 1.0, //dummy 32.703, 34.648, 36.708, 38.891, 41.203, 43.654, //first octave 46.249, 48.999, 51.913, 55.000, 58.270, 61.735, 65.406, 69.296, 73.416, 77.782, 82.407, 87.307, //second octave 92.499, 97.999, 103.83, 110.00, 116.54, 123.47, 130.81, 138.59, 146.83, 155.56, 164.81, 174.61, //third octave 185.00, 196.00, 207.65, 220.00, 233.08, 246.94, 261.63, 277.18, 293.66, 311.13, 329.63, 349.23, //fourth octave 369.99, 392.00, 415.30, 440.00, 466.16, 493.88, 523.25, 554.37, 587.33, 622.25, 659.26, 698.46, //fifth octave 739.99, 783.99, 830.61, 880.00, 932.33, 987.77, 1046.5, 1108.7, 1174.7, 1244.5, 1318.5, 1396.9, //sixth octave 1480.0, 1568.0, 1661.2, 1760.0, 1864.7, 1975.5, 2093.0, 2217.5, 2349.3, 2489.0, 2637.0, 2793.8, //seventh octave 2960.0, 3136.0, 3322.4, 3520.0, 3729.3, 3951.1, }; // Note numbers within octave for notes A - G (white keys on piano) int notes[7] = { 10, 12, 1, 3, 5, 6, 8 }; //C is first note = 1 // Allowable note modifiers for notes A - G int flats[7] = { -1, -1, 0, -1, -1, 0, -1 }; //not C or F int sharps[7] = {1, 0, 1, 1, 0, 1, 1 }; //not B or E // Play a melody from music data written in MML format. // // Parameters: // m - pointer to string containing music data // options (default 0) - support for different dialects of MML // bit 0 (1) - standard octave numbering // 0: octaves range from 0 to 6, middle C starts octave 3 // 1: octaves range from 1 to 7, middle C starts octave 4 // bit 1 (2) - stickyShift // 0: < and > act on next note only // 1: < and > act on all following notes // bit 2 (4) - longDots // 0: standard dot extensions (i.e. 150%, 175%, 187.5%) // 1: GW-BASIC dot extensions (i.e. 150%, 225%, 337.5%) // Returns: 0 if no error in input, otherwise position of offending character int PwmSound::play(char* m, int options) { bool run = true, kbdPoll = true; char c, c1; int n, n1, n2; bool stdOctNum = (options & 1) ? true : false; //options bits bool stickyShift = (options & 2) ? true : false; bool longDots = (options & 4) ? true : false; _octave = 4; //set defaults _shift = 0; _tempo = 120; _length = 4; _1dot = 1.5; _2dots = (longDots == true) ? 2.25 : 1.75; _3dots = (longDots == true) ? 3.375 : 1.875; _style = 7; _dutyCycle = 0.5; _mp = m; _haveNext = false; //pc.putc('['); while (run) { if (kbdPoll && pc.readable()) { pc.getc(); break; } c = _getChar(); //read next char in input stream //pc.putc(c); switch (c) { case 'A': //specify note by letter (and modifier) case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': if (stdOctNum) { n = ((_octave - 1) * 12) + notes[c - 'A']; } else { n = (_octave * 12) + notes[c - 'A']; } c1 = _nextChar(); //optional modifier if (c1 == '-' || c1 == '+' || c1 == '#') { c1 = _getChar(); n += (c1 == '-') ? flats[c - 'A'] : sharps[c - 'A']; } n += _shift * 12; if (!stickyShift) { _shift = 0; } n1 = _getNumber(); //optional length number n2 = _getDots(); //optional dots if (n1 == 0) { _note(n, _length, n2); } else { _note(n, n1, n2); } break; case 'K': kbdPoll = false; break; case 'L': //set note length n = _getNumber(); if (n >= 1 && n <= 64) { _length = n; } break; case 'M': //set music style (proportion of note length played) switch (_getChar() ) { case 'L': //legato _style = 8; break; case 'N': //normal _style = 7; break; case 'S': //staccato _style = 6; break; } break; case 'N': //specify note by number n = _getNumber(); n += _shift * 12; //not really sure about this if (!stickyShift) { _shift = 0; } _note(n, _length); break; case 'O': //set octave n = _getNumber(); if (stdOctNum) { if (n >= 1 && n <= 7) { _octave = n; } } else { if (n >= 0 && n <= 6) { _octave = n; } } break; case 'P': //pause or rest case 'R': n1 = _getNumber(); //optional length number n2 = _getDots(); //optional dots if (n1 == 0) { _note(0, _length, n2); } else { _note(0, n1, n2); } break; case 'Q': //set timbre n = _getNumber(); if (n >= 1 && n <= 4) { _dutyCycle = n / 8.0; } break; case 'T': //set tempo n = _getNumber(); if ( n>= 32 && n <= 255) { _tempo = n; } break; case '<': //move down an octave _shift--; break; case '>': //move up an octave _shift++; break; case ':': //comment to end of line case '#': while (_getChar() != '\n') ; break; case ' ': //skip over white space and line endings case '\t': case '\r': case '\n': break; case '\0': //end of string run = false; break; default: //abort on invalid characters _pin = 0.0; return(int (_mp - m) ); //return position of error } } //end of while //pc.putc(']'); _pin = 0.0; return 0; } // Play a musical note on output pin // // Parameters: // number - 0 = rest, notes from 1 to 84, middle C (262Hz) = 37 // length - duration of note (1-64): 1 = whole note (semibreve) // 2 = half note (minim) // 4 = quarter note (crotchet) = 1 beat // 8 = eighth note (quaver) // etc // dots - length extension (0-3, default 0) // Returns: nothing void PwmSound::_note(int number, int length, int dots) { float duration, play, rest; if (number < 1 || number > 84) { //convert bad note to a rest number = 0; } duration = 240.0 / (_tempo * length); if (dots == 1) { duration *= _1dot; } else if (dots == 2) { duration *= _2dots; } else if (dots == 3) { duration *= _3dots; } play = duration * _style / 8.0; rest = duration * (8 - _style) / 8.0; if (number > 0) { _pin.period(1.0 / notePitches[number]); _pin = _dutyCycle; } wait(play); _pin = 0.0; wait(rest); } // Read next character in input string // // Parameters: none // Returns: next character char PwmSound::_getChar(void) { if (_haveNext) { _haveNext = false; return _nextCh; } else { return *_mp++; } } // Examine next character in input string without consuming it // // Parameters: none // Returns: next character char PwmSound::_nextChar(void) { if (!_haveNext) { _nextCh = *_mp++; _haveNext = true; } return _nextCh; } // Read a variable length number from input string // // Parameters: none // Returns: number int PwmSound::_getNumber(void) { int n = 0; while (isdigit(_nextChar()) ) { n *= 10; n += _getChar() - '0'; } return n; } // Read variable number of dots from input string // // Parameters: none // Returns: number of dots int PwmSound::_getDots(void) { int n = 0; while (_nextChar() == '.') { _getChar(); n++; } return n; } // END of play.cpp