// tests/cpp/test_mode_driver_config.cpp — the mic/line wiring, minus the // hardware. // // Two halves, both of which are the parts that can actually be wrong: // // 1. SELECTION — what `Mode::driver_config()` resolves to per mode. This is // the logic the firmware reads at mode start (src/main.cpp → // glue/audio_driver.hpp). The interesting case is SoundAnalysisMIDIMode, // whose audio ENGINE is a silent NoOp while its AnalysisEngine is what // actually consumes the microphone; taking the engine's config there // would silently leave the codec on line input and defeat the mode. // // 2. CLAMPING — `glue/codec_config.hpp`, the only firmware-side logic in the // chain. Included directly (it is deliberately Arduino-free) so the // sample-rate resolution, which can `panic()` the device if it hands // `AudioDriver::GetSysClockSpeed()` a rate it has no divider for, is // pinned on the host. // // What is NOT proven here: that the SGTL5000 actually switches its input mux. // That needs hardware. #include "test_helpers.hpp" #include "../../nisps/modes/breakor.hpp" #include "../../nisps/modes/channel_strip.hpp" #include "../../nisps/modes/elysiamorf.hpp" #include "../../nisps/modes/external_synth_midi.hpp" #include "../../nisps/modes/memlcelium.hpp" #include "../../nisps/modes/paf_synth.hpp" #include "../../nisps/modes/slp_workshop.hpp" #include "../../nisps/modes/sound_analysis_midi.hpp" #include "../../nisps/modes/verb_fx.hpp" #include "../../nisps/modes/xiasri.hpp" #include "../../firmware/MEMLNaut-NISPS/glue/codec_config.hpp" using namespace nisps; // --------------------------------------------------------------------------- // 1. Per-mode selection // --------------------------------------------------------------------------- // The mode that exists to listen: mic input, with the analyser's pre-amp gain. NISPS_TEST(driver_config_sound_analysis_midi_selects_mic) { const modes::SoundAnalysisMIDIMode mode{}; const DriverConfig c = mode.driver_config(); NISPS_EXPECT(c.mic_input == true); NISPS_EXPECT(c.mic_gain_db == 20u); // ...and it must NOT be the (NoOp) engine's config, which is line input. NISPS_EXPECT(mode.engine().driver_config().mic_input == false); // Same object the mode delegates to. NISPS_EXPECT(c.mic_input == mode.analysis().driver_config().mic_input); NISPS_EXPECT(c.mic_gain_db == mode.analysis().driver_config().mic_gain_db); } // The two input-processing modes: line input, at the level their engines ask // for (louder than the default step, i.e. a real instrument/line source). NISPS_TEST(driver_config_channel_strip_selects_line) { const modes::ChannelStripMode mode{}; const DriverConfig c = mode.driver_config(); NISPS_EXPECT(c.mic_input == false); NISPS_EXPECT(c.line_level == 6u); NISPS_EXPECT_NEAR(c.output_volume, 0.9f, 1e-6f); } NISPS_TEST(driver_config_verb_fx_selects_line) { const modes::VerbFXMode mode{}; const DriverConfig c = mode.driver_config(); NISPS_EXPECT(c.mic_input == false); NISPS_EXPECT(c.line_level == 6u); NISPS_EXPECT_NEAR(c.output_volume, 0.9f, 1e-6f); } NISPS_TEST(driver_config_xiasri_selects_line) { const DriverConfig c = modes::XIASRIMode{}.driver_config(); NISPS_EXPECT(c.mic_input == false); NISPS_EXPECT(c.line_level == 6u); } // Synth modes: no input opinion beyond the default, own output level. NISPS_TEST(driver_config_synth_modes_default_line) { for (const DriverConfig c : {modes::PAFSynthMode{}.driver_config(), modes::MEMLCeliumMode{}.driver_config(), modes::SLPWorkshopMode{}.driver_config()}) { NISPS_EXPECT(c.mic_input == false); NISPS_EXPECT(c.line_level == DriverConfig{}.line_level); NISPS_EXPECT_NEAR(c.output_volume, 0.9f, 1e-6f); } } // Modes that say nothing get exactly the struct defaults — which are, by // construction, the codec setup the firmware used before any of this was // wired. Wiring them up is a no-op, not a silent behaviour change. NISPS_TEST(driver_config_silent_modes_get_defaults) { const DriverConfig def{}; const modes::BreakOrMode breakor{}; const modes::ElysiamorfMode elysiamorf{}; const modes::ExternalSynthMIDIMode ext{}; for (const DriverConfig c : {breakor.driver_config(), elysiamorf.driver_config(), ext.driver_config()}) { NISPS_EXPECT(c.mic_input == def.mic_input); NISPS_EXPECT(c.line_level == def.line_level); NISPS_EXPECT(c.mic_gain_db == def.mic_gain_db); NISPS_EXPECT_NEAR(c.output_volume, def.output_volume, 1e-6f); NISPS_EXPECT_NEAR(c.sample_rate, 0.f, 1e-6f); } } // The defaults are load-bearing (see above): pin them to memllib's historical // `AudioDriver::Setup()` values so a future edit has to be deliberate. NISPS_TEST(driver_config_defaults_match_legacy_firmware_setup) { const DriverConfig def{}; NISPS_EXPECT(def.mic_input == false); NISPS_EXPECT(def.line_level == 3u); NISPS_EXPECT(def.mic_gain_db == 0u); NISPS_EXPECT_NEAR(def.output_volume, 0.8f, 1e-6f); NISPS_EXPECT_NEAR(def.sample_rate, 0.f, 1e-6f); } // No mode may request a rate the driver cannot clock (that would panic() at // boot). Every mode currently says "don't care"; this holds the line. NISPS_TEST(driver_config_every_mode_requests_a_clockable_rate) { const float rates[] = { modes::PAFSynthMode{}.driver_config().sample_rate, modes::ChannelStripMode{}.driver_config().sample_rate, modes::XIASRIMode{}.driver_config().sample_rate, modes::VerbFXMode{}.driver_config().sample_rate, modes::MEMLCeliumMode{}.driver_config().sample_rate, modes::SLPWorkshopMode{}.driver_config().sample_rate, modes::BreakOrMode{}.driver_config().sample_rate, modes::ElysiamorfMode{}.driver_config().sample_rate, modes::SoundAnalysisMIDIMode{}.driver_config().sample_rate, }; for (const float r : rates) { const std::uint32_t resolved = nisps_firmware::select_sample_rate(r); NISPS_EXPECT(r <= 0.f || resolved == static_cast(r + 0.5f)); } } // --------------------------------------------------------------------------- // 2. Firmware-side clamping (glue/codec_config.hpp) // --------------------------------------------------------------------------- NISPS_TEST(codec_config_clamp_passes_through_valid_values) { DriverConfig in{}; in.mic_input = true; in.mic_gain_db = 20u; in.line_level = 6u; in.output_volume = 0.9f; const DriverConfig c = nisps_firmware::clamp_driver_config(in); NISPS_EXPECT(c.mic_input == true); NISPS_EXPECT(c.mic_gain_db == 20u); NISPS_EXPECT(c.line_level == 6u); NISPS_EXPECT_NEAR(c.output_volume, 0.9f, 1e-6f); } NISPS_TEST(codec_config_clamp_bounds_out_of_range_values) { DriverConfig hi{}; hi.mic_gain_db = 200u; hi.line_level = 99u; hi.output_volume = 4.f; const DriverConfig c = nisps_firmware::clamp_driver_config(hi); NISPS_EXPECT(c.mic_gain_db == nisps_firmware::kMaxMicGainDb); NISPS_EXPECT(c.line_level == nisps_firmware::kMaxLineLevel); NISPS_EXPECT_NEAR(c.output_volume, nisps_firmware::kMaxOutputVolume, 1e-6f); DriverConfig lo{}; lo.output_volume = -1.f; NISPS_EXPECT_NEAR(nisps_firmware::clamp_driver_config(lo).output_volume, 0.f, 1e-6f); } NISPS_TEST(codec_config_sample_rate_dont_care_is_48k) { NISPS_EXPECT(nisps_firmware::select_sample_rate(0.f) == 48000u); NISPS_EXPECT(nisps_firmware::select_sample_rate(-1.f) == 48000u); } NISPS_TEST(codec_config_sample_rate_supported_rates_pass_through) { NISPS_EXPECT(nisps_firmware::select_sample_rate(24000.f) == 24000u); NISPS_EXPECT(nisps_firmware::select_sample_rate(32000.f) == 32000u); NISPS_EXPECT(nisps_firmware::select_sample_rate(44100.f) == 44100u); NISPS_EXPECT(nisps_firmware::select_sample_rate(48000.f) == 48000u); } // The one that keeps the device bootable: GetSysClockSpeed() panics on these. NISPS_TEST(codec_config_sample_rate_unsupported_falls_back_to_48k) { NISPS_EXPECT(nisps_firmware::select_sample_rate(96000.f) == 48000u); NISPS_EXPECT(nisps_firmware::select_sample_rate(22050.f) == 48000u); NISPS_EXPECT(nisps_firmware::select_sample_rate(1.f) == 48000u); NISPS_EXPECT(nisps_firmware::select_sample_rate(48001.f) == 48000u); }