Stack L/R waveform lanes in stereo mode, with overlays drawn once at full band height

This commit is contained in:
2026-07-30 09:04:57 -04:00
parent 81f5861ba4
commit 2a0d10fab5
9 changed files with 341 additions and 77 deletions
+53 -31
View File
@@ -48,6 +48,8 @@ void ReaSamplerEditor::refreshFromBank() {
// Main/UI thread only — reads the live bank over the bridge (allocates, calls REAPER).
thumbCache_.clear(); // a bank edit may have re-captured/removed a sample; drop stale peaks
pcmCache_.clear(); // and its decoded PCM (the waveform + snap source)
channelPcmId_.clear();
channelPcm_ = ChannelPcm{};
if (!processor_) {
samples_.clear();
banks_.clear();
@@ -202,42 +204,62 @@ int ReaSamplerEditor::effectiveRoot() const {
return 60;
}
std::string ReaSamplerEditor::samplePathFor(const std::string& sampleId) const {
// SampleChoice is the browser's metadata projection and does not carry the WAV path, so
// resolve it from the live bank blob (selectSample).
if (!processor_) return {};
auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
if (banksJson) {
if (auto sel = selectSample(*banksJson, sampleId)) return sel->relativePath;
}
// Fallback: the bank blob is not readable (extension absent / not yet parsed) or the id
// went stale there — the instance-owned ref still carries the path, so a self-contained
// instance draws its loaded sound's waveform regardless.
const SampleRefs refs = processor_->sampleRefs();
if (const SelectedSample* r = findRef(refs, sampleId)) return r->relativePath;
return {};
}
const ReaSamplerEditor::ChannelPcm& ReaSamplerEditor::channelPcmFor(
const std::string& sampleId) {
if (channelPcmId_ == sampleId && !sampleId.empty()) return channelPcm_;
// A failed decode is still cached (channelCount stays 0) so a broken/missing file is not
// re-read on every paint.
channelPcmId_ = sampleId;
channelPcm_ = ChannelPcm{};
const std::string relativePath = samplePathFor(sampleId);
if (relativePath.empty()) return channelPcm_;
const std::string projectDir = processor_->bridge().activeProjectDir();
const std::vector<std::uint8_t> bytes =
readFileBytes(resolveBankFile(projectDir, relativePath)); // empty on any failure
const WavLayout layout = parseWavLayout(bytes);
if (layout.valid) {
channelPcm_.interleaved = extractFloatFrames(bytes, layout, 0, layout.frameCount());
channelPcm_.channelCount = static_cast<int>(layout.channelCount);
}
return channelPcm_;
}
const std::vector<AudioSample>& ReaSamplerEditor::monoPcmFor(const std::string& sampleId) {
auto it = pcmCache_.find(sampleId);
if (it != pcmCache_.end()) return it->second;
// SampleChoice is the browser's metadata projection and does not carry the WAV path, so
// resolve the path from the live bank blob (selectSample) and decode via the shared WAV
// parse. Every failure path caches an empty vector so a broken/missing file is not
// re-decoded on every paint. Keyed by id (width-independent) — the thumbnail bins this at
// whatever width, the snap scans it directly.
std::string relativePath;
// Every failure path caches an empty vector so a broken/missing file is not re-decoded on
// every paint. Keyed by id (width-independent) — the thumbnail bins this at whatever
// width, the snap scans it directly.
std::vector<AudioSample> mono;
if (processor_) {
auto banksJson =
processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
if (banksJson) {
if (auto sel = selectSample(*banksJson, sampleId)) relativePath = sel->relativePath;
}
if (relativePath.empty()) {
// Fallback: the bank blob is not readable (extension absent / not yet parsed) or
// the id went stale there — the instance-owned ref still carries the path, so a
// self-contained instance draws its loaded sound's waveform regardless.
const SampleRefs refs = processor_->sampleRefs();
if (const SelectedSample* r = findRef(refs, sampleId)) {
relativePath = r->relativePath;
}
}
if (!relativePath.empty()) {
const std::string projectDir = processor_->bridge().activeProjectDir();
const std::string abs = resolveBankFile(projectDir, relativePath);
const std::vector<std::uint8_t> bytes = readFileBytes(abs); // empty on any failure
const WavLayout layout = parseWavLayout(bytes);
if (layout.valid) {
std::vector<AudioSample> interleaved =
extractFloatFrames(bytes, layout, 0, layout.frameCount());
mono = downmixToMono(interleaved, layout.channelCount);
}
const std::string relativePath = samplePathFor(sampleId);
if (!relativePath.empty()) {
const std::string projectDir = processor_->bridge().activeProjectDir();
const std::string abs = resolveBankFile(projectDir, relativePath);
const std::vector<std::uint8_t> bytes = readFileBytes(abs); // empty on any failure
const WavLayout layout = parseWavLayout(bytes);
if (layout.valid) {
std::vector<AudioSample> interleaved =
extractFloatFrames(bytes, layout, 0, layout.frameCount());
mono = downmixToMono(interleaved, layout.channelCount);
}
}
auto ins = pcmCache_.emplace(sampleId, std::move(mono));