memlnaut-nisps/nisps/engines/xiasri.hpp

155 lines
5.3 KiB
C++
Raw Permalink Normal View History

feat(nisps/engines): port firmware audio engines to AudioEngine concept (meml-1v6) 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.
2026-04-29 15:09:12 +02:00
// nisps/engines/xiasri.hpp — Reverb + delays + pitch-shift FX engine.
//
// Mirrors firmware XIASRIAudioApp. Direct NN→param mapping — the firmware
// ignored its voice-space slot and read smoothParams[] inline, so this
// engine has no VoiceSpace machinery at all: the 24 NN outputs feed the
// smoother directly.
feat(nisps/engines): port firmware audio engines to AudioEngine concept (meml-1v6) 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.
2026-04-29 15:09:12 +02:00
//
// Pipeline (per sample):
// pitch_shift → DC-blocker → 6-allpass + 2-comb reverb tail → 4 parallel
// delays → wet/dry mix → tanh → mono out
//
// All parameters are smoothed via a OnePoleSmoother before they reach the
// audio path so that ML output discontinuities don't audibly click.
#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/dc_blocker.hpp"
#include "../dsp/delay.hpp"
#include "../dsp/filter.hpp"
#include "../dsp/pitch_shift.hpp"
#include "../dsp/reverb.hpp"
namespace nisps {
class XIASRIEngine {
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 "xiasri"; }
void setup(float sample_rate) noexcept {
sample_rate_ = sample_rate;
smoother_.setup(150.f, sample_rate);
pitch_shifter_.init(sample_rate);
for (auto& v : nn_outputs_) v = 0.f;
for (auto& v : smooth_params_) v = 0.f;
}
void set_params(std::span<const float> params) noexcept {
if (params.size() < kNParams) return;
// Direct copy — actual parameter mapping is per-sample in process().
for (std::size_t i = 0u; i < kNParams; ++i) nn_outputs_[i] = params[i];
}
NISPS_HOT NISPS_FORCE_INLINE stereosample_t process(stereosample_t x) noexcept {
smoother_.process(nn_outputs_.data(), smooth_params_.data());
const float dl1mix = smooth_params_[0] * 0.4f;
const float dl2mix = smooth_params_[1] * 0.4f;
const float dl3mix = smooth_params_[2] * 0.8f;
const float dl4mix = smooth_params_[22] * smooth_params_[22] * 0.41f;
const float allp1fb = smooth_params_[4] * 0.99f;
const float allp2fb = smooth_params_[5] * 0.99f;
const float comb1fb = smooth_params_[6] * 0.95f;
const float comb2fb = smooth_params_[7] * 0.95f;
const float dl1fb = smooth_params_[8] * 0.95f;
const float dl2fb = smooth_params_[9] * 0.95f;
const float dl3fb = smooth_params_[10] * 0.95f;
const float dl4fb = smooth_params_[23] * 0.95f;
const float wet = (smooth_params_[11] * 0.7f) + 0.3f;
// Keep firmware's curious +12 semitone offset for parity.
pitch_shifter_.set_transposition(12.f + smooth_params_[12]);
const float ps_mix = smooth_params_[13] * 0.99f;
const float allp3fb = smooth_params_[14] * 0.99f;
const float allp4fb = smooth_params_[15] * 0.99f;
const float allp5fb = smooth_params_[16] * 0.99f;
const float allp6fb = smooth_params_[17] * 0.99f;
const float allp3mix = smooth_params_[18];
const float allp4mix = smooth_params_[19];
const float allp5mix = smooth_params_[20];
const float allp6mix = smooth_params_[21];
// Stereo collapsed to mono pre-FX, scaled by 2.
float mix = (x.L + x.R) * 2.f;
float ps = pitch_shifter_.process(mix);
ps = (mix * (1.f - ps_mix)) + (ps * ps_mix);
float y = dcb_.play(ps, 0.99f) * 2.f;
float y1 = allp1_.process(y, 30u, allp1fb);
y1 = comb1_.process(y1, 127u, comb1fb);
float y2 = allp2_.process(y, 482u, allp2fb);
y2 = comb2_.process(y2, 808u, comb2fb);
const float y3 = allp3_.process(y, 19u, allp3fb) * allp3mix;
const float y4 = allp4_.process(y, 69u, allp4fb) * allp4mix;
const float y5 = allp5_.process(y, 131u, allp5fb) * allp5mix;
const float y6 = allp6_.process(y, 287u, allp6fb) * allp6mix;
y = y1 + y2 + y3 + y4 + y5 + y6;
const float d1 = dl1_.play(y, 3500u, dl1fb) * dl1mix;
const float d2 = dl2_.play(y, 7886u, dl2fb) * dl2mix;
const float d3 = dl3_.play(y, 299u, dl3fb) * dl3mix;
// d4 captured but not summed in firmware code path; preserve semantics.
(void)dl4_.play(y, 15873u, dl4fb);
(void)dl4mix;
y = y + d1 + d2 + d3;
y = (y * wet) + (mix * (1.f - wet));
y = std::tanh(y * 1.2f);
return {y, y};
}
DriverConfig driver_config() const noexcept {
DriverConfig c;
c.line_level = 6u;
c.output_volume = 0.9f;
return c;
}
private:
float sample_rate_ = 48000.f;
PitchShifter<8192> pitch_shifter_;
DCBlocker dcb_;
Delay<5000> dl1_;
Delay<8000> dl2_;
Delay<1201> dl3_;
Delay<16000> dl4_;
AllPass<300> allp1_;
AllPass<500> allp2_;
AllPass<500> allp3_;
AllPass<500> allp4_;
AllPass<500> allp5_;
AllPass<500> allp6_;
Comb<200> comb1_;
Comb<900> comb2_;
OnePoleSmoother<kNParams> smoother_;
std::array<float, kNParams> nn_outputs_{};
std::array<float, kNParams> smooth_params_{};
};
static_assert(AudioEngine<XIASRIEngine>, "XIASRIEngine must satisfy AudioEngine");
} // namespace nisps