Class to play tones, various sounds and music in MML format using a PWM channel.

Dependents:   PwmSoundTest

Files at this revision

API Documentation at this revision

Comitter:
paulg
Date:
Tue May 06 13:16:28 2014 +0000
Parent:
0:185bcd9f8e19
Commit message:
Added play() function to play music written in Music Macro Language (MML) format. Removed tune() as no longer required.

Changed in this revision

PwmSound.cpp Show annotated file Show diff for this revision Revisions of this file
PwmSound.h Show annotated file Show diff for this revision Revisions of this file
play.cpp Show annotated file Show diff for this revision Revisions of this file
diff -r 185bcd9f8e19 -r 67056b9df9ff PwmSound.cpp
--- a/PwmSound.cpp	Sun Mar 30 22:50:27 2014 +0000
+++ b/PwmSound.cpp	Tue May 06 13:16:28 2014 +0000
@@ -6,8 +6,8 @@
  * Version:   see below
  *
  * Description:
- * Class to play tones, various sounds and simple tunes using a PWM channel.
- * Inspired by Jim Hamblem's Speaker class. Thanks Jim!
+ * Class to play tones, various sounds, musical notes and simple tunes using
+ * a PWM channel. Inspired by Jim Hamblem's Speaker class. Thanks Jim!
  *
  * Refer to the tutorial "Using a Speaker for Audio Output" in the Cookbook.
  * The mbed LPC1768 PWM pins will drive a small speaker without amplification.
@@ -37,57 +37,37 @@
  * Ver  Date    By  Details
  * 0.00 25Mar14 PG  File created.
  * 1.00 30Mar14 PG  Initial release.
+ * 2.00 06May14 PG  Added play() etc to support MML music. Removed tune() etc.
  *
  ******************************************************************************/
 
 #include "mbed.h"
 #include "PwmSound.h"
-
-extern Serial pc;
-
-// Standard note pitches in Hz
-// 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 is element 37 (262Hz)
+#include "FastOut.h"
 
-int notePitches[1+84] = {
-    1,                                                      //dummy
-    33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62,
-    65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123,
-    131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247,
-    262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
-    523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988,
-    1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976,
-    2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951,
-};
+extern Serial pc;       //for debugging, comment out if not needed
+FastOut<LED1> led1;
 
 // Constructor
 
 PwmSound::PwmSound(PinName pin) : _pin(pin) {
-        _duration = 0.25;
-        _volume = 1.0;
-        _tempo = 120;   //120 beats/minute
-        _length = 4;    //quarter note (crotchet)
-        _style = PwmSound::NORMAL;
-        _pin = 0.0;
-        _playing = false;
-    }
+	_dutyCycle = 0.5;
+	_pin = 0.0;
+	_playing = false;
+}
 
 // Play a tone on output pin
-// 2 versions for convenience when using default duration
 //
 // Parameters:
 //    frequency - frequency of tone in Hz
 //    duration - duration of tone in seconds
 //               if duration = 0.0, tone continues in background until stopped
-//    volume - crude volume control (0.0-1.0), alters PWM duty cycle 0-50%
 // Returns: nothing
