This commit is contained in:
Stephan Mühl
2023-03-22 12:15:18 +01:00
committed by GitHub
parent 3e12414a87
commit adb5102869
203 changed files with 35010 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
/***************************************************************************
* This file is part of Melody Player, a library for Arduino *
* to play notes on piezoelectric buzzers. *
* *
* Copyright (C) 2020-2022 Fabiano Riccardi *
* *
* This library is free software; you can redistribute *
* it and/or modify it 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. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this library; if not, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#ifndef MELODY_H
#define MELODY_H
#include <Arduino.h>
#include <memory>
#include <vector>
/**
* A note and its duration.
*/
struct NoteDuration {
// The note frequency.
unsigned short frequency;
// The note duration. The representation can be either relative (fixed-point, decimal
// part = 1 bit) to time units or absolute (in milliseconds)
unsigned short duration;
};
/**
* This class stores the data melody (notes and metadata).
* To ease the creation of a melody, you should use MelodyFactory class.
*/
class Melody {
public:
Melody() : notes(nullptr){};
Melody(String title, unsigned short timeUnit, std::shared_ptr<std::vector<NoteDuration>> notes, bool automaticSilence)
: title(title), timeUnit(timeUnit), notes(notes), automaticSilence(automaticSilence){};
/**
* Return the title of the melody.
*/
String getTitle() const {
return title;
};
/**
* Return the time unit (i.e. the minimum length of a note), in milliseconds.
*/
unsigned short getTimeUnit() const {
return timeUnit;
};
/**
* Get the number of notes.
*/
unsigned short getLength() const {
if (notes == nullptr) return 0;
return (*notes).size();
}
/**
* Get the note at the given position.
* If the melody or the position is invalid, return a zeroed NoteDuration.
*/
NoteDuration getNote(unsigned short i) const {
if (i < getLength()) { return (*notes)[i]; }
return { 0, 0 };
}
/**
* Return true if the melody should be played with a small delay between each note.
*/
bool getAutomaticSilence() const {
return automaticSilence;
}
/**
* Return true if the melody is valid, false otherwise.
*/
bool isValid() const {
return notes != nullptr && (*notes).size() != 0;
}
/**
* Return true if the melody is valid, false otherwise.
*/
explicit operator bool() const {
return isValid();
}
private:
String title;
// in milliseconds
unsigned short timeUnit;
std::shared_ptr<std::vector<NoteDuration>> notes;
const static unsigned short maxLength = 1000;
bool automaticSilence;
// Enable debug messages over serial port
const static bool debug = false;
};
#endif // END MELODY_H

View File

