#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> Songbook::s_timeSources; Songbook::Songbook(String const& species) { m_activeCooldown = 0; m_dataUpdated = false; m_dataChanged = false; m_timeSourceEpoch = 0; m_epochUpdated = false; 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 notemap; auto tuning = Root::singleton().assets()->json(strf("/sfx/instruments/{}/tuning.config", instrument)); for (auto e : tuning.get("mapping").iterateObject()) { int keyNumber = lexicalCast(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_epochUpdated) { m_epochUpdated = false; m_timeSourceEpoch -= m_globalNowDelta; } 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(); 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: {}", 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