Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarSongbook.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarSongbook.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarSongbook.cpp')
-rw-r--r--source/game/StarSongbook.cpp729
1 files changed, 729 insertions, 0 deletions
diff --git a/source/game/StarSongbook.cpp b/source/game/StarSongbook.cpp
new file mode 100644
index 0000000..482fcd3
--- /dev/null
+++ b/source/game/StarSongbook.cpp
@@ -0,0 +1,729 @@
+#include "StarSongbook.hpp"
+#include "StarRoot.hpp"
+#include "StarAssets.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarRandom.hpp"
+#include "StarWorld.hpp"
+#include "StarLogging.hpp"
+#include "StarEntityRendering.hpp"
+#include "StarTime.hpp"
+
+namespace Star {
+
+Mutex Songbook::s_timeSourcesMutex;
+StringMap<shared_ptr<Songbook::TimeSource>> Songbook::s_timeSources;
+
+Songbook::Songbook(String const& species) {
+ m_activeCooldown = 0;
+ m_dataUpdated = false;
+ m_dataChanged = false;
+ m_timeSourceEpoch = 0;
+ m_globalNowDelta = 0;
+ m_species = species;
+ m_stopped = true;
+ m_serverMode = true;
+
+ addNetElement(&m_songNetState);
+ addNetElement(&m_timeSourceEpochNetState);
+ addNetElement(&m_timeSourceNetState);
+ addNetElement(&m_activeNetState);
+ addNetElement(&m_instrumentNetState);
+}
+
+Songbook::~Songbook() {
+ stop();
+}
+
+Songbook::NoteMapping& Songbook::noteMapping(String const& instrument, String const& species, int note) {
+ if (!m_noteMapping.contains(instrument)) {
+ Map<int, NoteMapping> notemap;
+ auto tuning = Root::singleton().assets()->json(strf("/sfx/instruments/%s/tuning.config", instrument));
+ for (auto e : tuning.get("mapping").iterateObject()) {
+ int keyNumber = lexicalCast<int>(e.first);
+ NoteMapping nm;
+ if (e.second.contains("file")) {
+ nm.files.append(e.second.getString("file", "").replace("$instrument$", instrument).replace("$species$", species));
+ } else if (e.second.contains("files")) {
+ for (auto entry : e.second.getArray("files"))
+ nm.files.append(entry.toString().replace("$instrument$", instrument).replace("$species$", species));
+ }
+ nm.frequency = e.second.getDouble("f");
+ nm.velocity = 1;
+ nm.fadeout = e.second.getDouble("fadeOut", tuning.getDouble("fadeout"));
+ notemap[keyNumber] = nm;
+ }
+ for (int key = 21; key <= 108; ++key) {
+ NoteMapping& nm = notemap[key];
+ if (nm.files.empty()) {
+ auto prev = notemap[key - 1];
+ nm.files = prev.files;
+ nm.velocity = prev.velocity * nm.frequency / prev.frequency;
+ }
+ }
+ m_noteMapping[instrument] = notemap;
+ }
+ return m_noteMapping[instrument][note];
+}
+
+void Songbook::update(EntityMode mode, World* world) {
+ m_serverMode = world->isServer();
+
+ if (m_serverMode)
+ return;
+
+ m_globalNowDelta = world->epochTime() * 1000 - Time::millisecondsSinceEpoch();
+ if (m_dataUpdated) {
+ m_dataUpdated = false;
+ if (!m_song.isNull()) {
+ try {
+ {
+ MutexLocker lock(s_timeSourcesMutex);
+ if (!s_timeSources.contains(m_timeSource)) {
+ m_timeSourceInstance = make_shared<TimeSource>();
+ s_timeSources[m_timeSource] = m_timeSourceInstance;
+ m_timeSourceInstance->epoch = m_timeSourceEpoch;
+ m_timeSourceInstance->keepalive = m_timeSourceEpoch;
+ } else
+ m_timeSourceInstance = s_timeSources[m_timeSource];
+ }
+ m_track.clear();
+ m_stopped = false;
+ m_track.appendAll(parseABC(m_song.getString("abc")));
+ } catch (StarException const& e) {
+ Logger::error("Failed to handle abc: %s", outputException(e, true));
+ m_stopped = true;
+ }
+ }
+ }
+ if (mode == EntityMode::Master) {
+ if (active())
+ m_activeCooldown--;
+ }
+ playback();
+}
+
+void Songbook::playback() {
+ if (!active() || (m_track.empty() && m_heldNotes.empty())) {
+ stop();
+ return;
+ }
+ m_timeSourceInstance->keepalive = Time::millisecondsSinceEpoch();
+ auto now = (Time::millisecondsSinceEpoch() - m_timeSourceInstance->epoch) / 1000.0;
+ if (!m_heldNotes.empty()) {
+ for (auto& note : m_heldNotes)
+ note.audio->setPosition(m_position);
+ eraseWhere(m_heldNotes, [&](HeldNote const& note) -> bool {
+ return note.audio->finished();
+ });
+ }
+
+ while (!m_track.empty() && (m_track.first().timecode <= (now + 0.5))) {
+ auto note = m_track.takeFirst();
+ auto delta = now - note.timecode;
+ if (delta > 1)
+ continue; // skip notes that are more than a second behind
+ if (!m_uncompressedSamples.contains(note.file)) {
+ auto sample = Root::singleton().assets()->audio(note.file);
+ if (sample->compressed()) {
+ auto copy = make_shared<Audio>(*sample);
+ copy->uncompress();
+ m_uncompressedSamples[note.file] = copy;
+ } else
+ m_uncompressedSamples[note.file] = sample;
+ }
+
+ AudioInstancePtr audioInstance = make_shared<AudioInstance>(*m_uncompressedSamples[note.file]);
+ audioInstance->setPitchMultiplier(note.velocity);
+
+ auto start = m_timeSourceInstance->epoch + (int64_t)(note.timecode * 1000.0);
+ audioInstance->setClockStart(start);
+ audioInstance->setClockStop(start + (int64_t)(note.duration * 1000.0), (int64_t)(note.fadeout * 1000.0));
+
+ audioInstance->setPosition(m_position);
+
+ m_pendingAudio.append(audioInstance);
+ m_heldNotes.append(HeldNote{audioInstance, note.timecode, note.duration + note.timecode});
+ }
+}
+
+void Songbook::render(RenderCallback* renderCallback) {
+ for (auto& a : m_pendingAudio)
+ renderCallback->addAudio(a);
+ m_pendingAudio.clear();
+}
+
+void Songbook::keepalive(String const& instrument, Vec2F const& position) {
+ if (instrument != m_instrument) {
+ m_instrument = instrument;
+ m_dataUpdated = true;
+ }
+ m_position = position;
+ if (active())
+ m_activeCooldown = 3;
+}
+
+List<Songbook::Note> Songbook::parseABC(String const& abc) {
+ List<Songbook::Note> result;
+
+ StringMap<String> fields;
+
+ auto meter = [&]() -> Vec2I {
+ auto m = fields.value("M", "C");
+ if (m.equalsIgnoreCase("C"))
+ m = "4/4";
+ if (m.equalsIgnoreCase("C|"))
+ m = "2/2";
+ auto p = m.split("/", 1);
+ return Vec2I{lexicalCast<int>(p[0]), lexicalCast<int>(p[1])};
+ };
+
+ auto noteLength = [&]() -> Vec2I {
+ auto m = fields.value("L", "C");
+ if (m.equalsIgnoreCase("C"))
+ return {1, meter()[1]};
+ auto p = m.split("/", 1);
+ return Vec2I{lexicalCast<int>(p[0]), lexicalCast<int>(p[1])};
+ };
+
+ auto secondsPerBeat = [&]() -> double {
+ auto m = fields.value("Q", "120");
+ auto p = m.split("=", 1);
+ double secondsPerBeat = 60.0 / lexicalCast<double>(p.last());
+ if (p.size() > 1) {
+ auto pp = p[0].split("/", 1);
+ auto d = Vec2I{lexicalCast<int>(pp[0]), lexicalCast<int>(pp[1])};
+ secondsPerBeat = d[1] * secondsPerBeat / d[0];
+ } else
+ secondsPerBeat = noteLength()[1] * secondsPerBeat / noteLength()[0];
+ return secondsPerBeat;
+ };
+
+ auto transpose = [&]() -> int {
+ auto t = fields.value("Transpose", "0");
+ return lexicalCast<int>(t);
+ };
+
+ auto fetchKeySignatureMapping = [&]() -> List<int> {
+ auto cleanupKey = [](String const& key) -> String {
+ return key.toLower().replace(" ", "").replace("minor", "m").replace("min", "m").replace("major", "maj");
+ };
+ String key = cleanupKey(fields.value("K", "c"));
+ auto keys = Root::singleton().assets()->json("/songbook.config:keys");
+ while (true) {
+ if (!keys.contains(key)) {
+ Logger::info("Failed to find key %s, falling back to C", key);
+ key = "c";
+ }
+ auto signature = keys.get(key);
+ if (signature.isType(Json::Type::String)) {
+ key = cleanupKey(signature.toString());
+ continue;
+ }
+ List<int> keySignatureMapping;
+ for (auto e : signature.toArray())
+ keySignatureMapping.append(e.toInt());
+ return keySignatureMapping;
+ }
+ };
+
+ double now = 0;
+ int accidentals = 0;
+ bool accidentalSpecified = false;
+ double lastBarNow = now;
+
+ Map<int, int> impliedAccidentals;
+ bool grouped = false;
+ double groupDuration = 0;
+ bool dirtyFlags = true;
+
+ List<int> keySignatureMapping;
+ List<int> tupleMapping;
+ double fullNoteDuration = 0;
+ double noteDuration = 0;
+ double barDuration = 0;
+ Map<int, size_t> pendingTies;
+ int groupStartIndex = 0;
+
+ int tupleCount = 0;
+ double tupleDurationFactor = 0;
+ bool staccato = false;
+
+ for (auto l : abc.replace("\t", " ").splitLines()) {
+ if (l[0] == '%') {
+ // extended values support, outside of standard, semi standard seen in
+ // some files
+ // only used for Transpose
+ if ((l.length() >= 3) && (l[1] == ' ') && (l[2] == ' ')) {
+ auto s = l.substr(3).split(":", 1);
+ if (s.size() > 1) {
+ fields[s[0].trim()] = s[1].trim();
+ dirtyFlags = true;
+ }
+ }
+ continue;
+ }
+
+ if (l.contains("%")) {
+ // truncate comments
+ l = l.split("%", 1)[0];
+ }
+
+ if ((l.length() > 1) && (l[1] == ':') && (l[0] != '|')) {
+ auto s = l.split(":", 1);
+ if (s.size() > 1) {
+ fields[s[0].trim()] = s[1].trim();
+ dirtyFlags = true;
+ }
+ } else {
+ if (dirtyFlags) {
+ keySignatureMapping = fetchKeySignatureMapping();
+ fullNoteDuration = secondsPerBeat();
+ noteDuration = noteLength()[0] * fullNoteDuration / noteLength()[1];
+ auto m = meter();
+ barDuration = m[0] * fullNoteDuration / m[1];
+ dirtyFlags = false;
+
+ int tdt = 2;
+
+ if ((m[1] == 8) && ((m[0] == 6) || (m[0] == 9) || (m[0] == 12)))
+ tdt = 3;
+
+ tupleMapping.clear();
+ tupleMapping.append(0);
+ tupleMapping.append(0);
+ tupleMapping.append(3);
+ tupleMapping.append(2);
+ tupleMapping.append(3);
+ tupleMapping.append(tdt);
+ tupleMapping.append(2);
+ tupleMapping.append(tdt);
+ tupleMapping.append(3);
+ tupleMapping.append(tdt);
+ }
+
+ Deque<String::Char> buffer(l.begin(), l.end());
+
+ auto peek = [&]() -> String::Char {
+ if (buffer.empty())
+ return '\0';
+ return buffer.first();
+ };
+
+ auto readDuration = [&]() -> double {
+ double duration = 1;
+ if (String::isAsciiNumber(peek())) {
+ String s = "";
+ while (String::isAsciiNumber(peek())) {
+ s += buffer.takeFirst();
+ }
+ duration *= lexicalCast<int>(s);
+ }
+ if (peek() == '/') {
+ buffer.takeFirst();
+ double divisor = 2;
+ if (String::isAsciiNumber(peek())) {
+ String s = "";
+ while (String::isAsciiNumber(peek())) {
+ s += buffer.takeFirst();
+ }
+ divisor = lexicalCast<int>(s);
+ }
+ duration /= divisor;
+ }
+ return duration;
+ };
+
+ while (!buffer.empty()) {
+ auto head = buffer.takeFirst();
+ if (String::isSpace(head))
+ continue;
+
+ if (head == '|') {
+ now = lastBarNow + barDuration;
+ lastBarNow = now;
+ // section/repetition artifact, nor supported
+ if (peek() == ':')
+ buffer.takeFirst();
+ else if (peek() == ']')
+ buffer.takeFirst();
+ else {
+ if (peek() == '[')
+ buffer.takeFirst();
+ while (String::isAsciiNumber(peek()))
+ buffer.takeFirst();
+ }
+ accidentals = 0;
+ accidentalSpecified = false;
+ impliedAccidentals.clear();
+ continue;
+ }
+ if (head == '~') {
+ // ornament ?
+ continue;
+ }
+ if (head == ':') {
+ // section/repetition artifact, nor supported
+ buffer.takeFirst();
+ continue;
+ }
+ if (head == '^') {
+ accidentals += 1;
+ accidentalSpecified = true;
+ continue;
+ }
+ if (head == '_') {
+ accidentals -= 1;
+ accidentalSpecified = true;
+ continue;
+ }
+ if (head == '=') {
+ accidentals = 0;
+ accidentalSpecified = true;
+ continue;
+ }
+ if (head == '[') {
+ grouped = true;
+ groupStartIndex = result.size();
+ continue;
+ }
+ if (head == ']') {
+ grouped = false;
+ if (tupleCount > 0)
+ tupleCount--;
+ auto duration = readDuration();
+ if (duration != 1) {
+ for (int index = groupStartIndex; index < (int)result.size(); index++) {
+ auto& entry = result[index];
+ entry.duration *= duration;
+ }
+ }
+ now += groupDuration * duration;
+ groupDuration = 0;
+ staccato = false;
+ continue;
+ }
+ if (head == '(') {
+ if (String::isAsciiNumber(peek())) {
+ int p = 0;
+ int q = 0;
+ int r = 0;
+ p = (int)buffer.takeFirst() - (int)'0';
+ if (peek() == ':') {
+ buffer.takeFirst();
+ if (String::isAsciiNumber(peek()))
+ q = (int)buffer.takeFirst() - (int)'0';
+ if (peek() == ':') {
+ buffer.takeFirst();
+ if (String::isAsciiNumber(peek()))
+ r = (int)buffer.takeFirst() - (int)'0';
+ }
+ }
+
+ if (r == 0)
+ r = p;
+ if (q == 0)
+ q = tupleMapping[p];
+
+ tupleCount = p;
+ tupleDurationFactor = (float)q / (float)p;
+ }
+
+ continue;
+ }
+
+ if (head == '+') {
+ while (!buffer.empty()) {
+ if (buffer.takeFirst() == '+')
+ break;
+ }
+ continue;
+ }
+ if (head == '!') {
+ while (!buffer.empty()) {
+ if (buffer.takeFirst() == '!')
+ break;
+ }
+ continue;
+ }
+ if (head == '.') {
+ staccato = true;
+ continue;
+ }
+
+ if (String::isAsciiLetter(head)) {
+ int note = (String::toLower(head) != head) ? 60 : 72;
+ while (peek() == ',') {
+ buffer.takeFirst();
+ note -= 12;
+ }
+ while (peek() == '\'') {
+ buffer.takeFirst();
+ note += 12;
+ }
+ switch (String::toLower(head)) {
+ case 'c': {
+ note += 0;
+ break;
+ }
+ case 'd': {
+ note += 2;
+ break;
+ }
+ case 'e': {
+ note += 4;
+ break;
+ }
+ case 'f': {
+ note += 5;
+ break;
+ }
+ case 'g': {
+ note += 7;
+ break;
+ }
+ case 'a': {
+ note += 9;
+ break;
+ }
+ case 'b': {
+ note += 11;
+ break;
+ }
+ case 'x': {
+ note = 0;
+ break;
+ }
+ case 'z': {
+ note = 0;
+ break;
+ }
+ default:
+ throw StarException(strf("Unrecognized note %s", head));
+ }
+ if (note != 0) {
+ bool accidentalActive = accidentalSpecified;
+ if (!accidentalSpecified) {
+ if (impliedAccidentals.contains(note)) {
+ accidentals = impliedAccidentals.value(note);
+ accidentalActive = true;
+ }
+ } else {
+ impliedAccidentals[note] = accidentals;
+ }
+ note += accidentals;
+
+ // int base = note;
+
+ if (!accidentalActive) {
+ switch (String::toLower(head)) {
+ case 'c': {
+ note += keySignatureMapping[0];
+ break;
+ }
+ case 'd': {
+ note += keySignatureMapping[1];
+ break;
+ }
+ case 'e': {
+ note += keySignatureMapping[2];
+ break;
+ }
+ case 'f': {
+ note += keySignatureMapping[3];
+ break;
+ }
+ case 'g': {
+ note += keySignatureMapping[4];
+ break;
+ }
+ case 'a': {
+ note += keySignatureMapping[5];
+ break;
+ }
+ case 'b': {
+ note += keySignatureMapping[6];
+ break;
+ }
+ case 'x': {
+ break;
+ }
+ case 'z': {
+ break;
+ }
+ default:
+ throw StarException(strf("Unrecognized note %s", head));
+ }
+ }
+
+ // std::cerr << ":" << note << ":" << (int)accidentalSpecified <<
+ // ":" <<
+ // (int)accidentalActive << ":" << accidentals << ":" << (note-base)
+ // << ":" << head <<
+ // "\n";
+ }
+ accidentals = 0;
+ accidentalSpecified = false;
+ double duration = readDuration();
+ duration *= noteDuration;
+
+ if (tupleCount > 0)
+ duration *= tupleDurationFactor;
+
+ double noteDuration = duration;
+ if (staccato)
+ noteDuration *= 0.5;
+
+ if (peek() == '-') {
+ if (note != 0) {
+ note += transpose();
+ if (pendingTies.contains(note)) {
+ auto& noteInstance = result[pendingTies.get(note)];
+ noteInstance.duration += noteDuration;
+ } else {
+ auto mapping = noteMapping(m_instrument, m_species, note);
+ result.append(Note{
+ m_instrument,
+ Random::randFrom(mapping.files),
+ now,
+ noteDuration,
+ mapping.fadeout,
+ mapping.velocity
+ });
+ pendingTies.add(note, result.size() - 1);
+ }
+ }
+ } else {
+ if (note != 0) {
+ note += transpose();
+ if (pendingTies.contains(note)) {
+ auto& noteInstance = result[pendingTies.take(note)];
+ noteInstance.duration += noteDuration;
+ } else {
+ auto mapping = noteMapping(m_instrument, m_species, note);
+ result.append(Note{
+ m_instrument,
+ Random::randFrom(mapping.files),
+ now,
+ noteDuration,
+ mapping.fadeout,
+ mapping.velocity
+ });
+ }
+ }
+ }
+ if (!grouped) {
+ if (tupleCount > 0)
+ tupleCount--;
+ now += duration;
+ staccato = false;
+ } else if (groupDuration == 0)
+ groupDuration = duration;
+ else
+ groupDuration = std::min(groupDuration, duration);
+ }
+ }
+ }
+ }
+ sortByComputedValue(result, [](Note const& note) { return note.timecode; });
+ return result;
+}
+
+void Songbook::stop() {
+ if (m_stopped)
+ return;
+ m_stopped = true;
+ m_track.clear();
+ m_heldNotes.clear();
+ m_pendingAudio.clear();
+ m_noteMapping.clear();
+ m_uncompressedSamples.clear();
+ m_activeCooldown = 0;
+ m_song = {};
+ m_dataUpdated = true;
+ m_dataChanged = true;
+}
+
+void Songbook::play(Json const& song, String const& timeSource) {
+ stop();
+
+ m_song = song;
+ m_timeSource = timeSource;
+ if (m_timeSource.empty())
+ m_timeSource = toString(Random::randu64());
+
+ {
+ m_timeSourceEpoch = Time::millisecondsSinceEpoch();
+ MutexLocker lock(s_timeSourcesMutex);
+ if (!s_timeSources.contains(m_timeSource)) {
+ m_timeSourceInstance = make_shared<TimeSource>();
+ s_timeSources[m_timeSource] = m_timeSourceInstance;
+ m_timeSourceInstance->epoch = m_timeSourceEpoch;
+ m_timeSourceInstance->keepalive = m_timeSourceEpoch;
+ } else {
+ m_timeSourceInstance = s_timeSources[m_timeSource];
+ if ((Time::millisecondsSinceEpoch() - m_timeSourceInstance->keepalive) > 5000) {
+ m_timeSourceInstance->epoch = m_timeSourceEpoch;
+ m_timeSourceInstance->keepalive = m_timeSourceEpoch;
+ }
+ }
+ m_timeSourceEpoch = m_timeSourceInstance->epoch;
+ }
+
+ m_dataUpdated = true;
+ m_dataChanged = true;
+ m_activeCooldown = 3;
+}
+
+bool Songbook::active() {
+ return m_activeCooldown > 0;
+}
+
+bool Songbook::instrumentPlaying() {
+ if (!active())
+ return false;
+ if (m_timeSourceInstance) {
+ auto now = (Time::millisecondsSinceEpoch() - m_timeSourceInstance->epoch) / 1000.0;
+ for (auto n : m_heldNotes) {
+ if ((n.start <= now) && (now <= n.end))
+ return true;
+ }
+ }
+ return false;
+}
+
+double Songbook::fundamentalFrequency(double p) {
+ return 55.0 * pow(2.0, (p - 69.0) / 12.0 + 3.0);
+}
+
+double Songbook::fundamentalPitch(double f) {
+ return 69.0 + 12 * log2(f / 440.0);
+}
+
+void Songbook::netElementsNeedLoad(bool) {
+ if (m_songNetState.pullUpdated()) {
+ m_song = m_songNetState.get();
+ m_timeSourceEpoch = m_timeSourceEpochNetState.get() - m_globalNowDelta;
+ m_dataUpdated = true;
+ }
+ m_timeSource = m_timeSourceNetState.get();
+ if (m_activeNetState.get())
+ m_activeCooldown = 3;
+ else
+ m_activeCooldown = 0;
+ m_instrument = m_instrumentNetState.get();
+}
+
+void Songbook::netElementsNeedStore() {
+ if (m_serverMode)
+ return;
+ if (m_dataChanged) {
+ m_songNetState.set(m_song);
+ m_timeSourceEpochNetState.set(m_globalNowDelta + m_timeSourceEpoch);
+ m_dataChanged = false;
+ }
+ m_activeNetState.set(active());
+ m_instrumentNetState.set(m_instrument);
+ m_timeSourceNetState.set(m_timeSource);
+}
+
+}