-
Notifications
You must be signed in to change notification settings - Fork 63
The player machine
Plays a single note or a pattern defined by an array of frequency/period/pause triplets through a passive piezo speaker or a standard speaker.
But it's more useful than that. By using the onNote() connector the player machine can be used to drive other machines or process any task at the intervals given in the pattern array. The onFinish() connector allows for chaining different patterns into 'songs'.
Note that the Arduino tone() command cannot control more than one pin at the same time so playing music on two player machines at the same time unfortunately won't work.
- begin()
- play()
- repeat()
- speed()
- pitch()
- start()
- stop()
- onNote()
- onFinish()
- trace()
- EVT_START
- EVT_STOP
- EVT_TOGGLE
#include <Automaton.h>
#include "musical_notes.h"
// Playback 'Frere Jacques' with button trigger and speed control
Atm_player player;
Atm_button button;
Atm_analog speed;
int pattern[] = {
_G4, _N04, 0, _A4, _N04, 0, _B4, _N04, 0, _G4, _N04, 0, // Frere Jacques
_G4, _N04, 0, _A4, _N04, 0, _B4, _N04, 0, _G4, _N04, 0,
_B4, _N04, 0, _C5, _N04, 0, _D5, _N04, _N04, // Dormez vous?
_B4, _N04, 0, _C5, _N04, 0, _D5, _N04, _N04,
_D5, _N08, 0, _E5, _N08, 0, _D5, _N08, 0, _C5, _N08, 0, _B4, _N04, 0, _G4, _N04, 0, // Sonnez les matines
_D5, _N08, 0, _E5, _N08, 0, _D5, _N08, 0, _C5, _N08, 0, _B4, _N04, 0, _G4, _N04, 0,
_G4, _N04, 0, _D4, _N04, 0, _G4, _N04, _N04, // Ding dang dong
_G4, _N04, 0, _D4, _N04, 0, _G4, _N04, _N04,
};
void setup() {
player.begin( 19 ) // A passive buzzer or speaker on pin 19
.play( pattern, sizeof( pattern ) )
.repeat( -1 );
button.begin( 2 ) // A button on pin 2 toggles playback on and off
.onPress( player, player.EVT_TOGGLE );
speed.begin( A0 ) // An analog pot on pin A0 controls playback speed
.range( 50, 300 ) // From 50% to 300% of original speed
.onChange( []( int idx, int v, int up ) {
player.speed( v );
});
}
void loop() {
automaton.run();
}
You'll also need the musical_notes.h file to compile this.
Initializes a player machine. If the pin argument is greater than -1 the machine will drive a piezo speaker connected to that pin through the Arduino tone() function. Leave out the pin argument if you want to drive something else through the onNote() connector.
void setup() {
player.begin()
.play( pattern, sizeof( pattern ) )
.onNote( true, led, led.EVT_ON )
.onNote( false, led, led.EVT_OFF )
.start();
}
In its default setting the player machine will play a single 880 hz tone for 50 milliseconds.
void setup() {
player.begin( 19 ).start(); // Plays a 880 hz tone on pin 19
}
Sets a pattern to be played by the player machine. The pattern should be an array of int with 3 entries for every tone.
Index | Function |
---|---|
0 | Frequency in herz |
1 | Duration of note on in milliseconds |
2 | Duration of note off in milliseconds |
The patsize argument contains the total number of bytes in the pattern array.
The onFinish() connector can be used to chain different patterns together from a callback handler.
This variant plays 32 bit patterns. This is probably not very useful for playing sounds over the standard tone() command but it enables the use of 32 bit patterns to control e.g. 32 output pins connected to 32 leds which is something many people like to do.
uint32_t pattern[] = { // Bitmapped pattern (32 bits)
B4INT( B00000000, B00000000, B00000001, B00100000 ), 100, 0,
B4INT( B00000000, B00000000, B00000010, B00010000 ), 100, 0,
B4INT( B00000000, B00000000, B00000001, B00001000 ), 100, 0,
B4INT( B00000000, B00000000, B00000010, B00000100 ), 100, 0,
B4INT( B00000000, B00000000, B00000001, B00000010 ), 100, 0,
B4INT( B00000000, B00000000, B00000010, B00000001 ), 100, 0,
B4INT( B00000000, B00000000, B00000001, B00000010 ), 100, 0,
B4INT( B00000000, B00000000, B00000010, B00000100 ), 100, 0,
B4INT( B00000000, B00000000, B00000001, B00001000 ), 100, 0,
B4INT( B00000000, B00000000, B00000010, B00010000 ), 100, 0,
};
int pattern2[] = { // Bitmapped pattern (16 bits)
B2INT( B00000011, B11111111 ), 200, 0,
B2INT( B00000000, B00000000 ), 200, 0,
};
void setup() {
player.begin()
.play( pattern, sizeof( pattern ) );
The B4INT macro (defined in automaton.h) maps 4 bytes to a 32 bit unsigned integer so that we can use the handy Arduino Bxxxxxxxx constants. There's also a B2INT macro that does the same for 2 byte integers.
In 32 bit mode the lower 16 bits are in the v argument of the onNote() callback and the upper 16 bits are in the up argument.
Plays a single tone through the piezo speaker.
Argument | Function |
---|---|
freq | Frequency in herz |
period | Duration of note on in milliseconds |
pause | Duration of note off in milliseconds |
void setup() {
player.begin( 4 )
.play( 440, 200 ); // Play a 440 hz tone for 200 ms
button.begin( 2 )
.onPress( player, player.EVT_START ); // When the button is pressed
}
Specifies how often the pattern should be repeated. Default is once (1). After the last repeat has finished the onFinish() connector is called.
Modifies the speed of the pattern, value is a percentage. At speed( 100 ) - the default - everything plays at the speed specified in the pattern array, at 50 everything plays at half speed, at 300 everything plays at triple speed, etc...
The example below displays a pulsing led pattern on pins 4..9 defined by a bitmapped player pattern. The speed of the display can be controlled with a potmeter on pin A0 from 50% to 500% of the default pattern speed.
#include <Automaton.h>
Atm_player player; // A player machine
Atm_analog speed; // An analog machine for the potmeter
// These pins correspond to the bitmap bit positions
const uint8_t pin[] = { 4, 5, 6, 7, 8, 9 };
const int speedPotPin = A0;
const int speedMin = 50; // Half speed
const int speedMax = 500; // Quintuple speed
int pattern[] = { // Bitmapped pattern
B00000000, 100, 0,
B00001100, 100, 0,
B00011110, 100, 0,
B00111111, 100, 0,
B00011110, 100, 0,
B00001100, 100, 0,
};
void setup() {
player.begin() // No sound this time!
.play( pattern, sizeof( pattern ) ) // Set up the pattern
.onNote( true, []( int idx, int v, int up ) { // Called on every note
for ( int i = 0; i <= sizeof( pin ); i++ ) {
pinMode( pin[i], OUTPUT ); // LED on/off according to bit
digitalWrite( pin[i], v & ( 1 << i ) ? HIGH : LOW );
}
})
.repeat( -1 ) // Repeat forever
.start(); // Kickoff!
speed.begin( speedPotPin )
.range( speedMin, speedMax ) // Set the range for the pot values
.onChange( []( int idx, int v, int up ) {
player.speed( v ); // Set speed on every change of the potmeter
});
}
void loop() {
automaton.run();
}
Note that this time we don't use state machines to represent the leds. There's no need to since we're just switching them on and off and this way we need only 2 state machines instead of 10. State machines are a great tool, but there's no need to go overboard on them.
Modifies the pattern frequencies, value is a percentage. At speed( 100 ) - the default - everything plays at the frequency specified in the pattern array, at 50 everything plays at half the frequency, at 300 everything plays at triple frequency, etc...
#include <Automaton.h>
// Vary pitch and speed of a note by turning a pot
Atm_player player;
Atm_analog pot;
void setup() {
player.begin( 19 )
.play( 440, 100, 100 )
.repeat( -1 )
.start();
pot.begin( A0 )
.range( 30, 500 )
.onChange( []( int idx, int v, int up ) {
player.speed( v );
player.pitch( v );
});
}
void loop() {
automaton.run();
}
Starts the player.
Stops the player.
This is where it gets interesting. By using the onNote() connector the player machine can be used to drive other machines or process any task at the intervals given in the pattern array. The frequency value (which may very well contain something totally different) is passed on to a callback in the v argument.
void setup() {
led.begin( 4 );
player.begin()
.play( pattern, sizeof( pattern ) )
.onNote( true, led, led.EVT_ON ) // On note on
.onNote( false, led, led.EVT_OFF ) // On note off
.start();
}
The example below uses a callback combined with bitmapped values in the pattern/frequency field to trigger two solenoid valves in a particular order.
#include <Automaton.h>
Atm_player player;
Atm_button button;
Atm_led valve1, valve2;
int pattern[] = {
B00000001, 10, 0, // Frequency, Duration, Pause triplets
B00000011, 980, 0,
B00000001, 10, 0,
B00000000, 0, 0,
};
// Open and close two solenoid valves in this order
// Valve 1: ___|^^^^^^^^|____
// Valve 2: ____|^^^^^^|_____
void setup() {
valve1.begin( 4 );
valve2.begin( 5 );
player.begin()
.play( pattern, sizeof( pattern ) )
.onNote( true, []( int idx, int v, int up ) {
valve1.trigger( ( v & B00000001 ) > 0 ? valve1.EVT_ON : valve1.EVT_OFF );
valve2.trigger( ( v & B00000010 ) > 0 ? valve2.EVT_ON : valve2.EVT_OFF );
});
button.begin( 2 )
.onPress( player, player.EVT_START );
}
void loop() {
automaton.run();
}
You can easily control 8 or more valves this way.
A connector that is called when playing stops (after the last repeat of a pattern has finished).
The code below uses onFinish() to chain two different patterns together.
#include <Automaton.h>
Atm_player player;
Atm_button button;
int pattern1[] = { // Odd pattern
440, 100, 0,
587, 100, 0,
880, 100, 100,
};
int pattern2[] = { // Even pattern
880, 100, 0,
587, 100, 0,
440, 100, 100,
};
void callback( int idx, int v, int up ) {
static int cnt = 0;
if ( cnt % 2 == 0 ) {
player.play( pattern2, sizeof( pattern2 ) ); // Even counts
} else {
player.play( pattern1, sizeof( pattern1 ) ).trigger( player.EVT_STOP ); // Odd counts
}
cnt++;
}
void setup() {
player.begin( 4 )
.play( pattern1, sizeof( pattern1 ) )
.onFinish( callback );
button.begin( 2 )
.onPress( player, player.EVT_TOGGLE );
}
void loop() {
automaton.run();
}
To monitor the behavior of this machine you may log state change events to a Stream object like Serial.
Serial.begin( 9600 );
player.trace( Serial );
The player machine starts playback on receipt of this event.
player.trigger( player.EVT_START );
The player machine stops playback on receipt of this event.
player.trigger( player.EVT_STOP );
The player machine toggle playback on or off on receipt of this event.
player.trigger( player.EVT_TOGGLE );