diff --git a/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp b/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp index 2729ea0f6..c7a5a0edd 100644 --- a/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp +++ b/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp @@ -7,6 +7,8 @@ */ #include "picoTrackerSamplePool.h" +#include "Application/Persistency/PersistencyService.h" +#include "Externals/etl/include/etl/vector.h" #include "hardware/flash.h" #include "hardware/sync.h" #include "pico/multicore.h" @@ -85,6 +87,12 @@ void picoTrackerSamplePool::Reset() { // Reset flash erase and write pointers when we close project flashEraseOffset_ = FLASH_TARGET_OFFSET; flashWriteOffset_ = FLASH_TARGET_OFFSET; + + // Intentionally do NOT delete the sample cache here. Reset() runs before + // every Load (including boot), and the cache is name-keyed — a different + // project will be rejected by LoadSampleCache's project-name check and fall + // back to an SD rebuild which rewrites the cache. Explicit invalidation + // happens in project-deletion and reload paths. }; bool picoTrackerSamplePool::loadSample(const char *name) { @@ -203,6 +211,81 @@ bool picoTrackerSamplePool::LoadInFlash(WavFile *wave) { bool picoTrackerSamplePool::unloadSample(uint32_t index) { return false; }; +bool picoTrackerSamplePool::rebuildSampleFromCache(const SampleCacheEntry &e) { + if (count_ >= MAX_SAMPLES) { + return false; + } + // Bounds-check the flash region against our allocator window. + if (e.flashOffset < FLASH_TARGET_OFFSET || + e.flashOffset + e.sampleBufferSize > flashLimit_) { + Trace::Error("Cache entry '%s' flash range out of bounds", e.name); + return false; + } + short *flashPtr = (short *)(XIP_BASE + e.flashOffset); + wav_[count_].OpenFromFlash(e, flashPtr); + snprintf(nameStore_[count_], sizeof(nameStore_[count_]), "%s", e.name); + count_++; + return true; +} + +void picoTrackerSamplePool::SaveSampleCacheForCurrentPool( + const char *projectName) { + static etl::vector entries; + static etl::vector verify; + entries.clear(); + verify.clear(); + for (uint32_t i = 0; i < count_; ++i) { + SampleCacheEntry e{}; + strncpy(e.name, nameStore_[i], MAX_INSTRUMENT_FILENAME_LENGTH); + e.name[MAX_INSTRUMENT_FILENAME_LENGTH] = '\0'; + short *ptr = wav_[i].GetSamplesPtr(); + e.flashOffset = ptr ? (uint32_t)((uintptr_t)ptr - (uintptr_t)XIP_BASE) : 0u; + e.sampleBufferSize = (uint32_t)wav_[i].GetSampleBufferSize(); + e.size = (uint32_t)wav_[i].GetSize(-1); + e.sampleRate = (uint32_t)wav_[i].GetSampleRate(-1); + e.channelCount = (uint16_t)wav_[i].GetChannelCount(-1); + e.bytePerSample = (uint16_t)wav_[i].GetBytePerSample(); + e.audioFormat = wav_[i].GetAudioFormat(); + entries.push_back(e); + } + + auto *ps = PersistencyService::GetInstance(); + auto res = + ps->SaveSampleCache(projectName, GetSampleCacheBuildId(), entries.data(), + entries.size(), flashEraseOffset_, flashWriteOffset_); + if (res != PERSIST_SAVED) { + Trace::Error("Failed to save sample cache for '%s'", projectName); + return; + } + + // Round-trip verify: read the cache back and sanity-check counts/offsets. + uint32_t eraseOff = 0, writeOff = 0; + auto loadRes = ps->LoadSampleCache(projectName, GetSampleCacheBuildId(), + verify, eraseOff, writeOff); + if (loadRes != PERSIST_LOADED || verify.size() != entries.size() || + eraseOff != flashEraseOffset_ || writeOff != flashWriteOffset_) { + Trace::Error("Sample cache round-trip verify failed (res=%d size=%u/%u)", + (int)loadRes, (unsigned)verify.size(), + (unsigned)entries.size()); + } else { + Trace::Log("SAMPLEPOOL", "Sample cache verified (%u entries)", + (unsigned)verify.size()); + } +} + +// Build-id mixed into the sample cache header. Combines FLASH_TARGET_OFFSET +// (catches firmware-size changes that move the sample region) with a hash of +// the build date/time so any rebuild of this adapter invalidates stale caches. +uint32_t picoTrackerSamplePool::GetSampleCacheBuildId() const { + constexpr const char kBuildStamp[] = __DATE__ " " __TIME__; + uint32_t hash = 2166136261u; // FNV-1a offset basis + for (size_t i = 0; i < sizeof(kBuildStamp) - 1; ++i) { + hash ^= static_cast(kBuildStamp[i]); + hash *= 16777619u; + } + return hash ^ static_cast(FLASH_TARGET_OFFSET); +} + bool picoTrackerSamplePool::CheckSampleFits(int sampleSize) { // Calculate flash storage needed (round up to flash page size) uint32_t flashNeeded = diff --git a/sources/Adapters/picoTracker/system/picoTrackerSamplePool.h b/sources/Adapters/picoTracker/system/picoTrackerSamplePool.h index 154829d77..b42c53b55 100644 --- a/sources/Adapters/picoTracker/system/picoTrackerSamplePool.h +++ b/sources/Adapters/picoTracker/system/picoTrackerSamplePool.h @@ -23,6 +23,21 @@ class picoTrackerSamplePool : public SamplePool { return (flashLimit_ - flashWriteOffset_); } + void SaveSampleCacheForCurrentPool(const char *projectName) override; + bool rebuildSampleFromCache(const SampleCacheEntry &e) override; + void ResumeFromCache(uint32_t flashEraseOffset, + uint32_t flashWriteOffset) override { + flashEraseOffset_ = flashEraseOffset; + flashWriteOffset_ = flashWriteOffset; + Trace::Log("SAMPLEPOOL", "Resumed flash allocator: erase=%u write=%u", + flashEraseOffset_, flashWriteOffset_); + } + + static uint32_t GetFlashEraseOffset() { return flashEraseOffset_; } + static uint32_t GetFlashWriteOffset() { return flashWriteOffset_; } + + uint32_t GetSampleCacheBuildId() const override; + protected: virtual bool loadSample(const char *name); virtual bool unloadSample(uint32_t index); diff --git a/sources/Application/AppWindow.cpp b/sources/Application/AppWindow.cpp index 0dcb35309..b9b7e1ad3 100644 --- a/sources/Application/AppWindow.cpp +++ b/sources/Application/AppWindow.cpp @@ -676,6 +676,7 @@ void AppWindow::AnimationUpdate() { if (awaitingProjectLoadAck_) { if (_mask != 0) { FileSystem::GetInstance()->DeleteFile("/.current"); + PersistencyService::GetInstance()->DeleteSampleCache(); npf_snprintf(projectName_, sizeof(projectName_), "%s", UNNAMED_PROJECT_NAME); loadProject_ = true; diff --git a/sources/Application/Instruments/SamplePool.cpp b/sources/Application/Instruments/SamplePool.cpp index f626a57d1..1f86d3130 100644 --- a/sources/Application/Instruments/SamplePool.cpp +++ b/sources/Application/Instruments/SamplePool.cpp @@ -49,7 +49,49 @@ void SamplePool::updateStatus(uint32_t index, uint32_t total, static_cast(percentage)); }; +bool SamplePool::LoadFromCache(const char *projectName) { + static etl::vector entries; + entries.clear(); + uint32_t eraseOff = 0; + uint32_t writeOff = 0; + auto *ps = PersistencyService::GetInstance(); + auto res = ps->LoadSampleCache(projectName, GetSampleCacheBuildId(), entries, + eraseOff, writeOff); + if (res != PERSIST_LOADED) { + Trace::Log("SAMPLEPOOL", + "No usable sample cache for '%s' (res=%d) — SD load", + projectName, (int)res); + return false; + } + // Monotonicity check: write offset must cover every cached entry. + for (size_t i = 0; i < entries.size(); ++i) { + uint32_t end = entries[i].flashOffset + entries[i].sampleBufferSize; + if (end > writeOff) { + Trace::Error("SAMPLEPOOL: cache entry '%s' exceeds writeOff (%u > %u)", + entries[i].name, end, writeOff); + ps->DeleteSampleCache(); + return false; + } + } + ResumeFromCache(eraseOff, writeOff); + for (size_t i = 0; i < entries.size(); ++i) { + if (!rebuildSampleFromCache(entries[i])) { + Trace::Error("SAMPLEPOOL: failed to rebuild '%s' from cache", + entries[i].name); + Reset(); + ps->DeleteSampleCache(); + return false; + } + } + Trace::Log("SAMPLEPOOL", "Loaded %u samples from cache for '%s'", + (unsigned)entries.size(), projectName); + return true; +} + void SamplePool::Load(const char *projectName) { + if (LoadFromCache(projectName)) { + return; + } auto fs = FileSystem::GetInstance(); if (!fs->chdir(PROJECTS_DIR) || !fs->chdir(projectName) || !fs->chdir(PROJECT_SAMPLES_DIR)) { @@ -104,6 +146,9 @@ void SamplePool::Load(const char *projectName) { swapEntries(index, rest - 1); rest--; }; + + // Write sample cache so that next boot can skip SD reloads. + SaveSampleCacheForCurrentPool(projectName); }; SoundSource *SamplePool::GetSource(uint32_t i) { @@ -355,6 +400,7 @@ int SamplePool::ImportSample(const char *name, const char *projectName) { projSampleFilename.size()); nameStore_[loadedIndex][projSampleFilename.size()] = '\0'; } + SaveSampleCacheForCurrentPool(projectName); } SetChanged(); @@ -387,6 +433,8 @@ void SamplePool::PurgeSample(int i, const char *projectName) { wav_[count_].Close(); nameStore_[count_][0] = '\0'; + SaveSampleCacheForCurrentPool(projectName); + // now notify observers SetChanged(); SamplePoolEvent ev; @@ -399,6 +447,9 @@ void SamplePool::PurgeSample(int i, const char *projectName) { int8_t SamplePool::ReloadSample(uint8_t index, const char *name) { if (unloadSample(index)) { if (loadSample(name)) { + // No projectName available here; invalidate cache so next boot rebuilds + // from SD and rewrites the cache. + PersistencyService::GetInstance()->DeleteSampleCache(); return count_ - 1; } } diff --git a/sources/Application/Instruments/SamplePool.h b/sources/Application/Instruments/SamplePool.h index a39060e70..1f3116839 100644 --- a/sources/Application/Instruments/SamplePool.h +++ b/sources/Application/Instruments/SamplePool.h @@ -44,6 +44,19 @@ class SamplePool : public T_Factory, public Observable { virtual bool unloadSample(uint32_t i) = 0; int8_t ReloadSample(uint8_t index, const char *name); + virtual void SaveSampleCacheForCurrentPool(const char *projectName) {} + virtual bool rebuildSampleFromCache(const SampleCacheEntry &e) { + return false; + } + virtual void ResumeFromCache(uint32_t flashEraseOffset, + uint32_t flashWriteOffset) {} + // Firmware-specific id mixed into the sample-cache header so a firmware + // update whose flash layout has shifted rejects a stale cache. Default 0 + // disables the check on platforms without a meaningful build id. + virtual uint32_t GetSampleCacheBuildId() const { return 0; } + + bool LoadFromCache(const char *projectName); + protected: virtual void updateStatus(uint32_t current, uint32_t total, const char *message); diff --git a/sources/Application/Instruments/WavFile.cpp b/sources/Application/Instruments/WavFile.cpp index 8f5980413..53b045814 100644 --- a/sources/Application/Instruments/WavFile.cpp +++ b/sources/Application/Instruments/WavFile.cpp @@ -169,6 +169,20 @@ etl::expected WavFile::Open(const char *name) { return {}; }; +void WavFile::OpenFromFlash(const SampleCacheEntry &e, short *flashPtr) { + Close(); + samples_ = flashPtr; + sampleBufferSize_ = (int)e.sampleBufferSize; + size_ = (int)e.size; + sampleRate_ = (int)e.sampleRate; + channelCount_ = e.channelCount; + bytePerSample_ = e.bytePerSample; + audioFormat_ = e.audioFormat; + dataPosition_ = 0; + readCount_ = 0; + readBufferSize_ = 0; +} + void *WavFile::GetSampleBuffer(int note) { return samples_; }; void WavFile::SetSampleBuffer(short *ptr) { samples_ = ptr; } diff --git a/sources/Application/Instruments/WavFile.h b/sources/Application/Instruments/WavFile.h index ac52693c2..b01910bc1 100644 --- a/sources/Application/Instruments/WavFile.h +++ b/sources/Application/Instruments/WavFile.h @@ -10,6 +10,7 @@ #ifndef _WAV_FILE_H_ #define _WAV_FILE_H_ +#include "Application/Persistency/PersistencyService.h" #include "Externals/etl/include/etl/expected.h" #include "SoundSource.h" #include "System/FileSystem/FileSystem.h" @@ -29,6 +30,7 @@ class WavFile : public SoundSource { virtual ~WavFile() = default; etl::expected Open(const char *); + void OpenFromFlash(const SampleCacheEntry &e, short *flashPtr); bool IsOpen() const; virtual void *GetSampleBuffer(int note); void SetSampleBuffer(short *ptr); @@ -47,6 +49,11 @@ class WavFile : public SoundSource { virtual bool IsMulti() { return false; }; + short *GetSamplesPtr() const { return samples_; } + int GetSampleBufferSize() const { return sampleBufferSize_; } + int GetBytePerSample() const { return bytePerSample_; } + uint16_t GetAudioFormat() const { return audioFormat_; } + protected: long readBlock(long position, long count); diff --git a/sources/Application/Persistency/PersistencyService.cpp b/sources/Application/Persistency/PersistencyService.cpp index d8a46696e..2cf6280ac 100644 --- a/sources/Application/Persistency/PersistencyService.cpp +++ b/sources/Application/Persistency/PersistencyService.cpp @@ -12,9 +12,11 @@ #include "Foundation/Services/ServiceRegistry.h" #include "Foundation/Types/Types.h" +#include "PersistencyDocument.h" #include "Persistent.h" #include "System/Console/Trace.h" #include "System/FileSystem/FileSystem.h" +#include #include #define PROJECT_STATE_FILE "/.current" @@ -67,6 +69,10 @@ bool PersistencyService::DeleteProject(const char *projectName) { return false; } + // Sample cache is keyed to a single project's flash contents; once the + // project is gone the cached offsets are meaningless. + DeleteSampleCache(); + return true; } @@ -427,6 +433,159 @@ PersistencyResult PersistencyService::ExportInstrument( return PERSIST_SAVED; } +PersistencyResult PersistencyService::SaveSampleCache( + const char *projectName, uint32_t buildId, const SampleCacheEntry *entries, + size_t count, uint32_t flashEraseOffset, uint32_t flashWriteOffset) { + auto fs = FileSystem::GetInstance(); + auto fp = fs->Open(PROJECT_SAMPLES_CACHE_FILE, "w"); + if (!fp) { + Trace::Error("PERSISTENCYSERVICE: Could not open sample cache for write"); + return PERSIST_ERROR; + } + { + tinyxml2::XMLPrinter printer(fp.get()); + printer.OpenElement("SAMPLECACHE"); + printer.PushAttribute("MAGIC", (int64_t)PROJECT_SAMPLES_CACHE_MAGIC); + printer.PushAttribute("VERSION", PROJECT_SAMPLES_CACHE_VERSION); + printer.PushAttribute("PROJECT", projectName); + printer.PushAttribute("BUILDID", (int64_t)buildId); + printer.PushAttribute("ERASEOFF", (int64_t)flashEraseOffset); + printer.PushAttribute("WRITEOFF", (int64_t)flashWriteOffset); + printer.PushAttribute("COUNT", (int64_t)count); + for (size_t i = 0; i < count; ++i) { + const SampleCacheEntry &e = entries[i]; + printer.OpenElement("SAMPLE"); + printer.PushAttribute("NAME", e.name); + printer.PushAttribute("FLASHOFF", (int64_t)e.flashOffset); + printer.PushAttribute("BUFSIZE", (int64_t)e.sampleBufferSize); + printer.PushAttribute("SIZE", (int64_t)e.size); + printer.PushAttribute("RATE", (int64_t)e.sampleRate); + printer.PushAttribute("CHANS", e.channelCount); + printer.PushAttribute("BPS", e.bytePerSample); + printer.PushAttribute("FMT", e.audioFormat); + printer.CloseElement(); + } + printer.CloseElement(); + } + // Ensure data + directory entry land on the SD card before power loss. + // Without this, the cache file is often absent after a cold reboot. + fp->Sync(); + fp.reset(); + Trace::Log("PERSISTENCYSERVICE", + "Wrote sample cache for '%s' (%u entries, erase=%u write=%u)", + projectName, (unsigned)count, flashEraseOffset, flashWriteOffset); + return PERSIST_SAVED; +} + +PersistencyResult PersistencyService::LoadSampleCache( + const char *expectedProjectName, uint32_t expectedBuildId, + etl::ivector &entries, uint32_t &flashEraseOffset, + uint32_t &flashWriteOffset) { + entries.clear(); + auto fs = FileSystem::GetInstance(); + if (!fs->exists(PROJECT_SAMPLES_CACHE_FILE)) { + return PERSIST_LOAD_FAILED; + } + PersistencyDocument doc; + if (!doc.Load(PROJECT_SAMPLES_CACHE_FILE)) { + return PERSIST_LOAD_FAILED; + } + if (!doc.FirstChild() || strcmp(doc.ElemName(), "SAMPLECACHE")) { + Trace::Error("PERSISTENCYSERVICE: sample cache missing root"); + return PERSIST_LOAD_FAILED; + } + + uint32_t magic = 0; + uint32_t version = 0; + uint32_t buildId = 0; + char projectInFile[MAX_PROJECT_NAME_LENGTH + 1] = {0}; + flashEraseOffset = 0; + flashWriteOffset = 0; + + bool hasAttr = doc.NextAttribute(); + while (hasAttr) { + if (!strcasecmp(doc.attrname_, "MAGIC")) { + magic = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "VERSION")) { + version = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "PROJECT")) { + snprintf(projectInFile, sizeof(projectInFile), "%s", doc.attrval_); + } else if (!strcasecmp(doc.attrname_, "BUILDID")) { + buildId = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "ERASEOFF")) { + flashEraseOffset = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "WRITEOFF")) { + flashWriteOffset = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } + hasAttr = doc.NextAttribute(); + } + + if (magic != PROJECT_SAMPLES_CACHE_MAGIC || + version != PROJECT_SAMPLES_CACHE_VERSION) { + Trace::Error("PERSISTENCYSERVICE: sample cache magic/version mismatch"); + return PERSIST_LOAD_FAILED; + } + if (buildId != expectedBuildId) { + Trace::Log("PERSISTENCYSERVICE", + "sample cache build-id mismatch: file=0x%08x expected=0x%08x", + buildId, expectedBuildId); + return PERSIST_LOAD_FAILED; + } + if (strcmp(projectInFile, expectedProjectName) != 0) { + Trace::Log("PERSISTENCYSERVICE", + "sample cache project mismatch: file='%s' expected='%s'", + projectInFile, expectedProjectName); + return PERSIST_LOAD_FAILED; + } + + bool hasChild = doc.FirstChild(); + while (hasChild) { + if (!strcmp(doc.ElemName(), "SAMPLE")) { + if (entries.full()) { + Trace::Error("PERSISTENCYSERVICE: sample cache has too many entries"); + return PERSIST_LOAD_FAILED; + } + SampleCacheEntry e{}; + bool a = doc.NextAttribute(); + while (a) { + if (!strcasecmp(doc.attrname_, "NAME")) { + snprintf(e.name, sizeof(e.name), "%s", doc.attrval_); + } else if (!strcasecmp(doc.attrname_, "FLASHOFF")) { + e.flashOffset = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "BUFSIZE")) { + e.sampleBufferSize = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "SIZE")) { + e.size = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "RATE")) { + e.sampleRate = (uint32_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "CHANS")) { + e.channelCount = (uint16_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "BPS")) { + e.bytePerSample = (uint16_t)strtoul(doc.attrval_, nullptr, 10); + } else if (!strcasecmp(doc.attrname_, "FMT")) { + e.audioFormat = (uint16_t)strtoul(doc.attrval_, nullptr, 10); + } + a = doc.NextAttribute(); + } + entries.push_back(e); + } + hasChild = doc.NextSibling(); + } + if (doc.HadError()) { + Trace::Error("PERSISTENCYSERVICE: XML error parsing sample cache"); + return PERSIST_LOAD_FAILED; + } + return PERSIST_LOADED; +} + +bool PersistencyService::DeleteSampleCache() { + auto fs = FileSystem::GetInstance(); + if (!fs->exists(PROJECT_SAMPLES_CACHE_FILE)) { + return true; + } + return fs->DeleteFile(PROJECT_SAMPLES_CACHE_FILE); +} + InstrumentType PersistencyService::DetectInstrumentType(const char *name) { auto fs = FileSystem::GetInstance(); diff --git a/sources/Application/Persistency/PersistencyService.h b/sources/Application/Persistency/PersistencyService.h index 270f30a75..63f9506c2 100644 --- a/sources/Application/Persistency/PersistencyService.h +++ b/sources/Application/Persistency/PersistencyService.h @@ -13,11 +13,28 @@ #include "Application/Instruments/I_Instrument.h" #include "Externals/TinyXML2/tinyxml2.h" #include "Externals/etl/include/etl/string.h" +#include "Externals/etl/include/etl/vector.h" #include "Externals/yxml/yxml.h" #include "Foundation/Services/Service.h" #include "Foundation/T_Singleton.h" #include "PersistenceConstants.h" +#include + +#define PROJECT_SAMPLES_CACHE_FILE "/.current.samples" +#define PROJECT_SAMPLES_CACHE_MAGIC 0x50545343u // 'PTSC' +#define PROJECT_SAMPLES_CACHE_VERSION 1 + +struct SampleCacheEntry { + char name[MAX_INSTRUMENT_FILENAME_LENGTH + 1]; + uint32_t flashOffset; + uint32_t sampleBufferSize; + uint32_t size; + uint32_t sampleRate; + uint16_t channelCount; + uint16_t bytePerSample; + uint16_t audioFormat; +}; enum PersistencyResult { PERSIST_SAVED, @@ -55,6 +72,17 @@ class PersistencyService : public Service, const char *name); InstrumentType DetectInstrumentType(const char *name); + PersistencyResult SaveSampleCache(const char *projectName, uint32_t buildId, + const SampleCacheEntry *entries, + size_t count, uint32_t flashEraseOffset, + uint32_t flashWriteOffset); + PersistencyResult LoadSampleCache(const char *expectedProjectName, + uint32_t expectedBuildId, + etl::ivector &entries, + uint32_t &flashEraseOffset, + uint32_t &flashWriteOffset); + bool DeleteSampleCache(); + private: PersistencyResult CreateProjectDirs_(const char *projectName); void CreatePath(etl::istring &path,