@@ -0,0 +1,229 @@
/***************************************************************************
* This file is part of Melody Player, a library for Arduino *
* to play notes on piezoelectric buzzers. *
* *
* Copyright (C) 2020-2022 Fabiano Riccardi *
* *
* This library is free software; you can redistribute *
* it and/or modify it 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. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this library; if not, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#include "melody_factory.h"
#include "notes_array.h"
#include <LittleFS.h>
#include <algorithm>
static void removeCarriageReturn(String& s) {
if (s.charAt(s.length() - 1) == '\r') { s = s.substring(0, s.length() - 1); }
}
Melody MelodyFactoryClass::load(String filepath, FS& fs) {
File f = LittleFS.open(filepath, "r");
f.setTimeout(0);
if (!f) {
if (debug) Serial.println("Opening file error");
return Melody();
}
// Skip multi-line comments at the begin of the file
String line = f.readStringUntil('\n');
while (line.charAt(0) == '#') { line = f.readStringUntil('\n'); }
bool success = false;
success = loadTitle(line);
if (!success) { return Melody(); }
success = loadTimeUnit(f.readStringUntil('\n'));
if (!success) { return Melody(); }
success = loadNumberOfNotes(f.readStringUntil('\n'));
if (!success) { return Melody(); }
NoteFormat noteFormat = loadNoteFormat(f.readStringUntil('\n'));
if (noteFormat == NoteFormat::ERROR) {
return Melody();
} else {
this->noteFormat = noteFormat;
}
if (debug)
Serial.println(String("This melody object will take at least: ") + (sizeof(NoteDuration) * nNotes) + "bytes");
if (nNotes < maxLength) {
notes = std::make_shared<std::vector<NoteDuration>>();
notes->reserve(nNotes);
bool error = false;
while (f.available() && notes->size() < nNotes && !error) {
// get a token
String noteDuration = f.readStringUntil('|');
error = !loadNote(noteDuration);
}
if (error) {
if (debug) Serial.println("error during the tokens loading!");
return Melody();
}
}
return Melody(title, timeUnit, notes, true);
}
Melody MelodyFactoryClass::load(String title, unsigned short timeUnit, String notesToLoad[],
unsigned short nNotesToLoad, bool autoSilence) {
if (title.length() == 0 && timeUnit <= 20) { return Melody(); }
if (nNotesToLoad == 0 || nNotesToLoad > maxLength) { return Melody(); }
if (notesToLoad == nullptr) { return Melody(); }
notes = std::make_shared<std::vector<NoteDuration>>();
notes->reserve(nNotesToLoad);
noteFormat = NoteFormat::STRING;
bool error = false;
while (this->notes->size() < nNotesToLoad && !error) {
String noteDuration = notesToLoad[notes->size()] + ",1";
error = !loadNote(noteDuration);
}
if (error) { return Melody(); }
return Melody(title, timeUnit, notes, autoSilence);
}
Melody MelodyFactoryClass::load(String title, unsigned short timeUnit, int frequenciesToLoad[],
unsigned short nFrequenciesToLoad, bool autoSilence) {
if (title.length() == 0 && timeUnit <= 20) { return Melody(); }
if (nFrequenciesToLoad == 0 || nFrequenciesToLoad > maxLength) { return Melody(); }
if (frequenciesToLoad == nullptr) { return Melody(); }
notes = std::make_shared<std::vector<NoteDuration>>();
notes->reserve(nFrequenciesToLoad);
noteFormat = NoteFormat::INTEGER;
bool error = false;
while (this->notes->size() < nFrequenciesToLoad && !error) {
String noteDuration = String(frequenciesToLoad[notes->size()]) + ",1";
error = !loadNote(noteDuration);
}
if (error) { return Melody(); }
return Melody(title, timeUnit, notes, autoSilence);
}
bool MelodyFactoryClass::loadTitle(String line) {
removeCarriageReturn(line);
if (debug) Serial.println(String("Reading line:--") + line + "-- Len:" + line.length());
if (line.startsWith("title")) {
// Skip also '='
String title = line.substring(6);
this->title = title;
return true;
}
return false;
}
bool MelodyFactoryClass::loadTimeUnit(String line) {
removeCarriageReturn(line);
if (debug) Serial.println(String("Reading line:--") + line + "-- Len:" + line.length());
if (line.startsWith("timeUnit")) {
// Skip '='
String t = line.substring(9);
this->timeUnit = t.toInt();
if (debug) Serial.println(this->timeUnit);
if (this->timeUnit > 20) { return true; }
}
return false;
}
bool MelodyFactoryClass::loadNumberOfNotes(String line) {
removeCarriageReturn(line);
if (debug) Serial.println(String("Reading line:--") + line + "-- Len:" + line.length());
if (line.startsWith("length")) {
// Skip also '='
String len = line.substring(7);
this->nNotes = len.toInt();
if (debug) Serial.println(this->nNotes);
return true;
}
return false;
}
MelodyFactoryClass::NoteFormat MelodyFactoryClass::loadNoteFormat(String line) {
removeCarriageReturn(line);
if (debug) Serial.println(String("Reading line:--") + line + "-- Len:" + line.length());
String format;
if (line.startsWith("format")) {
// Skip also '='
format = line.substring(7);
if (debug) Serial.println(format);
}
NoteFormat noteFormat = NoteFormat::ERROR;
if (format == "string") {
noteFormat = NoteFormat::STRING;
} else if (format == "integer") {
noteFormat = NoteFormat::INTEGER;
}
return noteFormat;
}
bool MelodyFactoryClass::loadNote(String token) {
token.trim();
NoteDuration note;
if (debug) Serial.println(String("note+duration: ") + token);
String aux;
unsigned int j = 0;
// Get the frequency
while (j < token.length() && token.charAt(j) != ',') {
aux += token.charAt(j);
j++;
}
if (noteFormat == NoteFormat::STRING) {
auto n = std::find_if(noteMapping.cbegin(), noteMapping.cend(),
[&aux](std::pair<StringView, unsigned short> e) {
return e.first == aux.c_str();
});
if (n != noteMapping.cend()) {
note.frequency = n->second;
} else {
if (debug) Serial.println(String("This note doesn't exist: ") + aux);
return false;
}
} else if (noteFormat == NoteFormat::INTEGER) {
note.frequency = aux.toInt();
}
if (debug) Serial.println(String("freq: ") + note.frequency);
j++;
aux = "";
while (j < token.length()) {
aux += token.charAt(j);
j++;
}
note.duration = aux.toInt();
if (debug) Serial.println(String("duration: ") + note.duration);
// The representation of relative note duration is fixed-point with decimal part length = 1bit
note.duration *= 2;
notes->push_back(note);
return true;
}
MelodyFactoryClass MelodyFactory;

View File

@@ -0,0 +1,163 @@
/***************************************************************************
* This file is part of Melody Player, a library for Arduino *
* to play notes on piezoelectric buzzers. *
* *
* Copyright (C) 2020-2022 Fabiano Riccardi *
* *
* This library is free software; you can redistribute *
* it and/or modify it 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. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this library; if not, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#ifndef MELODY_FACTORY_H
#define MELODY_FACTORY_H
#include "melody.h"
#include <FS.h>
#ifdef ESP32
#include <SPIFFS.h>
#endif
class MelodyFactoryClass {
public:
/**
* Load the melody from file in MelodyPlayer format.
*/
Melody load(String filePath, FS& fs = SPIFFS);
/**
* Load melody from file in RTTTL format. The file must contain only one melody.
*/
Melody loadRtttlFile(String filePath, FS& fs = SPIFFS);
/**
* Load melody with the given title from a file containing multiple RTTTL melody (one Melody per
* line).
*/
Melody loadRtttlDB(String filepath, String title, FS& fs = SPIFFS);
/**
* Load melody from string in RTTTL format.
*/
Melody loadRtttlString(const char rtttlMelody[]);
/**
* Create a melody with the given parameters.
* Notes are represented as string accordigly to english notation (i.e. "C4", "G3", "G6").
* This method assumes that each note lasts 1 beat.
* frequenciesToLoad are integer numbers expressing the real reproduced frequency.
* automaticSilence, if true, automatically inserts a small silence between 2 consecutive notes.
*/
Melody load(String title, unsigned short timeUnit, String notesToLoad[],
unsigned short nNotesToLoad, bool autoSilence = true);
/**
* Create a melody with the given parameters.
* This method assumes that each note lasts 1 beat.
* frequenciesToLoad are integer numbers expressing the real reproduced frequency.
* The last parameter, automaticSilence, if true, automatically inserts a small silence between 2
* consecutive notes.
*/
Melody load(String title, unsigned short timeUnit, int frequenciesToLoad[],
unsigned short nFrequenciesToLoad, bool autoSilence = true);
private:
enum class NoteFormat { ERROR, STRING, INTEGER };
String title;
unsigned short timeUnit;
NoteFormat noteFormat;
std::shared_ptr<std::vector<NoteDuration>> notes;
// Used to check how many notes are stored in a file.
unsigned short nNotes;
const unsigned short maxLength = 1000;
///////////// RTTTL helpers
/**
* The default duration of a note. For example,
* "4" means that each note with no duration specifier
* is by default considered a quarter note. Possibile values:
* 1 - whole note
* 2 - half note
* 4 - quarter note
* 8 - eighth note
* 16 - sixteenth note
* 32 - thirty-second note
*/
const unsigned short defaultDuration = 4;
unsigned short duration;
/**
* The default octave. There are four octaves in the RTTTL format [4-7].
*/
const unsigned short defaultOctave = 6;
unsigned short octave;
/**
* The default BPM (beats per minute) value. BPM is arbitrarily limited between 10 and 300. Look
* at the implementation of parseBeat for more info.
*/
const unsigned short defaultBeat = 63;
unsigned short beat;
/**
* Try to parse the default parameters of RTTTL melody.
* If user-defined defaults are not found it sets the default values as prescribed by RTTTL
* specification.
*/
void parseDefaultValues(String values);
unsigned int parseDuration(const String& s, int& startFrom);
unsigned int parseOctave(const String& s, int& startFrom);
unsigned int parseBeat(const String& s, int& startFrom);
bool parseRtttlNote(const String& s);
//////////// END RTTTL helpers
/**
* Parse the title from the given string.
* Return true on success.
*/
bool loadTitle(String line);
/**
* Parse the time unit from the given string.
* Return true on success.
*/
bool loadTimeUnit(String line);
/**
* Parse the number of notes from the given string.
* Return true on success.
*/
bool loadNumberOfNotes(String line);
/**
* Parse the note's format from the given string.
*/
NoteFormat loadNoteFormat(String line);
/**
* Parse a token (a note and its duration) from the given string.
* The format of this token is:
* <note> + ',' + <duration>.
* Return true if the parsing succeeds, false otherwise.
*/
bool loadNote(String token);
// Enable debug messages over serial port
static const bool debug = false;
};
extern MelodyFactoryClass MelodyFactory;
#endif // END MELODY_FACTORY_H

