Skip to content

Commit ab7ab29

Browse files
authored
test(audio+store): add pitch, transport and mixer reducer tests (#16)
* test(audio): add pitch playback rate tests * test(store): add transport reducer tests for BPM, playback and playhead * test(store): add mixer reducer tests for inserts, FX slots and meters
1 parent 63abded commit ab7ab29

3 files changed

Lines changed: 323 additions & 0 deletions

File tree

src/audio/domain/pitch.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
midiPitchToPlaybackRate,
4+
DEFAULT_SAMPLE_MIDI_PITCH,
5+
} from "./pitch";
6+
import { MIN_PLAYBACK_RATE, MAX_PLAYBACK_RATE } from "./constants";
7+
8+
describe("midiPitchToPlaybackRate", () => {
9+
it("returns 1 for default pitch (72)", () => {
10+
expect(midiPitchToPlaybackRate(DEFAULT_SAMPLE_MIDI_PITCH)).toBe(1);
11+
});
12+
13+
it("returns 1 for null/undefined", () => {
14+
expect(midiPitchToPlaybackRate(null)).toBe(1);
15+
expect(midiPitchToPlaybackRate(undefined)).toBe(1);
16+
});
17+
18+
it("doubles rate for +12 semitones", () => {
19+
expect(midiPitchToPlaybackRate(84)).toBeCloseTo(2, 5);
20+
});
21+
22+
it("halves rate for -12 semitones", () => {
23+
expect(midiPitchToPlaybackRate(60)).toBeCloseTo(0.5, 5);
24+
});
25+
26+
it("returns 2^(1/12) for +1 semitone", () => {
27+
expect(midiPitchToPlaybackRate(73)).toBeCloseTo(Math.pow(2, 1 / 12), 5);
28+
});
29+
30+
it("clamps to MIN_PLAYBACK_RATE for very low pitches", () => {
31+
expect(midiPitchToPlaybackRate(-100)).toBe(MIN_PLAYBACK_RATE);
32+
});
33+
34+
it("clamps to MAX_PLAYBACK_RATE for very high pitches", () => {
35+
expect(midiPitchToPlaybackRate(200)).toBe(MAX_PLAYBACK_RATE);
36+
});
37+
});

