Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<SampleCacheEntry, MAX_SAMPLES> entries;
static etl::vector<SampleCacheEntry, MAX_SAMPLES> 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<uint8_t>(kBuildStamp[i]);
hash *= 16777619u;
}
return hash ^ static_cast<uint32_t>(FLASH_TARGET_OFFSET);
}

bool picoTrackerSamplePool::CheckSampleFits(int sampleSize) {
// Calculate flash storage needed (round up to flash page size)
uint32_t flashNeeded =
Expand Down
15 changes: 15 additions & 0 deletions sources/Adapters/picoTracker/system/picoTrackerSamplePool.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions sources/Application/AppWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 51 additions & 0 deletions sources/Application/Instruments/SamplePool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,49 @@ void SamplePool::updateStatus(uint32_t index, uint32_t total,
static_cast<int>(percentage));
};

bool SamplePool::LoadFromCache(const char *projectName) {
static etl::vector<SampleCacheEntry, MAX_SAMPLES> 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)) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -355,6 +400,7 @@ int SamplePool::ImportSample(const char *name, const char *projectName) {
projSampleFilename.size());
nameStore_[loadedIndex][projSampleFilename.size()] = '\0';
}
SaveSampleCacheForCurrentPool(projectName);
}

SetChanged();
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand Down
13 changes: 13 additions & 0 deletions sources/Application/Instruments/SamplePool.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ class SamplePool : public T_Factory<SamplePool>, 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);
Expand Down
14 changes: 14 additions & 0 deletions sources/Application/Instruments/WavFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ etl::expected<void, WAVEFILE_ERROR> 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; }
Expand Down
7 changes: 7 additions & 0 deletions sources/Application/Instruments/WavFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,6 +30,7 @@ class WavFile : public SoundSource {
virtual ~WavFile() = default;

etl::expected<void, WAVEFILE_ERROR> Open(const char *);
void OpenFromFlash(const SampleCacheEntry &e, short *flashPtr);
bool IsOpen() const;
virtual void *GetSampleBuffer(int note);
void SetSampleBuffer(short *ptr);
Expand All @@ -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);

Expand Down
Loading
Loading