View File

@@ -0,0 +1,368 @@
/***************************************************************************
* This file is part of Melody Player, a library for Arduino *
* to play notes on piezoelectric buzzers. *
* *
* Copyright (C) 2020-2022 Fabiano Riccardi *
* *
* This library is free software; you can redistribute *
* it and/or modify it 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. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this library; if not, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#include "melody_factory.h"
#include "notes.h"
#include <LittleFS.h>
// clang-format off
const uint16_t sourceNotes[] = {
0,
NOTE_C4,
NOTE_CS4,
NOTE_D4,
NOTE_DS4,
NOTE_E4,
NOTE_F4,
NOTE_FS4,
NOTE_G4,
NOTE_GS4,
NOTE_A4,
NOTE_AS4,
NOTE_B4,
NOTE_C5,
NOTE_CS5,
NOTE_D5,
NOTE_DS5,
NOTE_E5,
NOTE_F5,
NOTE_FS5,
NOTE_G5,
NOTE_GS5,
NOTE_A5,
NOTE_AS5,
NOTE_B5,
NOTE_C6,
NOTE_CS6,
NOTE_D6,
NOTE_DS6,
NOTE_E6,
NOTE_F6,
NOTE_FS6,
NOTE_G6,
NOTE_GS6,
NOTE_A6,
NOTE_AS6,
NOTE_B6,
NOTE_C7,
NOTE_CS7,
NOTE_D7,
NOTE_DS7,
NOTE_E7,
NOTE_F7,
NOTE_FS7,
NOTE_G7,
NOTE_GS7,
NOTE_A7,
NOTE_AS7,
NOTE_B7,
2 * NOTE_C7,
2 * NOTE_CS7,
2 * NOTE_D7,
2 * NOTE_DS7,
2 * NOTE_E7,
2 * NOTE_F7,
2 * NOTE_FS7,
2 * NOTE_G7,
2 * NOTE_GS7,
2 * NOTE_A7,
2 * NOTE_AS7,
2 * NOTE_B7,
};
// clang-format on
Melody MelodyFactoryClass::loadRtttlFile(String filepath, FS& fs) {
File f = LittleFS.open(filepath, "r");
f.setTimeout(0);
if (!f) {
if (debug) Serial.println("Opening file error");
return Melody();
}
String title = f.readStringUntil(':');
title.trim();
if (debug) Serial.println(String("Title:") + title);
if (title.length() == 0) { return Melody(); }
String values = f.readStringUntil(':');
values.trim();
if (debug) Serial.println(String("Default values:") + values);
if (values.length() == 0) { return Melody(); }
parseDefaultValues(values);
// 32 because it is the shortest note!
int timeUnit = 60 * 1000 * 4 / beat / 32;
notes = std::make_shared<std::vector<NoteDuration>>();
bool result = true;
while (f.available() && notes->size() < maxLength && result) {
String s = f.readStringUntil(',');
s.trim();
result = parseRtttlNote(s);
}
if (result && notes->size() > 0) { return Melody(title, timeUnit, notes, false); }
return Melody();
}
Melody MelodyFactoryClass::loadRtttlDB(String filepath, String title, FS& fs) {
File f = LittleFS.open(filepath, "r");
f.setTimeout(0);
if (!f) {
if (debug) Serial.println("Opening file error");
return Melody();
}
if (title.length() == 0) {
if (debug) Serial.println("Title length = 0");
return Melody();
}
if (!f.find(title.c_str())) {
if (debug) Serial.println("Unable to find melody with title: " + String(title));
return Melody();
}
f.readStringUntil(':');
String values = f.readStringUntil(':');
values.trim();
if (debug) Serial.println(String("Default values:") + values);
if (values.length() == 0) { return Melody(); }
parseDefaultValues(values);
// 32 because it is the shortest note!
int timeUnit = 60 * 1000 * 4 / beat / 32;
size_t position = f.position();
int bytesUntilNewLine = f.readStringUntil('\n').length();
f.seek(position);
notes = std::make_shared<std::vector<NoteDuration>>();
bool result = true;
while (f.available() && notes->size() < maxLength && result && bytesUntilNewLine > 0) {
String s = f.readStringUntil(',');
if (s.length() > bytesUntilNewLine) { s = s.substring(0, bytesUntilNewLine); }
bytesUntilNewLine -= s.length() + 1;
s.trim();
result = parseRtttlNote(s);
}
if (result && notes->size() > 0) { return Melody(title, timeUnit, notes, false); }
return Melody();
}
Melody MelodyFactoryClass::loadRtttlString(const char rtttlMelody[]) {
String title;
int i = 0;
while (rtttlMelody[i] != 0 && rtttlMelody[i] != ':') {
title.concat(rtttlMelody[i]);
i++;
}
if (title.length() == 0 || rtttlMelody[i] == 0) { return Melody(); }
// skip ':'
i++;
String defaultParameters;
while (rtttlMelody[i] != 0 && rtttlMelody[i] != ':') {
defaultParameters.concat(rtttlMelody[i]);
i++;
}
if (rtttlMelody[i] == 0) { return Melody(); }
defaultParameters.trim();
parseDefaultValues(defaultParameters);
// 32 because it is the shortest note!
int timeUnit = 60 * 1000 * 4 / beat / 32;
// skip ':'
i++;
notes = std::make_shared<std::vector<NoteDuration>>();
// Read notes
while (rtttlMelody[i] != 0) {
String note;
while (rtttlMelody[i] != 0 && rtttlMelody[i] != ',') {
note.concat(rtttlMelody[i]);
i++;
}
note.trim();
parseRtttlNote(note);
if (rtttlMelody[i] == ',') { i++; }
}
if (notes->size() > 0) { return Melody(title, timeUnit, notes, false); }
return Melody();
}
/**
* Parse an unsigned integer starting from the given startFrom to the first non-digit char.
* Return zero if it cannot parse a number. *startFrom* will point to the first non-digit char.
*/
unsigned int getUnsignedInt(const String& s, int& startFrom) {
unsigned int temp = 0;
while (isDigit(s.charAt(startFrom))) {
temp = (temp * 10) + s.charAt(startFrom) - '0';
startFrom++;
}
return temp;
}
unsigned int MelodyFactoryClass::parseDuration(const String& s, int& startFrom) {
// Skip '='
startFrom++;
unsigned int temp = getUnsignedInt(s, startFrom);
if (temp != 1 && temp != 2 && temp != 4 && temp != 8 && temp != 16 && temp != 32) { return 0; }
// Discard ','
startFrom++;
return temp;
}
unsigned int MelodyFactoryClass::parseOctave(const String& s, int& startFrom) {
// Skip '='
startFrom++;
unsigned int temp = getUnsignedInt(s, startFrom);
if (temp < 4 || temp > 7) { return 0; }
// Discard ','
startFrom++;
return temp;
}
unsigned int MelodyFactoryClass::parseBeat(const String& s, int& startFrom) {
// Skip '='
startFrom++;
unsigned int temp = getUnsignedInt(s, startFrom);
// BPM is arbitrarily limited to 300. You may try to increase it, but remember that
// actually, the minimum note length is 60(seconds)/300(bpm)/32(minimum note length) = 6.25ms.
// If you reduce this duration, you may not be able to keep up the pace to play a smooth
// async playback while doing other operations.
if (!(10 <= temp && temp <= 300)) { return 0; }
// Discard ','
startFrom++;
return temp;
}
bool MelodyFactoryClass::parseRtttlNote(const String& s) {
int i = 0;
unsigned short relativeDuration = this->duration;
// Optional number: note duration (e.g 4=quarter note, ...)
if (isdigit(s.charAt(i))) {
unsigned int temp = getUnsignedInt(s, i);
if (temp) { relativeDuration = temp; }
}
// To match struct NoteDuration format, I need the direct
// note length, instead RTTTL provides the denominator
// of the whole note
if (relativeDuration == 32) {
relativeDuration = 1;
} else if (relativeDuration == 16) {
relativeDuration = 2;
} else if (relativeDuration == 8) {
relativeDuration = 4;
} else if (relativeDuration == 4) {
relativeDuration = 8;
} else if (relativeDuration == 2) {
relativeDuration = 16;
} else if (relativeDuration == 1) {
relativeDuration = 32;
} else {
relativeDuration = 0;
}
// note (p is silence)
int note = 0;
switch (s.charAt(i)) {
case 'c': note = 1; break;
case 'd': note = 3; break;
case 'e': note = 5; break;
case 'f': note = 6; break;
case 'g': note = 8; break;
case 'a': note = 10; break;
case 'b': note = 12; break;
case 'p':
default: note = 0;
}
i++;
// Optional #
if (s.charAt(i) == '#') {
note++;
i++;
}
// The representation of relative note duration is fixed-point with decimal part length = 1bit
relativeDuration *= 2;
// get optional '.' dotted note
// This note will last 50% more
if (s.charAt(i) == '.') {
relativeDuration += relativeDuration / 2;
i++;
}
int scale;
// now, get scale
if (isdigit(s.charAt(i))) {
scale = s.charAt(i) - '0';
i++;
} else {
scale = octave;
}
unsigned short freq;
if (note) {
freq = sourceNotes[(scale - 4) * 12 + note];
} else {
freq = 0;
}
notes->push_back({ .frequency = freq, .duration = relativeDuration });
return true;
}
void MelodyFactoryClass::parseDefaultValues(String values) {
int i = 0;
if (values.charAt(i) == 'd') { i++; }
duration = parseDuration(values, i);
if (duration == 0) { duration = defaultDuration; }
if (values.charAt(i) == 'o') { i++; }
octave = parseOctave(values, i);
if (octave == 0) { octave = defaultOctave; }
if (values.charAt(i) == 'b') {
i++;
beat = parseBeat(values, i);
}
if (beat == 0) { beat = defaultBeat; }
}

