11 constexpr double k_continuous_ceiling = 4.0;
16 double rail_peak(
const WaveguideNetwork::WaveguideSegment& seg)
19 for (
size_t i = 0; i < seg.p_plus.capacity(); ++i) {
20 level = std::max({ level, std::abs(seg.p_plus[i]), std::abs(seg.p_minus[i]) });
37 explicit DriveLimiter(
size_t interval)
42 double gain(
const WaveguideNetwork::WaveguideSegment& seg)
45 const double knee = 0.5 * k_continuous_ceiling;
46 const double over = rail_peak(seg) - knee;
47 m_gain = over <= 0.0 ? 1.0 : std::max(0.0, 1.0 - over / knee);
68 double fundamental_freq,
71 , m_fundamental(fundamental_freq)
103 const auto len = seg.p_plus.capacity();
107 seg.loop_filter->reset();
108 if (seg.loop_filter_open)
109 seg.loop_filter_open->reset();
110 if (seg.loop_filter_closed)
111 seg.loop_filter_closed->reset();
147 auto filter = std::make_shared<Filters::FIR>(
148 std::vector<double> { 0.5, 0.5 });
162 double fraction)
const
164 double s0 = delay[integer_part];
165 double s1 = delay[integer_part + 1];
166 return s0 + fraction * (s1 - s0);
194 std::this_thread::yield();
204 thread_local std::vector<double> scratch;
205 scratch.assign(num_samples, 0.0);
221 std::this_thread::yield();
230 unsigned int num_samples, std::vector<double>&
out)
234 const double continuous_gain = continuous
239 for (
unsigned int i = 0; i < num_samples; ++i) {
245 const double filtered = (!continuous && seg.
loop_filter)
250 const double drive = continuous ? exciter * limiter.gain(seg) : exciter;
259 unsigned int num_samples, std::vector<double>&
out)
263 const double continuous_gain = continuous
268 for (
unsigned int i = 0; i < num_samples; ++i) {
282 const double filtered_plus = (!continuous && filt_open)
283 ? filt_open->process_sample(plus_end)
285 const double filtered_minus = (!continuous && filt_closed)
286 ? filt_closed->process_sample(minus_end)
290 const double drive = continuous ? exciter * limiter.gain(seg) : exciter;
315 const auto pluck_sample = std::clamp(
316 static_cast<size_t>(
position *
static_cast<double>(len)),
size_t { 1 }, len - 1);
318 ? std::max(strength, 1.0)
324 std::vector<double> shape(len);
325 for (
size_t s = 0; s < len; ++s) {
326 if (s <= pluck_sample) {
327 shape[s] = effective_strength *
static_cast<double>(s)
328 /
static_cast<double>(pluck_sample);
330 shape[s] = effective_strength *
static_cast<double>(len - s)
331 /
static_cast<double>(len - pluck_sample);
337 offset = std::accumulate(shape.begin(), shape.end(), 0.0) /
static_cast<double>(len);
340 for (
const double value : shape) {
365 const auto strike_center =
static_cast<size_t>(
position *
static_cast<double>(len));
366 const size_t burst_width = std::max<size_t>(len / 10, 4);
368 ? std::max(strength, 1.0)
374 for (
size_t s = 0; s < len; ++s) {
375 const double dist = std::abs(
static_cast<double>(s)
376 -
static_cast<double>(strike_center));
377 const double window = std::exp(-(dist * dist)
378 / (2.0 *
static_cast<double>(burst_width * burst_width)));
393 ? std::numeric_limits<size_t>::max()
492 if (seg.p_plus.capacity() < required) {
501 loss = std::clamp(loss, 0.0, 1.0);
503 seg.loss_factor = loss;
556 double value = mapping.broadcast_source->get_last_output();
566 if (param ==
"frequency") {
568 }
else if (param ==
"damping" || param ==
"loss") {
570 }
else if (param ==
"position") {
572 }
else if (param ==
"scale") {
579 const std::shared_ptr<NodeNetwork>& )
584 const std::string& param_name,
585 const std::shared_ptr<Node>& source,
600 const std::string& param_name,
601 const std::shared_ptr<NodeNetwork>& source_network)
617 [&](
const auto& m) {
return m.param_name == param_name; });
624std::unordered_map<std::string, std::string>
std::optional< glm::vec3 > position
std::weak_ptr< Core::Window > window
std::unordered_map< std::string, std::string > metadata
std::vector< float > * out
vk::PhysicalDeviceType type
void push(const T &value)
Push new value to front of history.
History buffer for difference equations and recursive relations.
std::vector< ParameterMapping > m_parameter_mappings
void apply_output_scale()
Apply m_output_scale to m_last_audio_buffer.
virtual void set_topology(Topology topology)
Set the network's topology.
bool is_enabled() const
Check if network is enabled.
double m_output_scale
Post-processing scalar applied to m_last_audio_buffer each batch.
std::atomic_flag m_audio_buffer_lock
Spinlock guarding m_last_audio_buffer.
void ensure_initialized()
Ensure initialize() is called exactly once.
virtual std::unordered_map< std::string, std::string > get_metadata() const
Get network metadata for debugging/visualization.
std::vector< double > m_last_audio_buffer
void set_output_mode(OutputMode mode)
Set the network's output routing mode.
size_t m_exciter_samples_remaining
Kinesis::Stochastic::Stochastic m_random_generator
std::shared_ptr< Node > m_exciter_node
void set_loop_filter_closed(const std::shared_ptr< Filters::Filter > &filter)
Set filter for the closed-end termination (mouthpiece/nut)
void set_exciter_type(ExciterType type)
Set exciter type.
void set_loop_filter_open(const std::shared_ptr< Filters::Filter > &filter)
Set filter for the open-end termination (bell/bridge)
void strike(double position=0.5, double strength=1.0)
Strike the string/tube with an impulse.
std::unordered_map< std::string, std::string > get_metadata() const override
Get network metadata for debugging/visualization.
MeasurementMode m_measurement_mode
void process_unidirectional(WaveguideSegment &seg, unsigned int num_samples, std::vector< double > &out)
void update_mapped_parameters()
size_t m_exciter_node_buffer_pos
WaveguideType
Physical structure being modeled.
@ TUBE
Cylindrical bore (future: clarinet, flute)
void process_bidirectional(WaveguideSegment &seg, unsigned int num_samples, std::vector< double > &out)
void process_batch(unsigned int num_samples) override
Process the network for the given number of samples.
ExciterType m_exciter_type
void create_default_loop_filter()
void pluck(double position=0.5, double strength=1.0)
Pluck the string at a normalized position.
void set_loop_filter(const std::shared_ptr< Filters::Filter > &filter)
Replace the loop filter.
double observe_sample(const WaveguideSegment &seg) const
void map_parameter(const std::string ¶m_name, const std::shared_ptr< Node > &source, MappingMode mode=MappingMode::BROADCAST) override
Map external node output to network parameter.
void initialize_exciter()
std::vector< double > m_exciter_node_buffer
double generate_exciter_sample()
double m_delay_length_fraction
void set_loss_factor(double loss)
Set per-sample energy loss factor.
void set_fundamental(double freq)
Set fundamental frequency.
void unmap_parameter(const std::string ¶m_name) override
Remove parameter mapping.
void reset() override
Reset network to initial state.
size_t m_delay_length_integer
double get_pickup_position() const
Get current pickup position.
void set_exciter_sample(const std::vector< double > &sample)
Set custom excitation waveform.
std::vector< double > m_exciter_sample
std::shared_ptr< Filters::Filter > m_exciter_filter
WaveguideNetwork(WaveguideType type, double fundamental_freq, double sample_rate=48000.0)
Create waveguide network with specified type and frequency.
double read_with_interpolation(const Memory::HistoryBuffer< double > &delay, size_t integer_part, double fraction) const
Read from delay line with linear fractional interpolation.
ExciterType
Excitation signal types for waveguide synthesis.
@ IMPULSE
Single-sample Dirac impulse.
@ FILTERED_NOISE
Spectrally-shaped noise burst.
@ CONTINUOUS
External node as continuous exciter (bowing)
@ NOISE_BURST
Short white noise burst (default for pluck)
@ SAMPLE
User-provided excitation waveform.
std::optional< double > get_node_output(size_t index) const override
Get output of specific internal node (for ONE_TO_ONE mapping)
@ PRESSURE
Output is physical pressure at pickup (p_plus + p_minus)
double get_loss_factor() const
Get current loss factor.
void compute_delay_length()
double m_exciter_duration
void set_exciter_duration(double seconds)
Set noise burst duration for exciter.
void apply_broadcast_parameter(const std::string ¶m, double value)
void set_pickup_position(double position)
Set pickup position along the string.
size_t m_exciter_sample_position
void initialize() override
Called once before first process_batch()
void apply_one_to_one_parameter(const std::string ¶m, const std::shared_ptr< NodeNetwork > &source)
std::vector< WaveguideSegment > m_segments
std::optional< std::span< const double > > get_node_audio_buffer(size_t index) const override
Get output of specific internal node as audio buffer (for ONE_TO_ONE mapping)
@ RING
Circular: last node connects to first.
MappingMode
Defines how nodes map to external entities (e.g., audio channels, graphics objects)
@ ONE_TO_ONE
Node array/network → network nodes (must match count)
@ BROADCAST
One node → all network nodes.
@ AUDIO_SINK
Aggregated audio samples sent to output.
void extract_node_samples(const std::shared_ptr< Nodes::Node > &node, std::vector< double > &buffer, size_t &buffer_pos, size_t num_samples)
Extract num_samples from node into buffer using snapshot guard.
constexpr std::string_view enum_to_string(EnumType value) noexcept
Universal enum to string converter using magic_enum (original case)
std::shared_ptr< Node > broadcast_source
std::shared_ptr< NodeNetwork > network_source
Memory::HistoryBuffer< double > p_minus
Backward-traveling wave rail (BIDIRECTIONAL only)
std::shared_ptr< Filters::Filter > loop_filter_open
BIDIRECTIONAL: open-end filter (bell/bridge)
double reflection_open
Open-end reflection; a tube configures this to -1.
@ UNIDIRECTIONAL
Single loop (STRING)
@ BIDIRECTIONAL
Forward + backward rails (TUBE)
std::shared_ptr< Filters::Filter > loop_filter_closed
BIDIRECTIONAL: closed-end filter (mouthpiece/nut)
std::shared_ptr< Filters::Filter > loop_filter
UNIDIRECTIONAL: single termination filter.
Memory::HistoryBuffer< double > p_plus
Forward-traveling wave rail.
double reflection_closed
Closed-end reflection; a lumped string loop keeps +1 because its two inversions cancel.
1D delay-line segment supporting both uni- and bidirectional propagation