/*************************************************************************** * 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 * ***************************************************************************/ #include "melody_factory.h" #include "notes.h" #include // 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>(); 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>(); 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>(); // 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; } }