View File

@@ -0,0 +1,230 @@
/***************************************************************************
* This file is part of Melody Player, a library for Arduino *
* to play notes on piezoelectric buzzers. *
* *
* Copyright (C) 2020-2022 Fabiano Riccardi *
* *
* This library is free software; you can redistribute *
* it and/or modify it 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. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this library; if not, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#include "melody_player.h"
/**
* https://stackoverflow.com/questions/24609271/errormake-unique-is-not-a-member-of-std
*/
template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
void MelodyPlayer::play() {
if (melodyState == nullptr) { return; }
turnOn();
state = State::PLAY;
melodyState->advance();
while (melodyState->getIndex() + melodyState->isSilence() < melodyState->melody.getLength()) {
NoteDuration computedNote = melodyState->getCurrentComputedNote();
if (debug)
Serial.println(String("Playing: frequency:") + computedNote.frequency
+ " duration:" + computedNote.duration);
if (melodyState->isSilence()) {
#ifdef ESP32
ledcWriteTone(pwmChannel, 0);
#else
noTone(pin);
#endif
delay(0.3f * computedNote.duration);
} else {
#ifdef ESP32
ledcWriteTone(pwmChannel, computedNote.frequency);
#else
tone(pin, computedNote.frequency);
#endif
delay(computedNote.duration);
}
melodyState->advance();
}
stop();
}
void MelodyPlayer::play(Melody& melody) {
if (!melody) { return; }
melodyState = make_unique<MelodyState>(melody);
play();
}
void changeTone(MelodyPlayer* player) {
// The last silence is not reproduced
player->melodyState->advance();
if (player->melodyState->getIndex() + player->melodyState->isSilence()
< player->melodyState->melody.getLength()) {
NoteDuration computedNote(player->melodyState->getCurrentComputedNote());
float duration = player->melodyState->getRemainingNoteDuration();
if (duration > 0) {
player->melodyState->resetRemainingNoteDuration();
} else {
if (player->melodyState->isSilence()) {
duration = 0.3f * computedNote.duration;
} else {
duration = computedNote.duration;
}
}
if (player->debug)
Serial.println(String("Playing async: freq=") + computedNote.frequency + " dur=" + duration
+ " iteration=" + player->melodyState->getIndex());
if (player->melodyState->isSilence()) {
#ifdef ESP32
ledcWriteTone(player->pwmChannel, 0);
#else
tone(player->pin, 0);
#endif
#ifdef ESP32
player->ticker.once_ms(duration, changeTone, player);
#else
player->ticker.once_ms_scheduled(duration, std::bind(changeTone, player));
#endif
} else {
#ifdef ESP32
ledcWriteTone(player->pwmChannel, computedNote.frequency);
#else
tone(player->pin, computedNote.frequency);
#endif
#ifdef ESP32
player->ticker.once_ms(duration, changeTone, player);
#else
player->ticker.once_ms_scheduled(duration, std::bind(changeTone, player));
#endif
}
player->supportSemiNote = millis() + duration;
} else {
player->stop();
}
}
void MelodyPlayer::playAsync() {
if (melodyState == nullptr) { return; }
turnOn();
state = State::PLAY;
// Start immediately
#ifdef ESP32
ticker.once(0, changeTone, this);
#else
ticker.once_scheduled(0, std::bind(changeTone, this));
#endif
}
void MelodyPlayer::playAsync(Melody& melody) {
if (!melody) { return; }
melodyState = make_unique<MelodyState>(melody);
playAsync();
}
void MelodyPlayer::stop() {
if (melodyState == nullptr) { return; }
haltPlay();
state = State::STOP;
melodyState->reset();
}
void MelodyPlayer::pause() {
if (melodyState == nullptr) { return; }
haltPlay();
state = State::PAUSE;
melodyState->saveRemainingNoteDuration(supportSemiNote);
}
void MelodyPlayer::transferMelodyTo(MelodyPlayer& destPlayer) {
if (melodyState == nullptr) { return; }
destPlayer.stop();
bool playing = isPlaying();
haltPlay();
state = State::STOP;
melodyState->saveRemainingNoteDuration(supportSemiNote);
destPlayer.melodyState = std::move(melodyState);
if (playing) {
destPlayer.playAsync();
} else {
destPlayer.state = state;
}
}
void MelodyPlayer::duplicateMelodyTo(MelodyPlayer& destPlayer) {
if (melodyState == nullptr) { return; }
destPlayer.stop();
destPlayer.melodyState = make_unique<MelodyState>(*(this->melodyState));
destPlayer.melodyState->saveRemainingNoteDuration(supportSemiNote);
if (isPlaying()) {
destPlayer.playAsync();
} else {
destPlayer.state = state;
}
}
#ifdef ESP32
MelodyPlayer::MelodyPlayer(unsigned char pin, unsigned char pwmChannel, bool offLevel)
: pin(pin), pwmChannel(pwmChannel), offLevel(offLevel), state(State::STOP), melodyState(nullptr) {
pinMode(pin, OUTPUT);
digitalWrite(pin, offLevel);
};
#else
MelodyPlayer::MelodyPlayer(unsigned char pin, bool offLevel)
: pin(pin), offLevel(offLevel), state(State::STOP), melodyState(nullptr) {
pinMode(pin, OUTPUT);
digitalWrite(pin, offLevel);
};
#endif
void MelodyPlayer::haltPlay() {
// Stop player, but do not reset the melodyState
ticker.detach();
turnOff();
}
void MelodyPlayer::turnOn() {
#ifdef ESP32
const int resolution = 8;
// 2000 is a frequency, it will be changed at the first play
ledcSetup(pwmChannel, 2000, resolution);
ledcAttachPin(pin, pwmChannel);
ledcWrite(pwmChannel, 125);
#endif
}
void MelodyPlayer::turnOff() {
#ifdef ESP32
ledcWrite(pwmChannel, 0);
ledcDetachPin(pin);
#else
// Remember that this will set LOW output, it doesn't mean that buzzer is off (look at offLevel
// for more info).
noTone(pin);
#endif
pinMode(pin, LOW);
digitalWrite(pin, offLevel);
}

