memlnaut-nisps/tests/cpp/test_mode_voice_space.cpp
w1n5t0n 4964037da0 test(nisps/modes): host C++ tests for stream 4 mode layer (meml-beb)
Adds `nisps_modes_tests` executable to nisps/CMakeLists.txt with four TUs:

  - `test_mode_concepts.cpp`: 8 `static_assert(Mode<...>)` (concept
    satisfaction), plus runtime metadata sanity for each mode (mode_id,
    input_channel_count, schema sizes match engine param_count).

  - `test_mode_paf_synth.cpp`: end-to-end exercise — setup, set_input,
    tick_control, process audio. Verifies idle process is finite, output
    bounds [0,1] hold, note_on triggers nonzero audio, input clamping,
    and engine/ml accessors round-trip.

  - `test_mode_voice_space.cpp`: voice-space round trip for PAFSynth,
    ChannelStrip and VerbFX (all dispatched modes). Confirms
    out-of-range index is silently ignored.

  - `test_mode_breakor_events.cpp`: sequencer event pumping. BreakOr
    emits Clock + NoteOn/Off, Elysiamorf emits CC, SoundAnalysisMIDI
    converts 8 ML outputs → 8 ControlEvents (CC 0..7) per tick, ring
    buffer overflow drops cleanly.

Total: 22 new tests (110 across nisps/), all passing under -Werror
-Wpedantic. Build remains clean.
2026-04-29 16:27:03 +03:00

71 lines
2.3 KiB
C++

// tests/cpp/test_mode_voice_space.cpp — verify that switching voice spaces
// changes the engine's parameter mapping. We don't compare audio output
// directly (different voice spaces converge to similar timbres for some
// param vectors); instead we verify the engine stores the new voice-space
// index and that subsequent process() doesn't blow up.
#include <array>
#include <cmath>
#include "test_helpers.hpp"
#include "../../nisps/modes/channel_strip.hpp"
#include "../../nisps/modes/paf_synth.hpp"
#include "../../nisps/modes/verb_fx.hpp"
using nisps::modes::ChannelStripMode;
using nisps::modes::PAFSynthMode;
using nisps::modes::VerbFXMode;
NISPS_TEST(voice_space_paf_synth_round_trip) {
PAFSynthMode m;
m.setup(48000.f);
for (std::size_t i = 0u; i < nisps::PAFSynthEngine::kVoiceSpaceCount; ++i) {
m.set_voice_space(i);
NISPS_EXPECT(m.voice_space_index() == i);
// Tick control + process; no NaN / no crash.
m.set_input(0, 0.5f);
m.tick_control();
for (int n = 0; n < 64; ++n) {
const auto y = m.process({0.f, 0.f});
NISPS_EXPECT(std::isfinite(y.L));
NISPS_EXPECT(std::isfinite(y.R));
}
}
}
NISPS_TEST(voice_space_channel_strip_dispatch) {
ChannelStripMode m;
m.setup(48000.f);
for (std::size_t i = 0u; i < nisps::ChannelStripEngine::kVoiceSpaceCount; ++i) {
m.set_voice_space(i);
NISPS_EXPECT(m.engine().voice_space() ==
static_cast<nisps::ChannelStripEngine::VoiceSpace>(i));
}
}
NISPS_TEST(voice_space_verb_fx_all_dispatch) {
VerbFXMode m;
m.setup(48000.f);
for (std::size_t i = 0u; i < nisps::VerbFXEngine::kVoiceSpaceCount; ++i) {
m.set_voice_space(i);
m.set_input(0, 0.4f);
m.set_input(1, 0.6f);
m.tick_control();
// 64 samples — enough to surface a NaN if one's coming.
for (int n = 0; n < 64; ++n) {
const auto y = m.process({0.1f, -0.1f});
NISPS_EXPECT(std::isfinite(y.L));
NISPS_EXPECT(std::isfinite(y.R));
}
}
}
NISPS_TEST(voice_space_out_of_range_ignored) {
PAFSynthMode m;
m.setup(48000.f);
m.set_voice_space(2u);
NISPS_EXPECT(m.voice_space_index() == 2u);
m.set_voice_space(999u); // out of range — should be a no-op
NISPS_EXPECT(m.voice_space_index() == 2u);
}