"""Tests for the mixer DSP engine components. Tests are designed to run without JACK or Carla — they validate the logic, state management, and parameter dispatch without requiring actual audio hardware. """ from __future__ import annotations import pytest import time import math from src.mixer.osc_client import ( encode_osc, decode_osc, CarlaOSCClient, CarlaPluginInfo, DEFAULT_PLUGIN_LAYOUT, linear_to_db, db_to_linear, freq_to_normalized, normalized_to_freq, time_ms_to_normalized, mix_to_pan, ) from src.mixer.channel_strip import ( ChannelStrip, ChannelState, _apply_state, _transform_value, _get_osc_commands, _normalize_param, ) from src.mixer.routing_matrix import ( RoutingMatrix, RouteNode, RoutingEdge, NodeType, ) from src.mixer.bus_manager import ( BusManager, AuxBus, SubgroupBus, VCAGroup, MasterBus, BusType, ) from src.mixer.fader_automation import ( FaderAutomation, AutomationLane, AutomationPoint, Scene, InterpolationMode, _interpolate, ) from src.mixer.dsp_engine import ( DSPEngine, DSPEngineConfig, create_default_engine, _make_param_key, _parse_param_key, ) from src.midi.types import ( ParameterType, ParameterCategory, MixerParameter, ) # ═════════════════════════════════════════════════════════════════════════════ # OSC Protocol Tests # ═════════════════════════════════════════════════════════════════════════════ class TestOSCEncoding: """Test OSC message encoding/decoding.""" def test_encode_float(self): packet = encode_osc("/test", 1.0) assert b"/test" in packet assert b",f" in packet def test_encode_int(self): packet = encode_osc("/test", 42) assert b"/test" in packet assert b",i" in packet def test_encode_string(self): packet = encode_osc("/test", "hello") assert b"/test" in packet assert b",s" in packet assert b"hello" in packet def test_encode_multiple_args(self): packet = encode_osc("/Carla/1/set_parameter_value", 3, 0, 0.5) assert b"/Carla/1/set_parameter_value" in packet assert b",iif" in packet def test_decode_float(self): packet = encode_osc("/test", 3.14) addr, args = decode_osc(packet) assert addr == "/test" assert len(args) == 1 assert abs(args[0] - 3.14) < 0.001 def test_decode_int(self): packet = encode_osc("/test", 99) addr, args = decode_osc(packet) assert addr == "/test" assert args[0] == 99 def test_decode_string(self): packet = encode_osc("/test", "world") addr, args = decode_osc(packet) assert addr == "/test" assert args[0] == "world" def test_decode_multiple(self): packet = encode_osc("/path", 1.0, 42, "abc") addr, args = decode_osc(packet) assert addr == "/path" assert len(args) == 3 assert abs(args[0] - 1.0) < 0.001 assert args[1] == 42 assert args[2] == "abc" def test_roundtrip(self): """Encode then decode should return the same values.""" original_addr = "/some/path" original_args = [0.75, 128, "test_value"] packet = encode_osc(original_addr, *original_args) addr, args = decode_osc(packet) assert addr == original_addr assert len(args) == len(original_args) for a, b in zip(args, original_args): if isinstance(a, float) and isinstance(b, float): assert abs(a - b) < 0.001 else: assert a == b class TestCarlaOSCClient: """Test the OSC client (without actual network).""" def test_client_creation(self): client = CarlaOSCClient() assert client.host == "127.0.0.1" assert client.port == 22752 client.close() def test_client_context_manager(self): with CarlaOSCClient() as client: assert client.host == "127.0.0.1" def test_stats(self): client = CarlaOSCClient() stats = client.stats assert stats["host"] == "127.0.0.1" assert stats["sends"] == 0 assert stats["errors"] == 0 client.close() # ═════════════════════════════════════════════════════════════════════════════ # Value Conversion Tests # ═════════════════════════════════════════════════════════════════════════════ class TestValueConversions: def test_linear_to_db_silence(self): # With default min_db=-60.0, linear=0 returns -inf (true digital silence) assert linear_to_db(0.0) == float("-inf") def test_linear_to_db_unity(self): assert abs(linear_to_db(0.5) - (-24.0)) < 1.0 def test_linear_to_db_max(self): assert abs(linear_to_db(1.0) - 12.0) < 0.1 def test_db_to_linear_roundtrip(self): # Skip -60 (floor): mathematically db_to_linear(-60)=0.0, linear_to_db(0.0)=-inf for db in [-40, -20, 0, 6, 12]: lin = db_to_linear(db) db2 = linear_to_db(lin) assert abs(db - db2) < 0.5, f"Roundtrip failed at {db} dB" def test_freq_to_normalized(self): assert freq_to_normalized(20) < 0.01 assert freq_to_normalized(20000) > 0.99 assert 0.4 < freq_to_normalized(1000) < 0.6 def test_normalized_to_freq(self): for hz in [50, 200, 1000, 5000, 15000]: norm = freq_to_normalized(hz) hz2 = normalized_to_freq(norm) ratio = hz2 / hz assert 0.8 < ratio < 1.25, f"Failed at {hz} Hz: got {hz2:.0f}" def test_time_ms_to_normalized(self): assert time_ms_to_normalized(0.1) < 0.01 assert time_ms_to_normalized(2000) > 0.99 assert 0.0 < time_ms_to_normalized(100) < 0.5 def test_mix_to_pan_center(self): l, r = mix_to_pan(1.0, 0.0) # mix full, pan center assert abs(l - r) < 0.001 assert abs(l - 0.707) < 0.01 # cos(pi/4) def test_mix_to_pan_left(self): l, r = mix_to_pan(1.0, -1.0) assert l > 0.99 assert r < 0.01 def test_mix_to_pan_right(self): l, r = mix_to_pan(1.0, 1.0) assert l < 0.01 assert r > 0.99 # ═════════════════════════════════════════════════════════════════════════════ # Channel Strip Tests # ═════════════════════════════════════════════════════════════════════════════ class TestChannelStrip: def test_default_state(self): strip = ChannelStrip(index=0) assert strip.index == 0 assert strip.state.volume == 0.0 assert strip.state.mute is False assert strip.state.pan == 0.0 def test_set_volume(self): strip = ChannelStrip(index=0) strip.set_parameter(ParameterType.VOLUME, -6.0) assert strip.state.volume == -6.0 def test_set_mute(self): strip = ChannelStrip(index=0) strip.set_parameter(ParameterType.MUTE, 1.0) assert strip.state.mute is True strip.set_parameter(ParameterType.MUTE, 0.0) assert strip.state.mute is False def test_set_eq(self): strip = ChannelStrip(index=3) strip.set_parameter(ParameterType.EQ_LOW_FREQ, 120.0) strip.set_parameter(ParameterType.EQ_LOW_GAIN, 3.0) strip.set_parameter(ParameterType.EQ_LOW_Q, 1.5) assert strip.state.eq_low_freq == 120.0 assert strip.state.eq_low_gain == 3.0 assert strip.state.eq_low_q == 1.5 def test_set_compressor(self): strip = ChannelStrip(index=1) strip.set_parameter(ParameterType.COMP_THRESHOLD, -25.0) strip.set_parameter(ParameterType.COMP_RATIO, 4.0) strip.set_parameter(ParameterType.COMP_ATTACK, 5.0) assert strip.state.comp_threshold == -25.0 assert strip.state.comp_ratio == 4.0 assert strip.state.comp_attack == 5.0 def test_set_gate(self): strip = ChannelStrip(index=2) strip.set_parameter(ParameterType.GATE_THRESHOLD, -50.0) strip.set_parameter(ParameterType.GATE_RANGE, -70.0) assert strip.state.gate_threshold == -50.0 assert strip.state.gate_range == -70.0 def test_set_fx_sends(self): strip = ChannelStrip(index=0) strip.set_parameter(ParameterType.FX_SEND_A, -10.0) strip.set_parameter(ParameterType.FX_SEND_B, -20.0) assert strip.state.fx_send_a == -10.0 assert strip.state.fx_send_b == -20.0 def test_snapshot_restore(self): strip = ChannelStrip(index=5) strip.set_parameter(ParameterType.VOLUME, -12.0) strip.set_parameter(ParameterType.PAN, 0.5) snap = strip.snapshot() assert snap.volume == -12.0 assert snap.pan == 0.5 # Modify strip.set_parameter(ParameterType.VOLUME, 0.0) assert strip.state.volume == 0.0 # Restore strip.restore(snap, send_osc=False) assert strip.state.volume == -12.0 assert strip.state.pan == 0.5 def test_reset(self): strip = ChannelStrip(index=0) strip.set_parameter(ParameterType.VOLUME, 6.0) strip.set_parameter(ParameterType.MUTE, 1.0) strip.reset() assert strip.state.volume == 0.0 assert strip.state.mute is False def test_has_dsp(self): strip = ChannelStrip(index=0) assert strip.has_dsp is False plugin = CarlaPluginInfo(1, "Test Gate", "gate", 0, {"threshold": 0}) strip.register_plugin(plugin) assert strip.has_dsp is True def test_set_many(self): strip = ChannelStrip(index=0) strip.set_many({ ParameterType.VOLUME: -3.0, ParameterType.PAN: 0.5, ParameterType.MUTE: 1.0, }) assert strip.state.volume == -3.0 assert strip.state.pan == 0.5 assert strip.state.mute is True def test_plugin_registration(self): strip = ChannelStrip(index=3) plugin = CarlaPluginInfo(10, "Ch4 EQ", "eq", 3, {"low_freq": 0, "low_gain": 1}) strip.register_plugin(plugin) assert "eq" in strip._plugins assert strip._plugins["eq"].plugin_id == 10 # ═════════════════════════════════════════════════════════════════════════════ # Routing Matrix Tests # ═════════════════════════════════════════════════════════════════════════════ class TestRoutingMatrix: def test_add_node(self): rm = RoutingMatrix() node = rm.add_node(RouteNode("test", NodeType.CHANNEL_INPUT, "Test")) assert rm.get_node("test") is node def test_connect(self): rm = RoutingMatrix() src = rm.add_node(RouteNode("src", NodeType.SYSTEM_INPUT)) dst = rm.add_node(RouteNode("dst", NodeType.CHANNEL_INPUT)) edge = rm.connect("src", "dst", gain_db=-6.0, apply_jack=False) assert edge is not None assert edge.gain_db == -6.0 def test_connect_nonexistent(self): rm = RoutingMatrix() edge = rm.connect("a", "b") assert edge is None def test_disconnect(self): rm = RoutingMatrix() rm.add_node(RouteNode("src", NodeType.SYSTEM_INPUT)) rm.add_node(RouteNode("dst", NodeType.CHANNEL_INPUT)) rm.connect("src", "dst", apply_jack=False) assert len(rm._edges) == 1 assert rm.disconnect("src", "dst", apply_jack=False) assert len(rm._edges) == 0 def test_set_gain(self): rm = RoutingMatrix() rm.add_node(RouteNode("src", NodeType.SYSTEM_INPUT)) rm.add_node(RouteNode("dst", NodeType.CHANNEL_INPUT)) rm.connect("src", "dst", apply_jack=False) assert rm.set_gain("src", "dst", -12.0) edge = rm.get_edges("src")[0] assert edge.gain_db == -12.0 def test_set_mute(self): rm = RoutingMatrix() rm.add_node(RouteNode("src", NodeType.SYSTEM_INPUT)) rm.add_node(RouteNode("dst", NodeType.CHANNEL_INPUT)) rm.connect("src", "dst", apply_jack=False) rm.set_mute("src", "dst", True) edge = rm.get_edges("src")[0] assert edge.muted is True def test_solo_logic(self): rm = RoutingMatrix() src1 = rm.add_node(RouteNode("src1", NodeType.CHANNEL_INPUT)) src2 = rm.add_node(RouteNode("src2", NodeType.CHANNEL_INPUT)) dst = rm.add_node(RouteNode("dst", NodeType.MASTER_INPUT)) rm.connect("src1", "dst", apply_jack=False) rm.connect("src2", "dst", apply_jack=False) rm.set_solo("src1", True) edges = rm.get_edges() edge1 = [e for e in edges if e.source.node_id == "src1"][0] edge2 = [e for e in edges if e.source.node_id == "src2"][0] assert edge1.soloed is True assert edge2.soloed is False rm.clear_solo() edges = rm.get_edges() assert all(not e.soloed for e in edges) def test_remove_node_cleans_edges(self): rm = RoutingMatrix() rm.add_node(RouteNode("src", NodeType.SYSTEM_INPUT)) rm.add_node(RouteNode("dst", NodeType.CHANNEL_INPUT)) rm.connect("src", "dst", apply_jack=False) assert len(rm._edges) == 1 rm.remove_node("src") assert len(rm._edges) == 0 assert rm.get_node("src") is None def test_build_default_8ch(self): rm = RoutingMatrix() rm.build_default_8ch_matrix() # Should have system inputs (8), system outputs (2), channel nodes (16), # aux nodes (8: 4 buses × 2), subgroups (2), master (3) assert len(rm._nodes) >= 8 + 2 # Should have connections: 8 sys_in → channels, 8 channels → master, 2 master → sys_out assert len(rm._edges) >= 8 + 8 + 2 def test_serialization_roundtrip(self): rm = RoutingMatrix() rm.add_node(RouteNode("src", NodeType.SYSTEM_INPUT, "Input")) rm.add_node(RouteNode("dst", NodeType.CHANNEL_INPUT, "Channel")) rm.connect("src", "dst", gain_db=-3.0, apply_jack=False) data = rm.to_dict() rm2 = RoutingMatrix() rm2.from_dict(data) assert len(rm2._nodes) == 2 assert len(rm2._edges) == 1 assert rm2.get_node("src") is not None assert rm2._edges[0].gain_db == -3.0 # ═════════════════════════════════════════════════════════════════════════════ # Bus Manager Tests # ═════════════════════════════════════════════════════════════════════════════ class TestBusManager: def test_default_initialization(self): bm = BusManager(num_channels=16, num_aux=4, num_subgroups=2, num_vca=2) assert len(bm.aux_buses) == 4 assert len(bm.subgroups) == 2 assert len(bm.vca_groups) == 2 assert bm.master.volume_db == 0.0 def test_aux_send(self): bm = BusManager(num_channels=8, num_aux=4) bm.set_aux_send(0, 3, -10.0) aux = bm.get_aux(0) assert aux.get_send(3) == -10.0 def test_aux_return(self): bm = BusManager(num_channels=8, num_aux=4) bm.set_aux_return(0, -5.0) assert bm.get_aux(0).return_gain_db == -5.0 def test_aux_mute(self): bm = BusManager(num_channels=8, num_aux=4) bm.set_aux_mute(1, True) assert bm.get_aux(1).muted is True def test_aux_out_of_range(self): bm = BusManager(num_channels=8, num_aux=2) assert bm.get_aux(5) is None def test_subgroup_membership(self): bm = BusManager(num_channels=8, num_aux=2, num_subgroups=2) assert bm.subgroup_add_channel(0, 3) assert 3 in bm.subgroups[0].members assert bm.subgroup_remove_channel(0, 3) assert 3 not in bm.subgroups[0].members def test_vca_membership(self): bm = BusManager(num_channels=8, num_aux=2, num_vca=2) bm.vca_add_channel(0, 5) bm.vca_add_channel(1, 5) vcas = bm.get_channel_vcas(5) assert len(vcas) == 2 def test_vca_offset(self): bm = BusManager(num_channels=8, num_vca=2) bm.vca_add_channel(0, 2) vca = bm.get_vca(0) vca.master_db = -5.0 assert bm.get_channel_vca_offset(2) == -5.0 def test_vca_multiple_offset(self): bm = BusManager(num_channels=8, num_vca=2) bm.vca_add_channel(0, 2) bm.vca_add_channel(1, 2) bm.get_vca(0).master_db = -3.0 bm.get_vca(1).master_db = -2.0 assert bm.get_channel_vca_offset(2) == -5.0 def test_vca_muted_offset(self): bm = BusManager(num_channels=8, num_vca=2) bm.vca_add_channel(0, 2) bm.get_vca(0).master_db = -5.0 bm.get_vca(0).muted = True assert bm.get_channel_vca_offset(2) == 0.0 # muted VCA doesn't contribute def test_master_operations(self): bm = BusManager() bm.set_master_volume(-10.0) bm.set_master_mute(True) bm.set_master_dim(True) assert bm.master.volume_db == -10.0 assert bm.master.muted is True assert bm.master.dim_active is True # Effective gain with mute = 0 assert bm.master.effective_gain == 0.0 def test_master_dim_gain(self): bm = BusManager() bm.master.dim_db = -20.0 bm.master.dim_active = True # -20 dB dim assert abs(bm.master.effective_gain - 0.1) < 0.001 def test_serialization_roundtrip(self): bm = BusManager(num_channels=4, num_aux=2, num_subgroups=1, num_vca=1) bm.set_aux_send(0, 2, -6.0) bm.subgroup_add_channel(0, 1) bm.vca_add_channel(0, 0) bm.get_vca(0).master_db = -3.0 bm.set_master_volume(-12.0) data = bm.to_dict() bm2 = BusManager(num_channels=4, num_aux=2, num_subgroups=1, num_vca=1) bm2.from_dict(data) assert bm2.get_aux(0).get_send(2) == -6.0 assert 1 in bm2.subgroups[0].members assert 0 in bm2.vca_groups[0].members assert bm2.master.volume_db == -12.0 # ═════════════════════════════════════════════════════════════════════════════ # Fader Automation Tests # ═════════════════════════════════════════════════════════════════════════════ class TestInterpolation: def test_linear(self): assert _interpolate(0.0, 0, 10, InterpolationMode.LINEAR) == 0 assert _interpolate(0.5, 0, 10, InterpolationMode.LINEAR) == 5 assert _interpolate(1.0, 0, 10, InterpolationMode.LINEAR) == 10 def test_s_curve(self): assert _interpolate(0.0, 0, 10, InterpolationMode.S_CURVE) == 0 assert _interpolate(0.5, 0, 10, InterpolationMode.S_CURVE) == 5 # smoothstep at 0.5 = 0.5 assert _interpolate(1.0, 0, 10, InterpolationMode.S_CURVE) == 10 def test_logarithmic(self): v = _interpolate(0.5, 1, 100, InterpolationMode.LOGARITHMIC) assert 5 < v < 15 # roughly sqrt(100) = 10 def test_clamp(self): assert _interpolate(-0.5, 0, 10, InterpolationMode.LINEAR) == 0 assert _interpolate(1.5, 0, 10, InterpolationMode.LINEAR) == 10 class TestAutomationLane: def test_add_point_sorted(self): lane = AutomationLane("test") lane.add_point(2.0, 0.5) lane.add_point(1.0, 0.2) lane.add_point(3.0, 0.8) assert [p.time_sec for p in lane.points] == [1.0, 2.0, 3.0] def test_get_value_at_exact(self): lane = AutomationLane("test") lane.add_point(1.0, 0.2) lane.add_point(2.0, 0.8) assert abs(lane.get_value_at(1.5) - 0.5) < 0.01 def test_get_value_at_before(self): lane = AutomationLane("test") lane.add_point(1.0, 0.2) assert abs(lane.get_value_at(0.0) - 0.2) < 0.01 def test_get_value_at_after(self): lane = AutomationLane("test") lane.add_point(1.0, 0.2) lane.add_point(2.0, 0.8) assert abs(lane.get_value_at(3.0) - 0.8) < 0.01 def test_get_value_empty(self): lane = AutomationLane("test") assert lane.get_value_at(1.0, default=-1.0) == -1.0 def test_clone(self): lane = AutomationLane("test") lane.add_point(1.0, 0.5) clone = lane.clone() clone.points[0].value = 0.9 assert lane.points[0].value == 0.5 # original unchanged class TestFaderAutomation: def test_recording(self): fa = FaderAutomation() fa.start_recording() assert fa.is_recording time.sleep(0.01) fa.record("test_param", 0.5) fa.stop_recording() assert not fa.is_recording assert "test_param" in fa._lanes assert len(fa._lanes["test_param"].points) > 0 def test_not_recording_when_stopped(self): fa = FaderAutomation() fa.record("test", 0.5) assert "test" not in fa._lanes def test_playback(self): fa = FaderAutomation() fa.start_recording() fa.record("vol", 0.0) time.sleep(0.02) fa.record("vol", 1.0) fa.stop_recording() lane = fa._lanes["vol"] assert len(lane.points) == 2 def test_scene_save_load(self): fa = FaderAutomation() scene = fa.save_scene("TestScene", {0: {"volume": -6.0}}, {}, {}) assert "TestScene" in fa.list_scenes() loaded = fa.load_scene("TestScene") assert loaded.name == "TestScene" def test_scene_delete(self): fa = FaderAutomation() fa.save_scene("DeleteMe", {}, {}, {}) assert fa.delete_scene("DeleteMe") assert "DeleteMe" not in fa.list_scenes() def test_scene_recall(self): fa = FaderAutomation() fa.start_recording() fa.record("param", 0.25) fa.stop_recording() fa.save_scene("S1", {0: {"volume": -6.0}}, {}, {}) fa.recall_scene("S1") assert fa._current_scene == "S1" def test_crossfade(self): fa = FaderAutomation() fa.start_recording() fa.record("param", 0.0) fa.stop_recording() fa.save_scene("Target", {}, {}, {}) # Start very short crossfade result = fa.crossfade_to("Target", 0.05) assert result is True assert fa._crossfade_active # Let it complete time.sleep(0.1) fa.update_crossfade() assert not fa._crossfade_active def test_crossfade_missing_scene(self): fa = FaderAutomation() assert fa.crossfade_to("Nonexistent", 1.0) is False def test_cancel_crossfade(self): fa = FaderAutomation() fa.start_recording() fa.record("param", 0.0) fa.stop_recording() fa.save_scene("Target", {}, {}, {}) fa.crossfade_to("Target", 10.0) assert fa._crossfade_active fa.cancel_crossfade() assert not fa._crossfade_active def test_seek(self): fa = FaderAutomation() fa.start_recording() fa.record("param", 0.0) time.sleep(0.02) fa.record("param", 1.0) fa.stop_recording() fa.start_playback() fa.seek(1000.0) # seek way past val = fa.get_value("param") assert abs(val - 1.0) < 0.01 def test_get_duration(self): fa = FaderAutomation() fa.start_recording() fa.record("a", 0.0) time.sleep(0.05) fa.record("a", 0.5) time.sleep(0.05) fa.record("b", 1.0) fa.stop_recording() duration = fa.get_duration() assert duration > 0.08 # at least 100ms total def test_serialization_roundtrip(self): fa = FaderAutomation() fa.start_recording() fa.record("param1", 0.3) fa.stop_recording() fa.save_scene("Test", {}, {}, {}) data = fa.to_dict() fa2 = FaderAutomation() fa2.from_dict(data) assert len(fa2._lanes) == 1 assert "Test" in fa2.list_scenes() # ═════════════════════════════════════════════════════════════════════════════ # DSP Engine Tests # ═════════════════════════════════════════════════════════════════════════════ class TestDSPEngine: def test_create_default_engine(self): engine = create_default_engine(num_channels=8, osc_enabled=False) assert len(engine.channels) == 8 assert engine.config.num_aux == 4 assert not engine.running def test_start_stop(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) assert engine.running engine.stop() assert not engine.running def test_handle_channel_volume(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) param = MixerParameter( param_type=ParameterType.VOLUME, category=ParameterCategory.CHANNEL, channel=2, ) engine.handle_parameter(param, -12.0) assert engine.channels[2].state.volume == -12.0 def test_handle_mute(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) param = MixerParameter( param_type=ParameterType.MUTE, category=ParameterCategory.CHANNEL, channel=0, ) engine.handle_parameter(param, 1.0) assert engine.channels[0].state.mute is True def test_handle_master_volume(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) param = MixerParameter( param_type=ParameterType.MASTER_VOLUME, category=ParameterCategory.MASTER, channel=-1, ) engine.handle_parameter(param, -20.0) assert engine.buses.master.volume_db == -20.0 def test_handle_fx_return(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) param = MixerParameter( param_type=ParameterType.FX_RETURN_A, category=ParameterCategory.FX, channel=-1, ) engine.handle_parameter(param, -6.0) assert engine.buses.get_aux(0).return_gain_db == -6.0 def test_handle_eq_params(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) param = MixerParameter( param_type=ParameterType.EQ_MID_GAIN, category=ParameterCategory.CHANNEL, channel=1, ) engine.handle_parameter(param, 4.0) assert engine.channels[1].state.eq_mid_gain == 4.0 def test_snapshot_save_load(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) # Set some state engine.channels[0].set_parameter(ParameterType.VOLUME, -6.0) engine.buses.set_master_volume(-3.0) # Save engine.save_snapshot("test_snap") # Modify engine.channels[0].set_parameter(ParameterType.VOLUME, 0.0) engine.buses.set_master_volume(0.0) # Load assert engine.load_snapshot("test_snap") assert engine.channels[0].state.volume == -6.0 assert engine.buses.master.volume_db == -3.0 def test_param_key_helpers(self): key = _make_param_key(ParameterCategory.CHANNEL, 3, ParameterType.VOLUME) cat, ch, pt = _parse_param_key(key) assert cat == ParameterCategory.CHANNEL assert ch == 3 assert pt == ParameterType.VOLUME def test_param_key_master(self): key = _make_param_key(ParameterCategory.MASTER, -1, ParameterType.MASTER_VOLUME) cat, ch, pt = _parse_param_key(key) assert cat == ParameterCategory.MASTER assert ch == -1 assert pt == ParameterType.MASTER_VOLUME def test_stats(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) stats = engine.stats assert stats["running"] assert stats["num_channels"] == 4 assert not stats["osc_connected"] def test_to_dict_from_dict(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) engine.channels[0].set_parameter(ParameterType.VOLUME, -10.0) data = engine.to_dict() engine2 = create_default_engine(num_channels=4, osc_enabled=False) engine2.from_dict(data) assert engine2.channels[0].state.volume == -10.0 def test_next_prev_scene(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) engine.save_snapshot("A") engine.save_snapshot("B") engine.save_snapshot("C") assert engine.load_snapshot("B") assert engine.automation._current_scene == "B" # Bound to the scenes assert engine.next_scene() # goes to C assert engine.next_scene() # wraps to A assert engine.prev_scene() # goes to C def test_automation_recording_handling(self): engine = create_default_engine(num_channels=4, osc_enabled=False) engine.start(connect_osc=False) engine.automation.start_recording() param = MixerParameter( param_type=ParameterType.VOLUME, category=ParameterCategory.CHANNEL, channel=0, ) engine.handle_parameter(param, -5.0) key = _make_param_key(ParameterCategory.CHANNEL, 0, ParameterType.VOLUME) assert key in engine.automation._lanes # ═════════════════════════════════════════════════════════════════════════════ # Integration Tests # ═════════════════════════════════════════════════════════════════════════════ class TestIntegration: def test_full_signal_chain_state(self): """Test that the full signal chain can be configured through the engine.""" engine = create_default_engine(num_channels=16, osc_enabled=False) engine.start(connect_osc=False) # Configure channel 0 params_to_set = [ (ParameterType.GAIN, 20.0), (ParameterType.EQ_LOW_FREQ, 80.0), (ParameterType.EQ_LOW_GAIN, 3.0), (ParameterType.EQ_MID_FREQ, 2500.0), (ParameterType.EQ_MID_GAIN, -2.0), (ParameterType.EQ_HIGH_FREQ, 8000.0), (ParameterType.EQ_HIGH_GAIN, 1.5), (ParameterType.COMP_THRESHOLD, -18.0), (ParameterType.COMP_RATIO, 3.0), (ParameterType.GATE_THRESHOLD, -45.0), (ParameterType.FX_SEND_A, -8.0), (ParameterType.VOLUME, -3.0), (ParameterType.PAN, 0.2), ] for pt, val in params_to_set: param = MixerParameter(pt, ParameterCategory.CHANNEL, 0) engine.handle_parameter(param, val) ch0 = engine.channels[0] assert ch0.state.gain == 20.0 assert ch0.state.eq_low_freq == 80.0 assert ch0.state.comp_threshold == -18.0 assert ch0.state.volume == -3.0 def test_multiple_channels_independent(self): """Test that channels maintain independent state.""" engine = create_default_engine(num_channels=8, osc_enabled=False) engine.start(connect_osc=False) # Set different volumes on different channels for ch, vol in [(0, -6.0), (3, 0.0), (7, 6.0)]: param = MixerParameter(ParameterType.VOLUME, ParameterCategory.CHANNEL, ch) engine.handle_parameter(param, vol) assert engine.channels[0].state.volume == -6.0 assert engine.channels[3].state.volume == 0.0 assert engine.channels[7].state.volume == 6.0 assert engine.channels[1].state.volume == 0.0 # unchanged def test_bus_routing_persistence(self): """Test that bus state persists across snapshot save/load.""" engine = create_default_engine(num_channels=8, osc_enabled=False) engine.start(connect_osc=False) # Set up complex bus state engine.buses.set_aux_send(0, 1, -12.0) engine.buses.set_aux_send(2, 3, -4.0) engine.buses.subgroup_add_channel(0, 0) engine.buses.subgroup_add_channel(0, 4) engine.buses.vca_add_channel(0, 2) engine.buses.get_vca(0).master_db = -5.0 # Save and verify restore snap = engine.save_snapshot("complex") engine.load_snapshot("complex") assert engine.buses.get_aux(0).get_send(1) == -12.0 assert engine.buses.get_aux(2).get_send(3) == -4.0 assert 0 in engine.buses.subgroups[0].members assert 2 in engine.buses.vca_groups[0].members