src/store/reducers/mixer.test.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { describe, it, expect } from "vitest";
2+
import { mixerReducers } from "./mixer";
3+
4+
function createMixerState(inserts = []) {
5+
return {
6+
mixer: {
7+
inserts,
8+
selectedInsertId: inserts[0]?.id || null,
9+
},
10+
};
11+
}
12+
13+
function makeInsert(id) {
14+
return {
15+
id,
16+
name: "Insert " + id,
17+
isMaster: false,
18+
active: true,
19+
pan: 0,
20+
stereoSeparation: 0,
21+
fader: 1,
22+
meter: 0,
23+
meterSpectrum: [],
24+
meterWaveform: [],
25+
maximizerReduction: 0,
26+
maximizerOutputDb: -96,
27+
maximizerStereoMeter: {},
28+
routesTo: ["master"],
29+
fxSlots: Array.from({ length: 10 }, (_, i) => ({
30+
id: `slot-${i + 1}`,
31+
name: `Slot ${i + 1}`,
32+
enabled: false,
33+
effectType: "none",
34+
params: null,
35+
})),
36+
};
37+
}
38+
39+
describe("mixerReducers", () => {
40+
describe("selectInsert", () => {
41+
it("sets selected insert", () => {
42+
const state = createMixerState([makeInsert("insert-1")]);
43+
mixerReducers.selectInsert(state, { payload: "insert-1" });
44+
expect(state.mixer.selectedInsertId).toBe("insert-1");
45+
});
46+
});
47+
48+
describe("addMixerTrack", () => {
49+
it("adds a new insert with incremented number", () => {
50+
const state = createMixerState([makeInsert("insert-1")]);
51+
mixerReducers.addMixerTrack(state);
52+
expect(state.mixer.inserts.length).toBe(2);
53+
expect(state.mixer.inserts[1].id).toBe("insert-2");
54+
expect(state.mixer.inserts[1].fxSlots.length).toBe(10);
55+
expect(state.mixer.selectedInsertId).toBe("insert-2");
56+
});
57+
58+
it("creates insert-1 when list is empty", () => {
59+
const state = createMixerState([]);
60+
mixerReducers.addMixerTrack(state);
61+
expect(state.mixer.inserts[0].id).toBe("insert-1");
62+
});
63+
});
64+
65+
describe("setInsertActive", () => {
66+
it("toggles insert active state", () => {
67+
const state = createMixerState([makeInsert("insert-1")]);
68+
mixerReducers.setInsertActive(state, { payload: { insertId: "insert-1", value: false } });
69+
expect(state.mixer.inserts[0].active).toBe(false);
70+
});
71+
72+
it("ignores missing insert", () => {
73+
const state = createMixerState([makeInsert("insert-1")]);
74+
mixerReducers.setInsertActive(state, { payload: { insertId: "missing", value: false } });
75+
expect(state.mixer.inserts[0].active).toBe(true);
76+
});
77+
});
78+
79+
describe("setInsertPan", () => {
80+
it("sets pan within [-1, 1]", () => {
81+
const state = createMixerState([makeInsert("insert-1")]);
82+
mixerReducers.setInsertPan(state, { payload: { insertId: "insert-1", value: 0.5 } });
83+
expect(state.mixer.inserts[0].pan).toBe(0.5);
84+
});
85+
86+
it("clamps pan to [-1, 1]", () => {
87+
const state = createMixerState([makeInsert("insert-1")]);
88+
mixerReducers.setInsertPan(state, { payload: { insertId: "insert-1", value: 2 } });
89+
expect(state.mixer.inserts[0].pan).toBe(1);
90+
});
91+
});
92+
93+
describe("setInsertStereo", () => {
94+
it("sets stereo separation", () => {
95+
const state = createMixerState([makeInsert("insert-1")]);
96+
mixerReducers.setInsertStereo(state, { payload: { insertId: "insert-1", value: -0.5 } });
97+
expect(state.mixer.inserts[0].stereoSeparation).toBe(-0.5);
98+
});
99+
});
100+
101+
describe("setInsertFader", () => {
102+
it("sets fader within [0, 1.25]", () => {
103+
const state = createMixerState([makeInsert("insert-1")]);
104+
mixerReducers.setInsertFader(state, { payload: { insertId: "insert-1", value: 0.8 } });
105+
expect(state.mixer.inserts[0].fader).toBe(0.8);
106+
});
107+
108+
it("clamps fader to max 1.25", () => {
109+
const state = createMixerState([makeInsert("insert-1")]);
110+
mixerReducers.setInsertFader(state, { payload: { insertId: "insert-1", value: 2 } });
111+
expect(state.mixer.inserts[0].fader).toBe(1.25);
112+
});
113+
});
114+
115+
describe("toggleFxSlot", () => {
116+
it("does nothing for empty slot", () => {
117+
const state = createMixerState([makeInsert("insert-1")]);
118+
mixerReducers.toggleFxSlot(state, { payload: { insertId: "insert-1", slotId: "slot-1" } });
119+
expect(state.mixer.inserts[0].fxSlots[0].enabled).toBe(false);
120+
});
121+
122+
it("toggles enabled for loaded effect", () => {
123+
const state = createMixerState([makeInsert("insert-1")]);
124+
state.mixer.inserts[0].fxSlots[0].effectType = "graphic-eq";
125+
mixerReducers.toggleFxSlot(state, { payload: { insertId: "insert-1", slotId: "slot-1" } });
126+
expect(state.mixer.inserts[0].fxSlots[0].enabled).toBe(true);
127+
mixerReducers.toggleFxSlot(state, { payload: { insertId: "insert-1", slotId: "slot-1" } });
128+
expect(state.mixer.inserts[0].fxSlots[0].enabled).toBe(false);
129+
});
130+
});
131+
132+
describe("setFxSlotEffectType", () => {
133+
it("sets effect type and name for Graphic EQ", () => {
134+
const state = createMixerState([makeInsert("insert-1")]);
135+
mixerReducers.setFxSlotEffectType(state, {
136+
payload: { insertId: "insert-1", slotId: "slot-1", effectType: "graphic-eq" },
137+
});
138+
expect(state.mixer.inserts[0].fxSlots[0].effectType).toBe("graphic-eq");
139+
expect(state.mixer.inserts[0].fxSlots[0].name).toBe("Graphic EQ");
140+
});
141+
142+
it("clears effect when type is none", () => {
143+
const state = createMixerState([makeInsert("insert-1")]);
144+
state.mixer.inserts[0].fxSlots[0].effectType = "reverb";
145+
mixerReducers.setFxSlotEffectType(state, {
146+
payload: { insertId: "insert-1", slotId: "slot-1", effectType: "none" },
147+
});
148+
expect(state.mixer.inserts[0].fxSlots[0].effectType).toBe("none");
149+
expect(state.mixer.inserts[0].fxSlots[0].enabled).toBe(false);
150+
});
151+
});
152+
153+
describe("setInsertMeter", () => {
154+
it("sets meter value clamped to [0, 1]", () => {
155+
const state = createMixerState([makeInsert("insert-1")]);
156+
mixerReducers.setInsertMeter(state, { payload: { insertId: "insert-1", meter: 1.5 } });
157+
expect(state.mixer.inserts[0].meter).toBe(1);
158+
});
159+
160+
it("updates spectrum and waveform arrays", () => {
161+
const state = createMixerState([makeInsert("insert-1")]);
162+
mixerReducers.setInsertMeter(state, {
163+
payload: { insertId: "insert-1", meter: 0.5, spectrum: [0.2, 1.5], waveform: [-1.2, 0.8] },
164+
});
165+
expect(state.mixer.inserts[0].meterSpectrum).toEqual([0.2, 1]);
166+
expect(state.mixer.inserts[0].meterWaveform).toEqual([-1, 0.8]);
167+
});
168+
169+
it("clamps maximizerReduction", () => {
170+
const state = createMixerState([makeInsert("insert-1")]);
171+
mixerReducers.setInsertMeter(state, {
172+
payload: { insertId: "insert-1", meter: 0, maximizerReduction: 50 },
173+
});
174+
expect(state.mixer.inserts[0].maximizerReduction).toBe(36);
175+
});
176+
});
177+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, it, expect } from "vitest";
2+
import { transportReducers } from "./transport";
3+
4+
function createTransportState() {
5+
return {
6+
transport: {
7+
bpm: 140,
8+
isPlaying: false,
9+
isRecording: false,
10+
mode: "pattern",
11+
songLoopEnabled: false,
12+
currentStep16: 0,
13+
},
14+
};
15+
}
16+
17+
describe("transportReducers", () => {
18+
describe("setBpm", () => {
19+
it("updates bpm within range", () => {
20+
const state = createTransportState();
21+
transportReducers.setBpm(state, { payload: 128 });
22+
expect(state.transport.bpm).toBe(128);
23+
});
24+
25+
it("clamps bpm to minimum 40", () => {
26+
const state = createTransportState();
27+
transportReducers.setBpm(state, { payload: 20 });
28+
expect(state.transport.bpm).toBe(40);
29+
});
30+
31+
it("clamps bpm to maximum 300", () => {
32+
const state = createTransportState();
33+
transportReducers.setBpm(state, { payload: 400 });
34+
expect(state.transport.bpm).toBe(300);
35+
});
36+
37+
it("rounds fractional bpm", () => {
38+
const state = createTransportState();
39+
transportReducers.setBpm(state, { payload: 140.7 });
40+
expect(state.transport.bpm).toBe(141);
41+
});
42+
});
43+
44+
describe("setPlaying", () => {
45+
it("sets playing to true", () => {
46+
const state = createTransportState();
47+
transportReducers.setPlaying(state, { payload: true });
48+
expect(state.transport.isPlaying).toBe(true);
49+
});
50+
51+
it("resets currentStep16 when stopping", () => {
52+
const state = createTransportState();
53+
state.transport.currentStep16 = 8;
54+
transportReducers.setPlaying(state, { payload: false });
55+
expect(state.transport.isPlaying).toBe(false);
56+
expect(state.transport.currentStep16).toBe(0);
57+
});
58+
});
59+
60+
describe("setRecording", () => {
61+
it("toggles recording state", () => {
62+
const state = createTransportState();
63+
transportReducers.setRecording(state, { payload: true });
64+
expect(state.transport.isRecording).toBe(true);
65+
});
66+
});
67+
68+
describe("setTransportMode", () => {
69+
it("changes mode to song", () => {
70+
const state = createTransportState();
71+
transportReducers.setTransportMode(state, { payload: "song" });
72+
expect(state.transport.mode).toBe("song");
73+
});
74+
});
75+
76+
describe("setSongLoopEnabled", () => {
77+
it("enables loop", () => {
78+
const state = createTransportState();
79+
transportReducers.setSongLoopEnabled(state, { payload: 1 });
80+
expect(state.transport.songLoopEnabled).toBe(true);
81+
});
82+
83+
it("disables loop with falsy value", () => {
84+
const state = createTransportState();
85+
transportReducers.setSongLoopEnabled(state, { payload: false });
86+
expect(state.transport.songLoopEnabled).toBe(false);
87+
});
88+
});
89+
90+
describe("setPlayheadStep", () => {
91+
it("sets step to requested value", () => {
92+
const state = createTransportState();
93+
transportReducers.setPlayheadStep(state, { payload: 16 });
94+
expect(state.transport.currentStep16).toBe(16);
95+
});
96+
97+
it("clamps negative step to 0", () => {
98+
const state = createTransportState();
99+
transportReducers.setPlayheadStep(state, { payload: -5 });
100+
expect(state.transport.currentStep16).toBe(0);
101+
});
102+
103+
it("rounds fractional steps", () => {
104+
const state = createTransportState();
105+
transportReducers.setPlayheadStep(state, { payload: 7.8 });
106+
expect(state.transport.currentStep16).toBe(8);
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)