memlnaut-nisps/tests/cpp/test_engine_analysis.cpp
w1n5t0n 8d0d47b992 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 16:09:12 +03:00

37 lines
1.3 KiB
C++

// tests/cpp/test_engine_analysis.cpp — XiasriAnalysis port smoke test.
#include <cmath>
#include "test_helpers.hpp"
#include "../../nisps/engines/analysis.hpp"
NISPS_TEST(analysis_concept) {
static_assert(nisps::AudioEngine<nisps::AnalysisEngine>);
NISPS_EXPECT(nisps::AnalysisEngine::param_count() == 0u);
NISPS_EXPECT(nisps::AnalysisEngine::engine_id() == "analysis");
}
NISPS_TEST(analysis_features_in_range) {
nisps::AnalysisEngine e;
e.setup(48000.f);
// Feed a 220 Hz sine.
for (int n = 0; n < 48000; ++n) {
const float x = std::sin(2.f * 3.14159265f * 220.f * n / 48000.f) * 0.5f;
e.process({x, x});
}
const auto& f = e.features();
NISPS_EXPECT(f.pitch >= 0.f && f.pitch <= 1.f);
NISPS_EXPECT(f.aperiodicity >= 0.f && f.aperiodicity <= 1.f);
NISPS_EXPECT(f.energy >= 0.f && f.energy <= 1.f);
NISPS_EXPECT(f.attack >= 0.f && f.attack <= 1.f);
NISPS_EXPECT(f.brightness >= 0.f && f.brightness <= 1.f);
}
NISPS_TEST(analysis_silence_gives_low_energy) {
nisps::AnalysisEngine e;
e.setup(48000.f);
for (int n = 0; n < 48000; ++n) e.process({0.f, 0.f});
const auto& f = e.features();
NISPS_EXPECT(f.energy < 0.1f); // log envelope clamps to ~0
NISPS_EXPECT(f.energy_crude == 0.f);
}