Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulpset.hpp
Go to the documentation of this file.
1#pragma once
2
20
22#include <pulp/format/detail/locale_independent_float.hpp>
23#include <pulp/audio/buffer.hpp>
24#include <pulp/midi/buffer.hpp>
25#include <pulp/midi/message.hpp>
26
27#include <algorithm>
28#include <charconv>
29#include <cmath>
30#include <cstdint>
31#include <fstream>
32#include <sstream>
33#include <string>
34#include <string_view>
35#include <system_error>
36#include <type_traits>
37#include <vector>
38
39namespace pulp::format {
40
43 enum class Kind { Param, NoteOn, NoteOff };
44 std::int64_t sample = 0;
46 std::uint32_t id = 0;
47 float value = 0.0f;
48};
49
51struct Pulpset {
52 std::vector<PulpsetEvent> events;
53
54 static Pulpset parse(const std::string& text) {
55 Pulpset ps;
56 std::istringstream in(text);
57 std::string line;
58 while (std::getline(in, line)) {
59 auto hash = line.find('#');
60 if (hash != std::string::npos) line = line.substr(0, hash);
61
62 // Whitespace-delimited tokenizer over the line. Tokens are parsed
63 // with std::from_chars so numbers (including the fractional value)
64 // are locale-independent: a comma-decimal global locale can never
65 // make a `.pulpset` round-trip "0.5" as "0,5". (The previous
66 // std::istringstream >> path honored the stream's global locale.)
67 std::string_view rest{line};
68 auto next_token = [&rest]() -> std::string_view {
69 std::size_t b = rest.find_first_not_of(" \t\r");
70 if (b == std::string_view::npos) { rest = {}; return {}; }
71 std::size_t e = rest.find_first_of(" \t\r", b);
72 std::string_view tok = (e == std::string_view::npos)
73 ? rest.substr(b)
74 : rest.substr(b, e - b);
75 rest = (e == std::string_view::npos) ? std::string_view{}
76 : rest.substr(e);
77 return tok;
78 };
79
80 std::string_view sample_tok = next_token();
81 std::string_view op = next_token();
82 if (sample_tok.empty() || op.empty()) continue;
83
84 // A required numeric field must parse AND fully consume its token.
85 // Full-token consumption is what rejects "0,5" (parses just the "0"
86 // prefix) and "123abc" — a comma-decimal legacy file is rejected
87 // cleanly rather than silently replaying 0.5 as 0.0.
88 auto parse_full = [](std::string_view tok, auto& out) {
89 if (tok.empty()) return false;
90 using T = std::remove_reference_t<decltype(out)>;
91 if constexpr (std::is_floating_point_v<T>) {
92 // std::from_chars' float overload is =deleted on some
93 // toolchains (see locale_independent_float.hpp),
94 // so parse the fractional value via a C-locale strtod and
95 // require the whole token to be consumed — the same
96 // "0,5"/"0.5foo" rejection the integer path gives.
97 double parsed = 0.0;
98 const auto r = detail::parse_double_c_locale(tok, parsed);
99 if (r.consumed != tok.size() || r.range_error) return false;
100 out = static_cast<T>(parsed);
101 return true;
102 } else {
103 const char* first = tok.data();
104 const char* last = first + tok.size();
105 auto r = std::from_chars(first, last, out);
106 return r.ec == std::errc{} && r.ptr == last;
107 }
108 };
109
110 PulpsetEvent e;
111 if (!parse_full(sample_tok, e.sample)) continue;
112
113 if (op == "param") {
115 // id and value are required for a param steer.
116 if (!parse_full(next_token(), e.id)) continue;
117 if (!parse_full(next_token(), e.value)) continue;
118 } else if (op == "note_on") {
120 e.value = 100.0f; // default velocity
121 if (!parse_full(next_token(), e.id)) continue;
122 // Velocity is optional; if a token is present it must be valid.
123 std::string_view vel = next_token();
124 if (!vel.empty() && !parse_full(vel, e.value)) continue;
125 } else if (op == "note_off") {
127 if (!parse_full(next_token(), e.id)) continue;
128 } else {
129 continue;
130 }
131 ps.events.push_back(e);
132 }
133 std::stable_sort(ps.events.begin(), ps.events.end(),
134 [](const PulpsetEvent& a, const PulpsetEvent& b) { return a.sample < b.sample; });
135 return ps;
136 }
137
140 std::string to_text() const {
141 std::string out;
142 char buf[64];
143 auto append_num = [&](auto v) {
144 auto r = std::to_chars(buf, buf + sizeof(buf), v);
145 out.append(buf, r.ptr);
146 };
147 for (const auto& e : events) {
148 append_num(e.sample);
149 switch (e.kind) {
151 out += " param ";
152 append_num(e.id);
153 out += ' ';
154 append_num(e.value);
155 break;
157 out += " note_on ";
158 append_num(e.id);
159 out += ' ';
160 append_num(e.value);
161 break;
163 out += " note_off ";
164 append_num(e.id);
165 break;
166 }
167 out += '\n';
168 }
169 return out;
170 }
171
172 static Pulpset from_file(const std::string& path) {
173 std::ifstream f(path);
174 std::stringstream ss; ss << f.rdbuf();
175 return parse(ss.str());
176 }
177
178 std::int64_t last_sample() const { return events.empty() ? 0 : events.back().sample; }
179};
180
183 std::vector<float> left, right;
184 std::size_t frames() const { return left.size(); }
185};
186
191 std::size_t total_samples, std::size_t block_size = 512) {
192 PulpsetRender out;
193 out.left.resize(total_samples, 0.0f);
194 out.right.resize(total_samples, 0.0f);
195
196 std::vector<float> obuf((std::size_t)2 * block_size);
197 const float* in_planes[2] = {nullptr, nullptr};
198 audio::BufferView<const float> in_view(in_planes, 0, 0);
199
200 std::size_t ev = 0;
201 for (std::size_t s = 0; s < total_samples; s += block_size) {
202 const std::size_t n = std::min(block_size, total_samples - s);
203 float* planes[2] = {obuf.data(), obuf.data() + n};
204 audio::BufferView<float> out_view(planes, 2, n);
205
206 midi::MidiBuffer midi_in, midi_out;
207 // Apply all events whose sample falls in [s, s+n).
208 for (; ev < ps.events.size() && ps.events[ev].sample < (std::int64_t)(s + n); ++ev) {
209 const auto& e = ps.events[ev];
210 if (e.sample < (std::int64_t)s) { /* past-due: still apply now */ }
211 switch (e.kind) {
213 host.state().set_value(e.id, e.value);
214 break;
216 auto me = midi::MidiEvent::note_on(0, (std::uint8_t)e.id, (std::uint8_t)e.value);
217 me.sample_offset = (std::int32_t)std::max<std::int64_t>(0, e.sample - (std::int64_t)s);
218 midi_in.add(me);
219 break;
220 }
222 auto me = midi::MidiEvent::note_off(0, (std::uint8_t)e.id);
223 me.sample_offset = (std::int32_t)std::max<std::int64_t>(0, e.sample - (std::int64_t)s);
224 midi_in.add(me);
225 break;
226 }
227 }
228 }
229 midi_in.sort();
230 host.process(out_view, in_view, midi_in, midi_out);
231
232 std::copy(planes[0], planes[0] + n, out.left.begin() + s);
233 std::copy(planes[1], planes[1] + n, out.right.begin() + s);
234 }
235 return out;
236}
237
239inline double segment_rms(const std::vector<float>& v, std::size_t a, std::size_t b) {
240 double acc = 0; std::size_t n = 0;
241 for (std::size_t i = a; i < b && i < v.size(); ++i) { acc += (double)v[i] * v[i]; ++n; }
242 return n ? std::sqrt(acc / n) : 0.0;
243}
244
246inline bool write_wav_f32(const std::string& path, const PulpsetRender& r, std::uint32_t sample_rate = 48000) {
247 std::ofstream o(path, std::ios::binary);
248 if (!o) return false;
249 const std::uint32_t ch = 2, bits = 32, n = (std::uint32_t)r.frames();
250 const std::uint32_t data_bytes = n * ch * (bits / 8);
251 auto u32 = [&](std::uint32_t v) { o.write((const char*)&v, 4); };
252 auto u16 = [&](std::uint16_t v) { o.write((const char*)&v, 2); };
253 o.write("RIFF", 4); u32(36 + data_bytes); o.write("WAVE", 4);
254 o.write("fmt ", 4); u32(16); u16(3); u16((std::uint16_t)ch); u32(sample_rate);
255 u32(sample_rate * ch * (bits / 8)); u16((std::uint16_t)(ch * (bits / 8))); u16((std::uint16_t)bits);
256 o.write("data", 4); u32(data_bytes);
257 for (std::uint32_t i = 0; i < n; ++i) { o.write((const char*)&r.left[i], 4); o.write((const char*)&r.right[i], 4); }
258 return (bool)o;
259}
260
261} // namespace pulp::format
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Headless plugin host for testing and offline processing.
Definition headless.hpp:29
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
void sort()
Sort events by sample_offset for sample-accurate processing.
Definition buffer.hpp:177
bool add(const MidiEvent &event)
Append a MIDI event to the buffer.
Definition buffer.hpp:98
Headless processing adapter for programmatic plugin use without UI or audio device.
What the product grants a device: the quota table indexed by capability tier and live thermal state.
Definition aax_editor.hpp:31
PulpsetRender render(HeadlessHost &host, const Pulpset &ps, std::size_t total_samples, std::size_t block_size=512)
Replay ps over host for total_samples, in block_size blocks.
Definition pulpset.hpp:190
double segment_rms(const std::vector< float > &v, std::size_t a, std::size_t b)
RMS of [a,b) of one channel — for range/property assertions.
Definition pulpset.hpp:239
bool write_wav_f32(const std::string &path, const PulpsetRender &r, std::uint32_t sample_rate=48000)
Write 32-bit float stereo WAV (offline render artifact).
Definition pulpset.hpp:246
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
One timed steer in a .pulpset.
Definition pulpset.hpp:42
float value
param value (Param) or velocity (NoteOn)
Definition pulpset.hpp:47
Kind
Definition pulpset.hpp:43
@ NoteOn
Definition pulpset.hpp:43
@ NoteOff
Definition pulpset.hpp:43
@ Param
Definition pulpset.hpp:43
std::int64_t sample
absolute sample offset
Definition pulpset.hpp:44
Kind kind
Definition pulpset.hpp:45
Rendered stereo result of a replay.
Definition pulpset.hpp:182
std::vector< float > right
Definition pulpset.hpp:183
std::size_t frames() const
Definition pulpset.hpp:184
std::vector< float > left
Definition pulpset.hpp:183
A parsed .pulpset — events sorted by sample offset.
Definition pulpset.hpp:51
std::vector< PulpsetEvent > events
Definition pulpset.hpp:52
static Pulpset parse(const std::string &text)
Definition pulpset.hpp:54
std::string to_text() const
Serialize events back to .pulpset text.
Definition pulpset.hpp:140
std::int64_t last_sample() const
Definition pulpset.hpp:178
static Pulpset from_file(const std::string &path)
Definition pulpset.hpp:172
static MidiEvent note_on(uint8_t channel, uint8_t note, uint8_t velocity)
Create a Note On event.
Definition message.hpp:46
static MidiEvent note_off(uint8_t channel, uint8_t note, uint8_t velocity=0)
Create a Note Off event.
Definition message.hpp:54