View File

@@ -0,0 +1,248 @@
/***************************************************************************
* This file is part of Melody Player, a library for Arduino *
* to play notes on piezoelectric buzzers. *
* *
* Copyright (C) 2020-2022 Fabiano Riccardi *
* *
* This library is free software; you can redistribute *
* it and/or modify it 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. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this library; if not, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#ifndef MELODY_PLAYER_H
#define MELODY_PLAYER_H
#include "melody.h"
#include <Ticker.h>
#include <memory>
class MelodyPlayer {
public:
#ifdef ESP32
/**
* pwmChannel is optional and you have to configure it only if you play will
* simultaneous melodies.
*/
MelodyPlayer(unsigned char pin, unsigned char pwmChannel = 0, bool offLevel = HIGH);
#else
MelodyPlayer(unsigned char pin, bool offLevel = HIGH);
#endif
/**
* Play the last melody in a synchrounus (blocking) way.
* If the melody is not valid, this call has no effect.
*/
void play();
/**
* Play the given melody in a synchronous (blocking) way.
* If the melody is not valid, this call has no effect.
*/
void play(Melody& melody);
/**
* Play the last melody in asynchronous way (return immediately).
* If the melody is not valid, this call has no effect.
*/
void playAsync();
/**
* Play the given melody in asynchronous way (return immediately).
* If the melody is not valid, this call has no effect.
*/
void playAsync(Melody& melody);
/**
* Stop the current melody.
* Then, if you will call play() or playAsync(), the melody restarts from the begin.
*/
void stop();
/**
* Pause the current melody.
* Then, if you will call play() or playAsync(), the melody continues from
* where it was paused.
*/
void pause();
/**
* Tell if playing.
*/
bool isPlaying() const {
return state == State::PLAY;
}
/**
* Move the current melody and player's state to the given destination Player.
* The source player stops and lose the reference to the actual melody (i.e. you have to call
* play(Melody) to make the source player play again).
*/
void transferMelodyTo(MelodyPlayer& destination);
/**
* Duplicate the current melody and player's state to the given destination Player.
* Both players remains indipendent from each other (e.g. the melody can be independently
* stopped/paused/played).
*/
void duplicateMelodyTo(MelodyPlayer& destination);
private:
unsigned char pin;
#ifdef ESP32
unsigned char pwmChannel;
#endif
/**
* The voltage to turn off the buzzer.
*
* NOTE: Passive buzzers have 2 states: the "rest" state (no power consumption) and the "active"
* state (high power consumption). To emit sound, it have to oscillate between these 2 states. If
* it stops in the active state, it doesn't emit sound, but it continues to consume energy,
* heating the buzzer and possibly damaging itself.
*/
bool offLevel;
/**
* Store the playback state of a melody and provide the methods to control it.
*/
class MelodyState {
public:
MelodyState() : first(true), index(0), remainingNoteTime(0){};
MelodyState(const Melody& melody)
: melody(melody), first(true), silence(false), index(0), remainingNoteTime(0){};
Melody melody;
unsigned short getIndex() const {
return index;
}
bool isSilence() const {
return silence;
}
/**
* Advance the melody index by one step. If there is a pending partial note it hasn't any
* effect.
*/
void advance() {
if (first) {
first = false;
return;
}
if (remainingNoteTime != 0) { return; }
if (melody.getAutomaticSilence()) {
if (silence) {
index++;
silence = false;
} else {
silence = true;
}
} else {
index++;
}
}
/**
* Reset the state of the melody (i.e. a melody just loaded).
*/
void reset() {
first = true;
index = 0;
remainingNoteTime = 0;
silence = false;
}
/**
* Save the time to finish the current note.
*/
void saveRemainingNoteDuration(unsigned long supportSemiNote) {
remainingNoteTime = supportSemiNote - millis();
// Ignore partial reproduction if the current value is below the threshold. This is needed
// since Ticker may struggle with tight timings.
if (remainingNoteTime < 10) { remainingNoteTime = 0; }
}
/**
* Clear the partial note duration. It should be called after reproduction of the partial note
* and it is propedeutic to advance() method.
*/
void resetRemainingNoteDuration() {
remainingNoteTime = 0;
}
/**
* Get the remaining duration of the latest "saved" note.
*/
unsigned short getRemainingNoteDuration() const {
return remainingNoteTime;
}
/**
* Get the current note. The duration is absolute and expressed in milliseconds.
*/
NoteDuration getCurrentComputedNote() const {
NoteDuration note = melody.getNote(getIndex());
note.duration = melody.getTimeUnit() * note.duration;
// because the fixed point notation
note.duration /= 2;
return note;
}
private:
bool first;
bool silence;
unsigned short index;
/**
* Variable to support precise pauses and move/duplicate melodies between Players.
* Value are expressed in milliseconds.
*/
unsigned short remainingNoteTime;
};
enum class State { STOP, PLAY, PAUSE };
State state;
std::unique_ptr<MelodyState> melodyState;
unsigned long supportSemiNote;
Ticker ticker;
const static bool debug = false;
/**
* Change the current note with the next one.
*/
friend void changeTone(MelodyPlayer* melody);
/**
* Halt the advancement of the melody reproduction.
* This is the shared method for pause and stop.
*/
void haltPlay();
/**
* Configure pin to emit PWM.
*/
void turnOn();
/**
* Disable PWM and put the buzzer is low-power state.
* This calls will fails if PWM is not initialized!
*/
void turnOff();
};
#endif // END MELODY_PLAYER_H

