Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
hat.hpp
Go to the documentation of this file.
1#pragma once
2
7#include <pulp/signal/svf.hpp>
8
9#include <algorithm>
10#include <cmath>
11#include <cstddef>
12
13namespace pulp::signal::drum {
14
39class HatVoice : public Voice {
40public:
42 static constexpr std::size_t partial_count = 6;
43
48 static constexpr double partial_ratios[partial_count] = {
49 1.0, 1.4827, 1.8003, 2.5461, 2.6303, 3.8967};
50
53 r.level_db = 16.0f;
54 r.brightness_octaves = 0.8f;
56 }
57
59 void set_tune_hz(double hz) { tune_hz_ = std::clamp(hz, 40.0, 4000.0); }
60
64 void set_decay_ms(double ms) { decay_ms_ = std::clamp(ms, 5.0, 8000.0); }
65
69 void set_spread(double amount) { spread_ = std::clamp(amount, 0.0, 1.0); }
70
72 void set_metal(double amount) { metal_ = std::clamp(amount, 0.0, 1.0); }
73
79 void set_grit(double amount) { grit_ = std::clamp(amount, 0.0, 1.0); }
80 void set_grit_ratio(double ratio) { grit_ratio_ = std::clamp(ratio, 0.25, 16.0); }
81
85 void set_cutoff_hz(double hz) { cutoff_hz_ = std::clamp(hz, 200.0, 18000.0); }
86 void set_resonance(double q) { resonance_ = std::clamp(q, 0.5, 12.0); }
87
90 void set_bandpass(bool bandpass) { bandpass_ = bandpass; }
91
92 void set_noise_color(NoiseColor color) { noise_.set_color(color); }
93
94 OutputStage& output() { return output_; }
95 const OutputStage& output() const { return output_; }
96 OutputStage* output_stage() noexcept override { return &output_; }
97 int latency_samples() const noexcept override {
98 return output_.latency_samples();
99 }
100 OutputOversampling output_oversampling() const noexcept override {
101 return output_.oversampling();
102 }
104 output_.set_oversampling(factor);
105 }
106
107protected:
108 void on_prepare(double sample_rate) override {
109 noise_.prepare(sample_rate);
110 envelope_.set_sample_rate(sample_rate);
111 bank_.prepare(static_cast<float>(sample_rate));
112
113 SquareOscBank::Partial partials[partial_count];
114 bank_.set_partials(partials, partial_count);
115 apply_tuning();
116
117 filter_.set_sample_rate(static_cast<float>(sample_rate));
118 output_.prepare(sample_rate);
119 }
120
121 void on_reset() override {
122 envelope_.reset();
123 bank_.reset();
124 filter_.reset();
125 output_.reset();
126 noise_.reset();
127 grit_phase_ = 0.0;
128 }
129
130 void on_note_on(float velocity) override {
131 // The oscillator bank survives a retrigger, so its pending output FIR
132 // and lo-fi history are part of the same continuing physical state.
133 output_.trigger();
134 const auto& response = velocity_response();
135 velocity_gain_ = response.gain(velocity);
136 brightness_ = response.brightness_scale(velocity);
137
138 noise_.reset();
139 apply_tuning();
140 apply_filter();
141
142 // The oscillator bank is deliberately not reset. Its phases are what
143 // make one hat hit differ from the next: a cymbal is not re-struck from
144 // a known state, and restarting the cluster in phase every time is the
145 // machine-gun artefact in its most audible form -- every hit would open
146 // with the same broadband click.
147 envelope_.set_attack_ms(0.3);
148 envelope_.set_decay_t60_ms(decay_ms_);
149 envelope_.trigger();
150 }
151
152 bool on_is_active() const override {
153 return envelope_.is_active() || output_.has_tail();
154 }
155
156 void render_add(float* out, int num_samples) override {
157 const double noise_mix = 1.0 - metal_;
158 for (int i = 0; i < num_samples; ++i) {
159 double source = 0.0;
160 if (metal_ > 0.0) source += metal_ * static_cast<double>(bank_.process());
161 if (noise_mix > 0.0) source += noise_mix * static_cast<double>(noise_.process());
162
163 if (grit_ > 0.0) {
164 grit_phase_ += tune_hz_ * grit_ratio_ / sample_rate();
165 if (grit_phase_ >= 1.0) grit_phase_ -= std::floor(grit_phase_);
166 const double modulator =
167 std::sin(2.0 * 3.14159265358979323846 * grit_phase_);
168 source *= 1.0 + grit_ * modulator;
169 }
170
171 const double filtered = filter_.process(static_cast<float>(source));
172 const double shaped = filtered * envelope_.process();
173 out[i] += static_cast<float>(
174 output_.process(static_cast<float>(shaped)) * velocity_gain_);
175 }
176 }
177
178private:
179 void apply_tuning() {
180 for (std::size_t i = 0; i < partial_count; ++i) {
181 // Spread interpolates each oscillator between unison and its
182 // documented ratio, so the control is continuous rather than a
183 // switch between "square wave" and "hi-hat".
184 const double ratio = 1.0 + spread_ * (partial_ratios[i] - 1.0);
185 bank_.set_frequency(i, static_cast<float>(
186 std::min(tune_hz_ * ratio, 0.49 * sample_rate())));
187 bank_.set_amplitude(i, 1.0f / static_cast<float>(partial_count));
188 }
189 }
190
191 void apply_filter() {
192 filter_.set_mode(bandpass_ ? Svf::Mode::bandpass : Svf::Mode::highpass);
193 filter_.set_frequency(static_cast<float>(
194 std::min(cutoff_hz_ * brightness_, 0.49 * sample_rate())));
195 filter_.set_resonance(static_cast<float>(resonance_));
196 }
197
198 double tune_hz_ = 320.0;
199 double decay_ms_ = 60.0;
200 double spread_ = 1.0;
201 double metal_ = 0.8;
202 double grit_ = 0.0;
203 double grit_ratio_ = 1.4;
204 double cutoff_hz_ = 7000.0;
205 double resonance_ = 1.2;
206 bool bandpass_ = false;
207
208 NoiseSource noise_;
209 SquareOscBank bank_;
210 Svf filter_;
211 DecayEnvelope64 envelope_;
212 OutputStage output_;
213
214 double velocity_gain_ = 1.0;
215 double brightness_ = 1.0;
216 double grit_phase_ = 0.0;
217};
218
219} // namespace pulp::signal::drum
void set_frequency(std::size_t index, SampleType hz) noexcept
Retune one oscillator in place. No allocation; out-of-range is a no-op.
Definition square_osc_bank.hpp:79
void set_amplitude(std::size_t index, SampleType amp) noexcept
Reweight one oscillator in place. No allocation; out-of-range is a no-op.
Definition square_osc_bank.hpp:84
void set_frequency(SampleType hz)
Definition svf.hpp:21
void set_resonance(SampleType q)
Definition svf.hpp:22
void set_mode(Mode m)
Definition svf.hpp:23
bool on_is_active() const override
Definition hat.hpp:152
int latency_samples() const noexcept override
Fixed host-rate delay introduced by this voice's selected output quality.
Definition hat.hpp:97
void on_note_on(float velocity) override
Starts a hit at the given velocity, already clamped to [0, 1].
Definition hat.hpp:130
void render_add(float *out, int num_samples) override
Adds the voice's output into out. Must add, never assign.
Definition hat.hpp:156
void set_grit_ratio(double ratio)
Definition hat.hpp:80
void set_output_oversampling(OutputOversampling factor) override
Definition hat.hpp:103
void set_cutoff_hz(double hz)
Corner and resonance of the output filter.
Definition hat.hpp:85
void set_metal(double amount)
Balance between filtered noise (0) and the oscillator cluster (1).
Definition hat.hpp:72
HatVoice()
Definition hat.hpp:51
OutputStage * output_stage() noexcept override
The terminal saturation and degradation stage, at the generic voice boundary.
Definition hat.hpp:96
void on_reset() override
Clears every piece of state the voice carries, so is_active() becomes false and the next render start...
Definition hat.hpp:121
void set_tune_hz(double hz)
Frequency of the lowest oscillator in the bank, in Hz.
Definition hat.hpp:59
void set_decay_ms(double ms)
How long the metal rings, as a T60 in milliseconds.
Definition hat.hpp:64
const OutputStage & output() const
Definition hat.hpp:95
OutputOversampling output_oversampling() const noexcept override
Output quality at the generic voice boundary.
Definition hat.hpp:100
void set_resonance(double q)
Definition hat.hpp:86
OutputStage & output()
Definition hat.hpp:94
void set_spread(double amount)
How far the bank is spread from unison (0) to its full inharmonic cluster (1).
Definition hat.hpp:69
void set_bandpass(bool bandpass)
Use a bandpass rather than a highpass.
Definition hat.hpp:90
static constexpr std::size_t partial_count
Number of oscillators in the metallic bank.
Definition hat.hpp:42
void on_prepare(double sample_rate) override
Sizes buffers and fixes coefficients.
Definition hat.hpp:108
static constexpr double partial_ratios[partial_count]
Frequency ratios of the TR-808's hi-hat oscillator bank, relative to its lowest oscillator.
Definition hat.hpp:48
void set_noise_color(NoiseColor color)
Definition hat.hpp:92
void set_grit(double amount)
Depth of a ring modulation applied to the source before filtering, and the modulator's ratio to the t...
Definition hat.hpp:79
double sample_rate() const
Definition voice.hpp:105
void set_velocity_response(const VelocityResponse &r)
Definition voice.hpp:129
float velocity() const
Definition voice.hpp:104
const VelocityResponse & velocity_response() const
Definition voice.hpp:130
Definition clap.hpp:13
OutputOversampling
Realtime quality choices for the shared drum output nonlinearity.
Definition output_stage.hpp:20
OutputStageT< float > OutputStage
Definition output_stage.hpp:384
SvfT< float > Svf
Definition svf.hpp:148
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ i
Definition formant_filter_bank.hpp:20
SquareOscBankT< float > SquareOscBank
Definition square_osc_bank.hpp:134
@ bandpass
Definition filter_morph.hpp:19
NoiseColor
Spectral tilt of a NoiseSourceT, named by the usual colour convention.
Definition noise_source.hpp:17
DecayEnvelopeT< double > DecayEnvelope64
Definition decay_envelope.hpp:201
NoiseSourceT< float > NoiseSource
Definition noise_source.hpp:279
How hard a hit changes the sound, beyond changing how loud it is.
Definition velocity.hpp:33