Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
waveform_overview.hpp
Go to the documentation of this file.
1#pragma once
2
3// WaveformOverview + WaveformOverviewCache
4// ─────────────────────────────────────────────────────────────────────────────
5// Pre-decoded min/max peak summaries over an audio file at multiple zoom
6// levels. The peaks live in memory (and optionally on disk) so that a
7// waveform widget can redraw at any zoom without re-touching the source
8// audio.
9//
10// Design:
11// * One WaveformOverview per source. Peaks are stored per channel as
12// interleaved (min, max) int8 samples normalized to [-1, 1]. A single
13// int8-pair costs 2 bytes per channel per peak — tens of thousands of
14// peaks fit in a few KB.
15// * Higher zoom levels are decimations of the level below. We build the
16// base "samples_per_peak" level by walking the audio source once, then
17// decimate by 2x for each subsequent level until we are down to a
18// handful of peaks.
19// * WaveformOverviewCache is a tiny LRU keyed by source path and requested
20// base resolution. Eviction is by configurable maximum entry count.
21// * Optional on-disk persistence — call `set_disk_cache_dir(path)` to
22// point the cache at a directory. After that, `get_or_build()` first
23// consults the disk cache (keyed by SHA-256 of
24// "<source-path>|<mtime>|<samples-per-peak>") and writes a `.thumb`
25// file on every cache insert. Re-opening Pulp finds cached thumbnails
26// instantly without re-decoding the source. Disk cache files are
27// versioned with a magic header so old / corrupt entries are silently
28// ignored.
29//
30// Wired into core/view::WaveformView so that callers may pass either a raw
31// sample buffer (existing behavior) or an WaveformOverview* (new behavior
32// — peaks render straight from the cached table).
33
34#include <atomic>
35#include <cstddef>
36#include <cstdint>
37#include <list>
38#include <memory>
39#include <mutex>
40#include <optional>
41#include <string>
42#include <unordered_map>
43#include <vector>
44
46#include <pulp/audio/buffer.hpp>
47
48namespace pulp::audio {
49
50// A single (min, max) pair at one peak slot for one channel.
51// Stored normalized to [-1, 1]. int8 is plenty for waveform display
52// (256 levels per axis) and keeps peak tables small.
53struct AudioPeak {
54 int8_t min_q7 = 0;
55 int8_t max_q7 = 0;
56
57 float min() const noexcept { return static_cast<float>(min_q7) / 127.0f; }
58 float max() const noexcept { return static_cast<float>(max_q7) / 127.0f; }
59
60 static AudioPeak from_range(float lo, float hi) noexcept {
61 AudioPeak p;
62 if (lo < -1.0f) lo = -1.0f;
63 if (lo > 1.0f) lo = 1.0f;
64 if (hi < -1.0f) hi = -1.0f;
65 if (hi > 1.0f) hi = 1.0f;
66 p.min_q7 = static_cast<int8_t>(lo * 127.0f);
67 p.max_q7 = static_cast<int8_t>(hi * 127.0f);
68 return p;
69 }
70};
71
72// One zoom level: a peak table for every channel.
73// peaks[channel] has `peaks_per_channel` entries.
75 uint32_t samples_per_peak = 0;
76 uint32_t peaks_per_channel = 0;
77 std::vector<std::vector<AudioPeak>> peaks; // [channel][peak_index]
78};
79
80// Lightweight summary of an in-memory thumbnail.
82 uint32_t num_channels = 0;
83 uint64_t num_source_frames = 0;
84 uint32_t sample_rate = 0;
85 std::size_t num_levels = 0;
86 std::size_t bytes_used = 0;
87};
88
90public:
91 // Build from an audio-file path. Returns nullopt if the source file
92 // cannot be opened or has zero frames.
93 //
94 // `samples_per_peak` is the resolution of the base level (level 0).
95 // Subsequent levels are 2x decimations. 256 is a sensible default for
96 // typical UI scrubbing.
97 //
98 // This call is synchronous — the caller decides whether to run it on a
99 // background thread.
100 static std::optional<WaveformOverview> build_from_path(
101 const std::string& path,
102 uint32_t samples_per_peak = 256);
103
104 // Build from an in-memory decoded buffer. Useful for tests and for
105 // sources that are already loaded.
107 const AudioFileData& data,
108 uint32_t samples_per_peak = 256);
109 // Build from an immutable planar buffer view. This is the sampler/editor
110 // bridge for published sample slots and materialized rolling captures.
111 // The thumbnail owns its peak tables; it does not retain the source view.
114 uint32_t sample_rate,
115 uint32_t samples_per_peak = 256);
116
117 WaveformOverviewInfo info() const noexcept;
118
119 const OverviewLevel& level(std::size_t i) const { return levels_.at(i); }
120 std::size_t num_levels() const noexcept { return levels_.size(); }
121
122 // Pick the level whose peaks-per-channel is closest to (and at least
123 // as large as) `target_peaks`. If no level is fine enough, returns the
124 // finest available (level 0). If the thumbnail is empty, returns
125 // index 0; callers must check num_levels().
126 std::size_t best_level_for(uint32_t target_peaks) const noexcept;
127
128 // Produce a flattened, decimated (min, max) pair stream at any target
129 // resolution. Caller passes a destination of size `target_peaks * 2`
130 // where each pair is (min, max) in [-1, 1]. `channel == UINT32_MAX`
131 // means "fold all channels" (max-of-max / min-of-min). Returns the
132 // number of pairs actually written.
133 static constexpr uint32_t kAllChannels = static_cast<uint32_t>(-1);
134 std::size_t render_min_max(uint32_t channel,
135 uint32_t target_peaks,
136 float* dst_min_max_interleaved) const noexcept;
137
138 bool empty() const noexcept { return levels_.empty(); }
139
140 // Internal: rebuild a thumbnail from a serialized blob. Public so the
141 // free-function `deserialize_overview()` (defined in the .cpp) can
142 // construct one without becoming a friend. Returns nullopt on malformed
143 // input. Not part of the supported API surface — call
144 // `deserialize_overview()` instead.
145 static std::optional<WaveformOverview> from_serialized_levels(
146 uint32_t num_channels,
147 uint64_t num_source_frames,
148 uint32_t sample_rate,
149 uint32_t num_levels,
150 const uint8_t* data,
151 std::size_t size,
152 std::size_t offset);
153
154private:
155 std::vector<OverviewLevel> levels_;
156 uint32_t num_channels_ = 0;
157 uint64_t num_source_frames_ = 0;
158 uint32_t sample_rate_ = 0;
159};
160
161// Serialize/deserialize a thumbnail to a versioned blob. The on-disk
162// format is intentionally simple — `magic(4) version(2) num_channels(4)
163// num_source_frames(8) sample_rate(4) num_levels(4) [per-level:
164// samples_per_peak(4) peaks_per_channel(4) channels * peaks * AudioPeak]`
165// — little-endian. The version field is bumped only when the layout
166// changes; `deserialize_overview()` rejects mismatched versions and
167// returns nullopt.
168std::vector<uint8_t> serialize_overview(const WaveformOverview& t);
169std::optional<WaveformOverview> deserialize_overview(const uint8_t* data,
170 std::size_t size);
171
172// ─────────────────────────────────────────────────────────────────────────
173// WaveformOverviewCache — in-memory LRU of thumbnails keyed by source path,
174// with optional on-disk persistence layer.
175//
176// Thread-safe. Used by widgets that may rebuild the same waveform many
177// times (e.g. WaveformView opened repeatedly with the same sample).
178// ─────────────────────────────────────────────────────────────────────────
179
181 std::size_t hits = 0;
182 std::size_t misses = 0;
183 std::size_t evictions = 0;
184 std::size_t entries = 0;
185 std::size_t capacity = 0;
186 std::size_t disk_hits = 0;
187 std::size_t disk_writes = 0;
188};
189
191public:
192 static constexpr uint32_t kInferSamplesPerPeak = 0;
193
194 explicit WaveformOverviewCache(std::size_t max_entries = 32);
195
196 // Returns a shared_ptr to the thumbnail for `path`, building it on
197 // demand. Returns nullptr if the source cannot be opened.
198 std::shared_ptr<const WaveformOverview> get_or_build(
199 const std::string& path,
200 uint32_t samples_per_peak = 256);
201
202 // Returns the cached thumbnail if present, otherwise nullptr. Does
203 // NOT build. Records a hit or miss in stats.
204 std::shared_ptr<const WaveformOverview> get(
205 const std::string& path,
206 uint32_t samples_per_peak = 256);
207
208 // Insert a pre-built thumbnail. Used for tests and for sources that
209 // were built from a buffer rather than a path. The cache key is inferred
210 // from thumbnail.level(0).samples_per_peak when present; the third
211 // argument is only a fallback for thumbnails that do not expose a base
212 // resolution. kInferSamplesPerPeak falls back to the normal 256 default.
213 void put(const std::string& path,
214 std::shared_ptr<const WaveformOverview> thumbnail,
215 uint32_t fallback_samples_per_peak = kInferSamplesPerPeak);
216
217 // Drop everything.
218 void clear();
219
220 // Drop a single entry. Returns true if it was present.
221 bool erase(const std::string& path);
222
223 // Atomic snapshot of cache stats. Cheap.
225
226 std::size_t size() const;
227 std::size_t capacity() const;
228
229 // Resize the cache. If the new capacity is smaller, evicts LRU
230 // entries to fit.
231 void set_capacity(std::size_t max_entries);
232
233 // Point the cache at a directory for on-disk thumbnail persistence.
234 // Pass an empty path to disable disk caching. The directory is
235 // created on demand. Idempotent — safe to call repeatedly.
236 void set_disk_cache_dir(const std::string& dir);
237 std::string disk_cache_dir() const;
238
239 // Best-effort: drop every `.thumb` file under the disk cache dir.
240 // Does nothing if no dir is configured.
242
243private:
244 struct Entry {
245 std::string key;
246 std::shared_ptr<const WaveformOverview> value;
247 };
248
249 // LRU: front == most recently used.
250 using List = std::list<Entry>;
251 using Map = std::unordered_map<std::string, List::iterator>;
252
253 void touch_locked(Map::iterator it);
254 void evict_to_capacity_locked();
255
256 // Disk-cache helpers — return nullptr / false on any I/O failure.
257 // Both are safe to call with disk_dir_ empty; they no-op silently.
258 std::shared_ptr<const WaveformOverview> load_from_disk(const std::string& path,
259 uint32_t samples_per_peak) const;
260 bool write_to_disk(const std::string& path,
261 uint32_t samples_per_peak,
262 const WaveformOverview& thumbnail) const;
263 std::string disk_path_for(const std::string& source_path,
264 uint32_t samples_per_peak) const;
265
266 mutable std::mutex mtx_;
267 List list_;
268 Map index_;
269 std::size_t capacity_;
270 std::string disk_dir_; // empty == disk caching disabled
271
272 // Stats — relaxed atomics; reads can race with writes harmlessly.
273 std::atomic<std::size_t> hits_{0};
274 std::atomic<std::size_t> misses_{0};
275 std::atomic<std::size_t> evictions_{0};
276 std::atomic<std::size_t> disk_hits_{0};
277 std::atomic<std::size_t> disk_writes_{0};
278};
279
280
281
282} // namespace pulp::audio
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
void set_capacity(std::size_t max_entries)
WaveformOverviewCache(std::size_t max_entries=32)
void put(const std::string &path, std::shared_ptr< const WaveformOverview > thumbnail, uint32_t fallback_samples_per_peak=kInferSamplesPerPeak)
std::shared_ptr< const WaveformOverview > get_or_build(const std::string &path, uint32_t samples_per_peak=256)
std::shared_ptr< const WaveformOverview > get(const std::string &path, uint32_t samples_per_peak=256)
bool erase(const std::string &path)
static constexpr uint32_t kInferSamplesPerPeak
Definition waveform_overview.hpp:192
WaveformOverviewCacheStats stats() const
std::string disk_cache_dir() const
void set_disk_cache_dir(const std::string &dir)
Definition waveform_overview.hpp:89
WaveformOverviewInfo info() const noexcept
const OverviewLevel & level(std::size_t i) const
Definition waveform_overview.hpp:119
bool empty() const noexcept
Definition waveform_overview.hpp:138
std::size_t render_min_max(uint32_t channel, uint32_t target_peaks, float *dst_min_max_interleaved) const noexcept
std::size_t best_level_for(uint32_t target_peaks) const noexcept
static WaveformOverview build_from_buffer_view(BufferView< const float > source, uint32_t sample_rate, uint32_t samples_per_peak=256)
static WaveformOverview build_from_buffer(const AudioFileData &data, uint32_t samples_per_peak=256)
std::size_t num_levels() const noexcept
Definition waveform_overview.hpp:120
static std::optional< WaveformOverview > from_serialized_levels(uint32_t num_channels, uint64_t num_source_frames, uint32_t sample_rate, uint32_t num_levels, const uint8_t *data, std::size_t size, std::size_t offset)
static constexpr uint32_t kAllChannels
Definition waveform_overview.hpp:133
static std::optional< WaveformOverview > build_from_path(const std::string &path, uint32_t samples_per_peak=256)
Definition analyzer_provider.hpp:12
std::optional< WaveformOverview > deserialize_overview(const uint8_t *data, std::size_t size)
std::vector< uint8_t > serialize_overview(const WaveformOverview &t)
STL namespace.
Definition audio_file.hpp:24
Definition waveform_overview.hpp:53
static AudioPeak from_range(float lo, float hi) noexcept
Definition waveform_overview.hpp:60
int8_t max_q7
Definition waveform_overview.hpp:55
int8_t min_q7
Definition waveform_overview.hpp:54
float max() const noexcept
Definition waveform_overview.hpp:58
float min() const noexcept
Definition waveform_overview.hpp:57
Definition waveform_overview.hpp:74
std::vector< std::vector< AudioPeak > > peaks
Definition waveform_overview.hpp:77
uint32_t samples_per_peak
Definition waveform_overview.hpp:75
uint32_t peaks_per_channel
Definition waveform_overview.hpp:76
Definition waveform_overview.hpp:180
std::size_t misses
Definition waveform_overview.hpp:182
std::size_t disk_hits
get_or_build → loaded from disk
Definition waveform_overview.hpp:186
std::size_t capacity
Definition waveform_overview.hpp:185
std::size_t hits
Definition waveform_overview.hpp:181
std::size_t evictions
Definition waveform_overview.hpp:183
std::size_t entries
Definition waveform_overview.hpp:184
std::size_t disk_writes
put → wrote to disk
Definition waveform_overview.hpp:187
Definition waveform_overview.hpp:81
uint64_t num_source_frames
Definition waveform_overview.hpp:83
uint32_t num_channels
Definition waveform_overview.hpp:82
std::size_t num_levels
Definition waveform_overview.hpp:85
std::size_t bytes_used
Definition waveform_overview.hpp:86
uint32_t sample_rate
Definition waveform_overview.hpp:84