-// Uses: current volume (duty cycle)
+// Uses: current duty cycle
 
 void PwmSound::tone(float frequency, float duration) {
     _pin.period(1.0 / frequency);
-    _pin = _volume / 2.0;
+    _pin = _dutyCycle;
     if (duration == 0.0) {
         _playing = true;
         return;
@@ -96,17 +76,6 @@
     _pin = 0.0;
 }
 
-void PwmSound::tone(float frequency) {
-    _pin.period(1.0 / frequency);
-    _pin = _volume / 2.0;
-    if (_duration == 0.0) {
-        _playing = true;
-        return;
-    }
-    wait(_duration);
-    _pin = 0.0;
-}
-
 // Stop background tone or sound generation
 
 void PwmSound::stop(void) {
@@ -114,42 +83,53 @@
     _pin = 0.0;
 }
 
-// Set default duration
-//
-// Parameters:
-//    duration - duration of tone in seconds
-// Returns: nothing
-
-void PwmSound::duration(float duration) {
-    _duration = duration;
-}
-
-// Set default volume
+// Set timbre (tonal quality)
 //
 // Parameters:
-//    volume - crude volume control (0.0 - 1.0), alters PWM duty cycle 0 - 50%
+//    timbre - (1-4): sets PWM duty cycle to 12.5%, 25%, 37.5% or 50%
 // Returns: nothing
 
-void PwmSound::volume(float volume) {
-    _volume = volume;
+void PwmSound::timbre(int t) {
+	if (t >= 1 && t <= 4) {
+		_dutyCycle = t / 8.0;
+	}
 }
-
 // Beeps of various types and other sounds
-// Note: buzz, siren and trill use a ticker and callback to
-//       support continuous sound in background
+// Note: All sounds below except phone permit continuous sound in background
+//       To invoke this call the function with a zero parameter
+//       Call stop() to end the sound
 //
 // Parameters:
-//    n - number of cycles, 0 for continuous sound in background
+//    n - number of cycles, 0 for continuous sound in background (not phone)
 // Returns: nothing
 
 void PwmSound::bip(int n) {
+    if (n == 0) {
+        _setup(1047.0, 0.1, 0.0, 0.03);
+        return;
+    }
     for (int i = 0; i < n; i++) {
         tone(1047.0, 0.10);
         wait(0.03);
     }
 }
 
+void PwmSound::bop(int n) {
+    if (n == 0) {
+        _setup(700.0, 0.1, 0.0, 0.03);
+        return;
+    }
+    for (int i = 0; i < n; i++) {
+        tone(700.0, 0.10);
+        wait(0.03);
+    }
+}
+
 void PwmSound::beep(int n) {
+    if (n == 0) {
+        _setup(969.0, 0.3, 0.0, 0.1);
+        return;
+    }
     for (int i = 0; i < n; i++) {
         tone(969.0, 0.3);
         wait(0.1);
@@ -157,6 +137,10 @@
 }
 
 void PwmSound::bleep(int n) {
+    if (n == 0) {
+        _setup(800.0, 0.4, 0.0, 0.1);
+        return;
+    }
     for (int i = 0; i < n; i++) {
         tone(800.0, 0.4);
         wait(0.1);
@@ -165,7 +149,7 @@
 
 void PwmSound::buzz(int n) {
     if (n == 0) {
-        _setup(1900.0, 300.0, 0.01);
+        _setup(1900.0, 0.01, 300.0, 0.01);
         return;
     }
     for (int i = 0; i < n; i++) {
@@ -178,7 +162,7 @@
 
 void PwmSound::siren(int n) {
     if (n == 0) {
-        _setup(969.0, 800.0, 0.5);
+        _setup(969.0, 0.5, 800.0, 0.5);
         return;
     }
     for (int i = 0; i < n; i++) {
@@ -189,7 +173,7 @@
 
 void PwmSound::trill(int n) {
     if (n == 0) {
-        _setup(969.0, 800.0, 0.05);
+        _setup(969.0, 0.05, 800.0, 0.05);
         return;
     }
     for (int i = 0; i < n; i++) {
@@ -218,117 +202,40 @@
 }   
 
 // Continuous sound setup and callback routines
+// _sustain() has been optimised for speed. On a 96MHz LPC1768 it takes 8.5us.
+// Non-optimised version with floating point freqency & duration took 11.4us.
+// Execution times measured with 'scope on LED1 pin.
 
-void PwmSound::_setup(float freq1, float freq2, float duration) {
-    _freq1 = freq1;
-    _freq2 = freq2;
-    _beat = false;
-    _sustainTkr.attach(this, &PwmSound::_sustain, duration);
-    tone(freq1, 0.0);      //start the sound
+void PwmSound::_setup(float freq1, float dur1, float freq2, float dur2) {
+    _period1 = (int) (1000000.0 / freq1);
+    _period2 = (int) (1000000.0 / freq2);
+    _dur1 = (unsigned int) (1000000.0 * dur1);
+    _dur2 = (unsigned int) (1000000.0 * dur2);
+    _phase = false;
+    _sustainTmo.attach_us(this, &PwmSound::_sustain, _dur1);
+    _pin.period_us(_period1);      //start the sound
+    _pin = _dutyCycle;
     _playing = true;
 }
         
 void PwmSound::_sustain(void) {
+    //led1 = 1;
     if (_playing == false) {
-        _sustainTkr.detach();   //detach callback to stop sound
+        //kill pwm and no more callbacks
         _pin = 0.0;
     } else {
-        _beat = !_beat;
-        tone( (_beat ? _freq2 : _freq1), 0.0);
+        _phase = !_phase;
+        if (_phase) {
+            _pin.period_us(_period2);
+            _pin = _dutyCycle;
+            _sustainTmo.attach_us(this, &PwmSound::_sustain, _dur2);
+        } else {
+            _pin.period_us(_period1);
+            _pin = _dutyCycle;
+            _sustainTmo.attach_us(this, &PwmSound::_sustain, _dur1);
+        }
     }
-}
-    
-// 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
-// Returns: nothing
-// Uses: current tempo, music style and volume (duty cycle)
-
-void PwmSound::note(int number, int length) {
-    int frequency;
-    float duration, play, rest;
-
-    //pc.printf("<number %d, length %d, volume %f, ", number, length, volume);
-    if (number < 1 || number > 84) {    //convert bad note to a rest
-        number = 0;
-    }
-    frequency = notePitches[number];
-
-    duration = 240.0 / (_tempo * length);
-    if (_style == PwmSound::STACCATO) {           //staccato
-        play = duration * 0.75;
-        rest = duration * 0.25;
-    } else if (_style == PwmSound::LEGATO) {    //legato
-        play = duration;
-        rest = 0;
-    } else {                    //normal
-        play = duration * 0.875;
-        rest = duration * 0.125;
-    }
-    //pc.printf("f %d, d %f, p %f, r %f>\n", frequency, duration, play, rest);
-    if (number > 0) {
-        _pin.period(1.0 / frequency);
-        _pin = _volume / 2.0;
-    }
-    wait(play);
-    _pin = 0.0;
-    wait(rest);    
+    //led1 = 0;
 }
 
-// Set default tempo
-//
-// Parameters:
-//    tempo - tempo in BPM
-// Returns: nothing
-
-void PwmSound::tempo(int tempo) {
-    _tempo = tempo;
-}
-
-// Set default music style
-//
-// Parameters:
-//    style - STAACATO, NORMAL or LEGATO
-// Returns: nothing
-
-void PwmSound::style(MusicStyle style) {
-    _style = style;
-}
-
-// Play a simple tune from note data in an array.
-//
-// Parameters:
-//    tune - pointer to char array containing tune data
-//           first entry is tempo
-//           second entry is music style (STACCATO, NORMAL, LEGATO)
-//           subsequent entries are note number/length pairs
-//           final entry must be 0, 0
-// Returns: nothing
-// Uses: current volume
-
-void PwmSound::tune(unsigned char* tune) {
-    int t, number, length;
-
-    t = *tune++;
-    if (t == 0) {
-        return;
-    }
-    tempo(t);
-    style( (MusicStyle) *tune++);
-    while (true) {
-        number = *tune++;
-        length = *tune++;
-        if (number == 0 && length == 0) {
-            break;
-        }
-        note(number, length);
-    }
-}
-
-// END of PwmSound.cpp
\ No newline at end of file
+// END of PwmSound.cpp
diff -r 185bcd9f8e19 -r 67056b9df9ff PwmSound.h
--- a/PwmSound.h	Sun Mar 30 22:50:27 2014 +0000
+++ b/PwmSound.h	Tue May 06 13:16:28 2014 +0000
@@ -32,6 +32,7 @@
  * Ver  Date    By  Details
  * 0.00 25Mar14 PG  File created.
  * 1.00 30Mar14 PG  Initial release
+ * 2.00 06May14 PG  Added play() etc to support MML music. Removed tune() etc.
  *
  ******************************************************************************/
 
@@ -44,17 +45,14 @@
 //private:
 
 public:
-    enum MusicStyle { STACCATO, NORMAL, LEGATO };
-
     PwmSound(PinName pin);
 
-    void tone(float frequency, float duration);     //tones
-    void tone(float frequency);
+    void tone(float frequency, float duration = 0.0);     //tones
     void stop(void);
-    void duration(float duration);
-    void volume(float volume);
+    void timbre(int timbre);
 
     void bip(int n = 1);    //beeps and other sounds
+    void bop(int n = 1);
     void beep(int n = 1);
     void bleep(int n = 1);
     void buzz(int n = 1);
@@ -62,31 +60,43 @@
     void trill(int n = 1);
     void phone(int n = 1);
 
-    void note(int number, int length);  //musical note
-    void tempo(int tempo);
-    void style(MusicStyle style);
- 
-    void tune(unsigned char* t);    //play tune from data in array
+    int play(char* m, int options = 0);		//play tune in MML format
 
 private:
     PwmOut _pin;
-    float _duration;    //duration of tone in seconds
-    float _volume;      //crude volume 0.0-1.0 => 0-50% duty cycle
+    float _dutyCycle;   //PWM duty cycle 0-50%
+
+    //the following support continuous two-tone sounds in background
+    void _setup(float freq1, float dur1, float freq2, float dur2);
+    void _sustain(void);    
+    Timeout _sustainTmo;
+    int _period1;           //in us
+    unsigned int _dur1;     //in us
+    int _period2;
+    unsigned int _dur2;
+    bool _phase;
+    bool _playing;
+
+    //the following support play
+    void _note(int number, int length, int dots = 0);
+    char _getChar(void);
+    char _nextChar(void);
+    int _getNumber(void);
+    int _getDots(void);
+    int _octave;	//current octave
+    int _shift;		//octave shift from < and >
     int _tempo;     //pace of music in beats per minute (32-255)
                     //one beat equals one quarter note (ie a crotchet)
     int _length;    //length of note (1-64), 1 = whole note, 4 = quarter etc
-    MusicStyle _style;
-
-    //the following support continuous two-tone sounds in background
-    void _setup(float freq1, float freq2, float duration);
-    void _sustain(void);    
-    Ticker _sustainTkr;
-    float _freq1;
-    float _freq2;
-    bool _beat;
-    bool _playing;
+    float _1dot;	//note length extension factors
+    float _2dots;
+    float _3dots;
+    int _style;     //music style (1-8), 6 = Staccato, 7 = Normal, 8 = Legato
+    char* _mp;		//current position in music string
+    char _nextCh;
+    bool _haveNext;
 };
 
 #endif
 
-// END of PwmSound.h
\ No newline at end of file
+// END of PwmSound.h
diff -r 185bcd9f8e19 -r 67056b9df9ff play.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/play.cpp	Tue May 06 13:16:28 2014 +0000
@@ -0,0 +1,449 @@
+/******************************************************************************
+ * 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