View File

@@ -0,0 +1,89 @@
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978

View File

@@ -0,0 +1,147 @@
/***************************************************************************
* This file is part of Melody Player, a library for Arduino *
* to play notes on piezoelectric buzzers. *
* *
* Copyright (C) 2020-2022 Fabiano Riccardi *
* *
* This library is free software; you can redistribute *
* it and/or modify it 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. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this library; if not, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#ifndef PITCHES_UNORDERED_MAP_H
#define PITCHES_UNORDERED_MAP_H
#include <array>
/**
* This class resembles the std::string_view class introduced with c++17. MelodyPlayer uses this
* version to ensure retro-compatibility with esp8266-core v2.x.x, which uses GCC v4.8.2.
*/
struct StringView {
StringView() = delete;
constexpr StringView(const char* s) : str(s), lenght(__builtin_strlen(s)) {}
constexpr bool operator==(const StringView& other) const {
return lenght == other.lenght && (__builtin_memcmp(str, other.str, lenght) == 0);
}
constexpr size_t length() const {
return lenght;
}
constexpr const char* data() const {
return str;
}
private:
const char* str;
size_t lenght;
};
// clang-format off
constexpr std::array<std::pair<StringView, unsigned short>, 92> noteMapping{{
{"SILENCE", 0 },
{ "B0", 31 },
{ "C1", 33 },
{ "CS1", 35 },
{ "D1", 37 },
{ "DS1", 39 },
{ "D1", 37 },
{ "DS1", 39 },
{ "E1", 41 },
{ "F1", 44 },
{ "FS1", 46 },
{ "G1", 49 },
{ "GS1", 52 },
{ "A1", 55 },
{ "AS1", 58 },
{ "B1", 62 },
{ "C2", 65 },
{ "CS2", 69 },
{ "D2", 73 },
{ "DS2", 78 },
{ "E2", 82 },
{ "F2", 87 },
{ "FS2", 93 },
{ "G2", 98 },
{ "GS2", 104 },
{ "A2", 110 },
{ "AS2", 117 },
{ "B2", 123 },
{ "C3", 131 },
{ "CS3", 139 },
{ "D3", 147 },
{ "DS3", 156 },
{ "E3", 165 },
{ "F3", 175 },
{ "FS3", 185 },
{ "G3", 196 },
{ "GS3", 208 },
{ "A3", 220 },
{ "AS3", 233 },
{ "B3", 247 },
{ "C4", 262 },
{ "CS4", 277 },
{ "D4", 294 },
{ "DS4", 311 },
{ "E4", 330 },
{ "F4", 349 },
{ "FS4", 370 },
{ "G4", 392 },
{ "GS4", 415 },
{ "A4", 440 },
{ "AS4", 466 },
{ "B4", 494 },
{ "C5", 523 },
{ "CS5", 554 },
{ "D5", 587 },
{ "DS5", 622 },
{ "E5", 659 },
{ "F5", 698 },
{ "FS5", 740 },
{ "G5", 784 },
{ "GS5", 831 },
{ "A5", 880 },
{ "AS5", 932 },
{ "B5", 988 },
{ "C6", 1047},
{ "CS6", 1109},
{ "D6", 1175},
{ "DS6", 1245},
{ "E6", 1319},
{ "F6", 1397},
{ "FS6", 1480},
{ "G6", 1568},
{ "GS6", 1661},
{ "A6", 1760},
{ "AS6", 1865},
{ "B6", 1976},
{ "C7", 2093},
{ "CS7", 2217},
{ "D7", 2349},
{ "DS7", 2489},
{ "E7", 2637},
{ "F7", 2794},
{ "FS7", 2960},
{ "G7", 3136},
{ "GS7", 3322},
{ "A7", 3520},
{ "AS7", 3729},
{ "B7", 3951},
{ "C8", 4186},
{ "CS8", 4435},
{ "D8", 4699},
{ "DS8", 4978}
}};
// clang-format on
#endif // END PITCHES_UNORDERED_MAP_H