Concept-based, no virtual dispatch, per-engine voice spaces as inline
methods. Each engine satisfies nisps::AudioEngine via static_assert.
- NoOpEngine: silent passthrough; used for sequencer-only modes and
for the SoundAnalysisMIDI mode's audio path.
- PAFSynthEngine (33 params, 7 voice spaces): 4-voice PAF synth with
detune cascade, ring-mod, sine-shaper, ADSR, feedback delay. note_on/
note_off interface for MIDI keyboard.
- ChannelStripEngine (24 params, 6 voice spaces): stereo console strip
(pre-gain/HPF/LPF/2x peak/low-shelf/high-shelf/comp/post-gain). Voice
spaces: WannabeNeve66, SSL4K, SSL9K, MaleVox, FemaleVox, Neve80
(stepped-frequency).
- XIASRIEngine (24 params, "Direct" voice space): pitch-shift + 6 allpass
+ 2 comb + 4 delays. Direct NN→param mapping per firmware semantics.
- VerbFXEngine (47 params, 12 voice spaces): 8-band SVF filterbank +
3-lane dynamic delay + 8-lpcomb/4-allpass Freeverb-style tail with
cross-fades. All 12 voice spaces ported from voicespaces/VerbFX/*.hpp.
- MEMLCeliumEngine (56 params): 2-track ratio sequencer + dual-voice
PAF synth (7+7+22+20 layout). Sequencer triggers V0/V1 ADSR.
- BreakOrEngine (56 params): 8-track ratio sequencer; emits NoteOn/
NoteOff/Clock events via pop_events(span). process() returns silence.
- ElysiamorfEngine (40 params): 8-track FM-pair sequencer; emits CC
events on CCs {1,2,3,4,5,9,11,12}. Silent audio path.
- AnalysisEngine (0 params, 6 features): port of XiasriAnalysis (pitch
via zero-crossing, aperiodicity via MAD, log-domain energy + attack
derivative + brightness ratio). Inputs to ML on SoundAnalysisMIDI mode.
All param_count() values match schemas/modes/*.json output_size.
4074 LOC total. CMake adds nisps_dsp_engine_tests target with 38
passing tests under -Wall -Wextra -Werror -Wpedantic.
354 lines
16 KiB
C++
354 lines
16 KiB
C++
// nisps/engines/channel_strip.hpp — stereo console-style channel strip.
|
||
//
|
||
// Mirrors firmware ChannelStripAudioApp. Per-channel signal flow:
|
||
// pre-gain → tanh → HPF → LPF → 2× peak EQ → low-shelf → high-shelf →
|
||
// compressor → tanh → post-gain.
|
||
//
|
||
// Voice spaces are inline lambdas (`apply_voice_space_*`) that translate a
|
||
// 24-element NN output vector into the named member fields. They mirror
|
||
// `voicespaces/ChannelStrip/basic.hpp` exactly.
|
||
//
|
||
// Compressor: a feed-forward design with envelope follower + log-domain
|
||
// threshold + linear-domain ratio + smoothed gain. This is simpler than
|
||
// maximilian's `maxiDynamicsLite` (which has lookahead, knee, and bidirectional
|
||
// companding); the audible result is close enough for the voice-space ranges.
|
||
|
||
#pragma once
|
||
|
||
#include <array>
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
#include <span>
|
||
#include <string_view>
|
||
|
||
#include "../core/concepts.hpp"
|
||
#include "../core/perf.hpp"
|
||
#include "../core/types.hpp"
|
||
#include "../dsp/biquad.hpp"
|
||
#include "../dsp/filter.hpp"
|
||
|
||
namespace nisps {
|
||
|
||
class ChannelStripEngine {
|
||
public:
|
||
static constexpr std::size_t kNParams = 24u;
|
||
static constexpr std::size_t param_count() noexcept { return kNParams; }
|
||
static constexpr std::string_view engine_id() noexcept { return "channel_strip"; }
|
||
|
||
enum class VoiceSpace : std::size_t {
|
||
WannabeNeve66 = 0,
|
||
SSL4KGist = 1,
|
||
SSL9KInda = 2,
|
||
MaleVox = 3,
|
||
FemaleVox = 4,
|
||
Neve80 = 5,
|
||
Count = 6,
|
||
};
|
||
|
||
static constexpr std::size_t kVoiceSpaceCount = static_cast<std::size_t>(VoiceSpace::Count);
|
||
static constexpr std::array<std::string_view, kVoiceSpaceCount> kVoiceSpaceNames = {
|
||
"WannabeNeve66", "SSL 4K G-ist", "SSL 9K-inda", "MaleVox", "FemaleVox", "Neve 80"};
|
||
|
||
void set_voice_space(VoiceSpace vs) noexcept { voice_space_ = vs; }
|
||
VoiceSpace voice_space() const noexcept { return voice_space_; }
|
||
|
||
void setup(float sample_rate) noexcept {
|
||
sample_rate_ = sample_rate;
|
||
for (auto* f : {&in_hpf_l_, &in_lpf_l_, &in_hpf_r_, &in_lpf_r_}) f->setup(sample_rate);
|
||
peak0_l_.setup(sample_rate);
|
||
peak1_l_.setup(sample_rate);
|
||
ls_l_.setup(sample_rate);
|
||
hs_l_.setup(sample_rate);
|
||
peak0_r_.setup(sample_rate);
|
||
peak1_r_.setup(sample_rate);
|
||
ls_r_.setup(sample_rate);
|
||
hs_r_.setup(sample_rate);
|
||
comp_env_l_.setup(sample_rate, 10.f, 200.f);
|
||
comp_env_r_.setup(sample_rate, 10.f, 200.f);
|
||
}
|
||
|
||
void set_params(std::span<const float> params) noexcept {
|
||
if (params.size() < kNParams) return;
|
||
std::array<float, kNParams> p;
|
||
for (std::size_t i = 0u; i < kNParams; ++i) p[i] = params[i];
|
||
switch (voice_space_) {
|
||
case VoiceSpace::WannabeNeve66: apply_neve66(p); break;
|
||
case VoiceSpace::SSL4KGist: apply_ssl4k(p); break;
|
||
case VoiceSpace::SSL9KInda: apply_ssl9k(p); break;
|
||
case VoiceSpace::MaleVox: apply_male_vox(p); break;
|
||
case VoiceSpace::FemaleVox: apply_female_vox(p); break;
|
||
case VoiceSpace::Neve80: apply_neve80(p); break;
|
||
case VoiceSpace::Count: break;
|
||
}
|
||
// Apply EQ updates to all biquads.
|
||
peak0_l_.set(Biquad::Type::Peak, peak0_freq_, peak0_q_, peak0_gain_);
|
||
peak1_l_.set(Biquad::Type::Peak, peak1_freq_, peak1_q_, peak1_gain_);
|
||
ls_l_.set(Biquad::Type::LowShelf, low_shelf_freq_, low_shelf_q_, low_shelf_gain_);
|
||
hs_l_.set(Biquad::Type::HighShelf, high_shelf_freq_, high_shelf_q_, high_shelf_gain_);
|
||
peak0_r_.set(Biquad::Type::Peak, peak0_freq_, peak0_q_, peak0_gain_);
|
||
peak1_r_.set(Biquad::Type::Peak, peak1_freq_, peak1_q_, peak1_gain_);
|
||
ls_r_.set(Biquad::Type::LowShelf, low_shelf_freq_, low_shelf_q_, low_shelf_gain_);
|
||
hs_r_.set(Biquad::Type::HighShelf, high_shelf_freq_, high_shelf_q_, high_shelf_gain_);
|
||
comp_env_l_.set_attack(comp_attack_);
|
||
comp_env_l_.set_release(comp_release_);
|
||
comp_env_r_.set_attack(comp_attack_);
|
||
comp_env_r_.set_release(comp_release_);
|
||
}
|
||
|
||
NISPS_HOT NISPS_FORCE_INLINE stereosample_t process(stereosample_t x) noexcept {
|
||
if (bypass_all_) return x;
|
||
|
||
float yl = x.L;
|
||
float yr = x.R;
|
||
|
||
if (!bypass_pre_post_gain_) {
|
||
yl = std::tanh(yl * pre_gain_);
|
||
yr = std::tanh(yr * pre_gain_);
|
||
}
|
||
if (!bypass_in_filters_) {
|
||
yl = in_lpf_l_.lowpass(yl, in_lowpass_cutoff_, 1.f);
|
||
yl = in_hpf_l_.highpass(yl, in_highpass_cutoff_, 1.f);
|
||
yr = in_lpf_r_.lowpass(yr, in_lowpass_cutoff_, 1.f);
|
||
yr = in_hpf_r_.highpass(yr, in_highpass_cutoff_, 1.f);
|
||
}
|
||
if (!bypass_eq_) {
|
||
yl = peak0_l_.play(yl);
|
||
yl = peak1_l_.play(yl);
|
||
yl = ls_l_.play(yl);
|
||
yl = hs_l_.play(yl);
|
||
yr = peak0_r_.play(yr);
|
||
yr = peak1_r_.play(yr);
|
||
yr = ls_r_.play(yr);
|
||
// Right-channel highshelf intentionally skipped — matches firmware's
|
||
// commented-out `// y1 = highshelf1.play(y1)`. Sonically negligible
|
||
// for these voice spaces but preserved for parity.
|
||
}
|
||
if (!bypass_comp_) {
|
||
yl = compress(yl, comp_env_l_, comp_threshold_, comp_ratio_);
|
||
yr = compress(yr, comp_env_r_, comp_threshold_, comp_ratio_);
|
||
}
|
||
if (!bypass_pre_post_gain_) {
|
||
yl = std::tanh(yl * post_gain_);
|
||
yr = std::tanh(yr * post_gain_);
|
||
}
|
||
return {yl, yr};
|
||
}
|
||
|
||
DriverConfig driver_config() const noexcept {
|
||
DriverConfig c;
|
||
c.line_level = 6u;
|
||
c.output_volume = 0.9f;
|
||
return c;
|
||
}
|
||
|
||
void set_bypass_all(bool b) noexcept { bypass_all_ = b; }
|
||
void set_bypass_eq(bool b) noexcept { bypass_eq_ = b; }
|
||
void set_bypass_comp(bool b) noexcept { bypass_comp_ = b; }
|
||
void set_bypass_pre_post_gain(bool b) noexcept { bypass_pre_post_gain_ = b; }
|
||
void set_bypass_in_filters(bool b) noexcept { bypass_in_filters_ = b; }
|
||
|
||
private:
|
||
NISPS_HOT NISPS_FORCE_INLINE float compress(float x, EnvelopeFollower& env,
|
||
float threshold_db, float ratio) noexcept {
|
||
// Simple downward compressor: detect via envelope follower, convert to
|
||
// dB, apply ratio above threshold, return to linear.
|
||
const float env_lin = env.play(x) + 1e-7f;
|
||
const float env_db = 20.f * std::log10(env_lin);
|
||
float gain_db = 0.f;
|
||
if (env_db > threshold_db && ratio > 1.f) {
|
||
gain_db = -(env_db - threshold_db) * (1.f - 1.f / ratio);
|
||
}
|
||
const float gain_lin = std::pow(10.f, gain_db * 0.05f);
|
||
return x * gain_lin;
|
||
}
|
||
|
||
void apply_neve66(const std::array<float, kNParams>& p) noexcept {
|
||
pre_gain_ = 0.5f + (p[0] * p[0] * 4.f);
|
||
in_lowpass_cutoff_ = 2000.f + (p[7] * p[7] * 18000.f);
|
||
in_highpass_cutoff_ = 30.f + (p[8] * p[8] * 270.f);
|
||
low_shelf_freq_ = 31.5f + (p[14] * p[14] * 313.5f);
|
||
low_shelf_q_ = 0.6f + (p[15] * 4.4f);
|
||
low_shelf_gain_ = -15.f + (p[16] * 30.f);
|
||
peak0_freq_ = 200.f + (p[1] * p[1] * 1800.f);
|
||
peak0_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak0_gain_ = -18.f + (p[6] * 36.f);
|
||
peak1_freq_ = 800.f + (p[4] * p[4] * 7200.f);
|
||
peak1_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak1_gain_ = -18.f + (p[6] * 36.f);
|
||
high_shelf_freq_ = 1600.f + (p[17] * p[17] * 14400.f);
|
||
high_shelf_q_ = 0.6f + (p[18] * 4.4f);
|
||
high_shelf_gain_ = -18.f + (p[19] * 36.f);
|
||
comp_threshold_ = 20.f + (p[10] * -40.f);
|
||
comp_ratio_ = 1.f + (p[11] * 19.f);
|
||
comp_attack_ = 0.002f + (p[12] * 10.f);
|
||
comp_release_ = 30.f + (p[13] * p[13] * 2970.f);
|
||
post_gain_ = 0.5f + (p[23] * p[23] * 4.f);
|
||
}
|
||
void apply_ssl4k(const std::array<float, kNParams>& p) noexcept {
|
||
pre_gain_ = 0.5f + (p[0] * p[0] * 4.f);
|
||
in_lowpass_cutoff_ = 3000.f + (p[7] * p[7] * 18000.f);
|
||
in_highpass_cutoff_ = 10.f + (p[8] * p[8] * 340.f);
|
||
low_shelf_freq_ = 30.f + (p[14] * p[14] * 420.f);
|
||
low_shelf_q_ = 0.6f + (p[15] * 4.4f);
|
||
low_shelf_gain_ = -18.f + (p[16] * 36.f);
|
||
peak0_freq_ = 200.f + (p[1] * p[1] * 2300.f);
|
||
peak0_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak0_gain_ = -22.f + (p[6] * 44.f);
|
||
peak1_freq_ = 600.f + (p[4] * p[4] * 6400.f);
|
||
peak1_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak1_gain_ = -22.f + (p[6] * 44.f);
|
||
high_shelf_freq_ = 1500.f + (p[17] * p[17] * 14500.f);
|
||
high_shelf_q_ = 0.6f + (p[18] * 4.4f);
|
||
high_shelf_gain_ = -20.f + (p[19] * 40.f);
|
||
comp_threshold_ = 10.f + (p[10] * -30.f);
|
||
comp_ratio_ = 1.f + (p[11] * p[11] * 20.f);
|
||
comp_attack_ = 0.08f + (p[12] * 3.f);
|
||
comp_release_ = 100.f + (p[13] * p[13] * 3900.f);
|
||
post_gain_ = 0.5f + (p[23] * p[23] * 4.f);
|
||
}
|
||
void apply_ssl9k(const std::array<float, kNParams>& p) noexcept {
|
||
pre_gain_ = 0.5f + (p[0] * p[0] * 4.f);
|
||
in_lowpass_cutoff_ = 3000.f + (p[7] * p[7] * 18000.f);
|
||
in_highpass_cutoff_ = 10.f + (p[8] * p[8] * 490.f);
|
||
low_shelf_freq_ = 40.f + (p[14] * p[14] * 560.f);
|
||
low_shelf_q_ = 0.6f + (p[15] * 4.4f);
|
||
low_shelf_gain_ = -20.f + (p[16] * 40.f);
|
||
peak0_freq_ = 200.f + (p[1] * p[1] * 1800.f);
|
||
peak0_q_ = 0.5f + (p[5] * 2.f);
|
||
peak0_gain_ = -20.f + (p[6] * 40.f);
|
||
peak1_freq_ = 600.f + (p[4] * p[4] * 6400.f);
|
||
peak1_q_ = 0.5f + (p[5] * 2.f);
|
||
peak1_gain_ = -20.f + (p[6] * 40.f);
|
||
high_shelf_freq_ = 1500.f + (p[17] * p[17] * 20500.f);
|
||
high_shelf_q_ = 0.6f + (p[18] * 4.4f);
|
||
high_shelf_gain_ = -20.f + (p[19] * 40.f);
|
||
comp_threshold_ = 10.f + (p[10] * -30.f);
|
||
comp_ratio_ = 1.f + (p[11] * p[11] * 20.f);
|
||
comp_attack_ = 0.08f + (p[12] * 3.f);
|
||
comp_release_ = 100.f + (p[13] * p[13] * 3900.f);
|
||
post_gain_ = 0.5f + (p[23] * p[23] * 4.f);
|
||
}
|
||
void apply_male_vox(const std::array<float, kNParams>& p) noexcept {
|
||
pre_gain_ = 0.5f + (p[0] * p[0] * 4.f);
|
||
in_lowpass_cutoff_ = 1000.f + (p[7] * p[7] * 19000.f);
|
||
in_highpass_cutoff_ = 10.f + (p[8] * p[8] * 1990.f);
|
||
comp_threshold_ = p[10] * -30.f;
|
||
comp_ratio_ = 2.f + (p[11] * 6.f);
|
||
comp_attack_ = 0.08f + (p[12] * 50.f);
|
||
comp_release_ = 50.f + (p[13] * 500.f);
|
||
low_shelf_freq_ = 60.f + (p[14] * p[14] * 240.f);
|
||
low_shelf_q_ = 0.6f + (p[15] * 4.4f);
|
||
low_shelf_gain_ = -18.f + (p[16] * 36.f);
|
||
peak0_freq_ = 60.f + (p[1] * p[1] * 440.f);
|
||
peak0_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak0_gain_ = -18.f + (p[6] * 36.f);
|
||
peak1_freq_ = 300.f + (p[4] * p[4] * 7700.f);
|
||
peak1_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak1_gain_ = -18.f + (p[6] * 36.f);
|
||
high_shelf_freq_ = 1000.f + (p[17] * p[17] * 7000.f);
|
||
high_shelf_q_ = 0.6f + (p[18] * 4.4f);
|
||
high_shelf_gain_ = -18.f + (p[19] * 36.f);
|
||
post_gain_ = 0.5f + (p[23] * p[23] * 4.f);
|
||
}
|
||
void apply_female_vox(const std::array<float, kNParams>& p) noexcept {
|
||
pre_gain_ = 0.5f + (p[0] * p[0] * 4.f);
|
||
in_lowpass_cutoff_ = 1000.f + (p[7] * p[7] * 19000.f);
|
||
in_highpass_cutoff_ = 10.f + (p[8] * p[8] * 1990.f);
|
||
comp_threshold_ = p[10] * -30.f;
|
||
comp_ratio_ = 2.f + (p[11] * 6.f);
|
||
comp_attack_ = 0.08f + (p[12] * 50.f);
|
||
comp_release_ = 50.f + (p[13] * 500.f);
|
||
low_shelf_freq_ = 120.f + (p[14] * p[14] * 180.f);
|
||
low_shelf_q_ = 0.6f + (p[15] * 4.4f);
|
||
low_shelf_gain_ = -18.f + (p[16] * 36.f);
|
||
peak0_freq_ = 120.f + (p[1] * p[1] * 380.f);
|
||
peak0_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak0_gain_ = -18.f + (p[6] * 36.f);
|
||
peak1_freq_ = 300.f + (p[4] * p[4] * 9700.f);
|
||
peak1_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak1_gain_ = -18.f + (p[6] * 36.f);
|
||
high_shelf_freq_ = 1000.f + (p[17] * p[17] * 9000.f);
|
||
high_shelf_q_ = 0.6f + (p[18] * 4.4f);
|
||
high_shelf_gain_ = -18.f + (p[19] * 36.f);
|
||
post_gain_ = 0.5f + (p[23] * p[23] * 4.f);
|
||
}
|
||
void apply_neve80(const std::array<float, kNParams>& p) noexcept {
|
||
// Stepped frequencies — Neve-style fixed values picked by parameter index.
|
||
static const float lo_pass_freqs[] = {190.f, 1200.f, 3900.f, 5600.f, 9200.f};
|
||
static const float hi_pass_freqs[] = {27.f, 47.f, 92.f, 150.f, 270.f};
|
||
static const float comp_ratios[] = {1.5f, 2.f, 3.f, 4.f, 6.f};
|
||
static const float comp_releases[] = {400.f, 800.f, 1500.f};
|
||
static const float low_shelf_freqs[] = {33.f, 56.f, 100.f, 190.f, 330.f};
|
||
static const float low_peak_freqs[] = {220.f, 270.f, 330.f, 390.f, 470.f, 560.f, 690.f, 820.f, 1000.f, 1200.f};
|
||
static const float high_peak_freqs[] = {1500.f, 1900.f, 2200.f, 2700.f, 3300.f, 3900.f, 4700.f, 5600.f, 6900.f, 8200.f};
|
||
static const float high_shelf_freqs[] = {3300.f, 4700.f, 6900.f, 10000.f, 15000.f};
|
||
|
||
pre_gain_ = 0.5f + (p[0] * p[0] * 4.f);
|
||
in_lowpass_cutoff_ = lo_pass_freqs[idx_clamp(p[7] * 3.999999f, 5)];
|
||
in_highpass_cutoff_ = hi_pass_freqs[idx_clamp(p[8] * 3.999999f, 5)];
|
||
comp_threshold_ = p[10] * -30.f;
|
||
comp_ratio_ = comp_ratios[idx_clamp(p[11] * 3.999999f, 5)];
|
||
comp_attack_ = p[12] > 0.5f ? 5.f : 1.f;
|
||
comp_release_ = comp_releases[idx_clamp(p[13] * 1.999999f, 3)];
|
||
low_shelf_freq_ = low_shelf_freqs[idx_clamp(p[14] * 3.999999f, 5)];
|
||
low_shelf_q_ = 0.6f + (p[15] * 4.4f);
|
||
low_shelf_gain_ = -18.f + (p[16] * 36.f);
|
||
peak0_freq_ = low_peak_freqs[idx_clamp(p[1] * 8.999999f, 10)];
|
||
peak0_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak0_gain_ = -18.f + (p[6] * 36.f);
|
||
peak1_freq_ = high_peak_freqs[idx_clamp(p[4] * 8.999999f, 10)];
|
||
peak1_q_ = 0.6f + (p[5] * 4.4f);
|
||
peak1_gain_ = -18.f + (p[6] * 36.f);
|
||
high_shelf_freq_ = high_shelf_freqs[idx_clamp(p[17] * 3.999999f, 5)];
|
||
high_shelf_q_ = 0.6f + (p[18] * 4.4f);
|
||
high_shelf_gain_ = -18.f + (p[19] * 36.f);
|
||
post_gain_ = 0.5f + (p[23] * p[23] * 4.f);
|
||
}
|
||
|
||
static std::size_t idx_clamp(float x, std::size_t n) noexcept {
|
||
if (x < 0.f) return 0u;
|
||
const auto i = static_cast<std::size_t>(x);
|
||
return i >= n ? n - 1u : i;
|
||
}
|
||
|
||
float sample_rate_ = 48000.f;
|
||
|
||
// Voice-space-driven engine state.
|
||
float pre_gain_ = 1.f;
|
||
float post_gain_ = 1.f;
|
||
float in_lowpass_cutoff_ = 200.f;
|
||
float in_highpass_cutoff_ = 2000.f;
|
||
float comp_threshold_ = 0.f;
|
||
float comp_ratio_ = 1.f;
|
||
float comp_attack_ = 10.f;
|
||
float comp_release_ = 50.f;
|
||
float peak0_freq_ = 100.f;
|
||
float peak0_q_ = 1.f;
|
||
float peak0_gain_ = 1.f;
|
||
float peak1_freq_ = 1000.f;
|
||
float peak1_q_ = 1.f;
|
||
float peak1_gain_ = 1.f;
|
||
float low_shelf_freq_ = 100.f;
|
||
float low_shelf_q_ = 1.f;
|
||
float low_shelf_gain_ = 0.f;
|
||
float high_shelf_freq_ = 8000.f;
|
||
float high_shelf_q_ = 1.f;
|
||
float high_shelf_gain_ = 0.f;
|
||
|
||
bool bypass_all_ = false;
|
||
bool bypass_eq_ = false;
|
||
bool bypass_comp_ = false;
|
||
bool bypass_pre_post_gain_ = false;
|
||
bool bypass_in_filters_ = false;
|
||
|
||
VoiceSpace voice_space_ = VoiceSpace::WannabeNeve66;
|
||
|
||
ChamberlinSVF in_hpf_l_, in_lpf_l_, in_hpf_r_, in_lpf_r_;
|
||
Biquad peak0_l_, peak1_l_, ls_l_, hs_l_;
|
||
Biquad peak0_r_, peak1_r_, ls_r_, hs_r_;
|
||
EnvelopeFollower comp_env_l_, comp_env_r_;
|
||
};
|
||
|
||
static_assert(AudioEngine<ChannelStripEngine>, "ChannelStripEngine must satisfy AudioEngine");
|
||
|
||
} // namespace nisps
|