1
+ #include < cstdint>
2
+ #include < limits>
3
+ #include < type_traits>
4
+
5
+ // Prefer the project's established test framework.
6
+ // Defaulting to GoogleTest. If your project uses Catch2 or doctest,
7
+ // replace the includes and TEST/EXPECT macros accordingly.
8
+ #include < gtest/gtest.h>
9
+
10
+ #include < HyperSonicDrivers/drivers/midi/IMidiChannelVoice.hpp>
11
+ #include < HyperSonicDrivers/drivers/midi/IMidiChannel.hpp>
12
+
13
+ namespace HSD = HyperSonicDrivers::drivers::midi;
14
+
15
+ // A minimal concrete test double for IMidiChannelVoice in case the interface
16
+ // has other pure virtual functions. We expose accessors to observe
17
+ // effective (real) volume for assertions, relying only on base class behavior.
18
+ class TestMidiChannelVoice : public HSD ::IMidiChannelVoice {
19
+ public:
20
+ // Provide a constructor that sets the channel pointer and initial volume state if needed.
21
+ explicit TestMidiChannelVoice (HSD::IMidiChannel* ch) {
22
+ m_channel = ch;
23
+ // Ensure deterministic initial values
24
+ m_volume = 0 ;
25
+ m_real_volume = 0 ;
26
+ }
27
+
28
+ // If IMidiChannelVoice declares other pure virtuals, stub them here.
29
+ // For example:
30
+ // void noteOn(uint8_t, uint8_t) override {}
31
+ // void noteOff(uint8_t, uint8_t) override {}
32
+ // ... (No-ops for unit testing of base functionality.)
33
+
34
+ // Test-only helpers to read protected/private state computed by base logic.
35
+ uint8_t test_getRealVolume () const noexcept { return m_real_volume; }
36
+ uint8_t test_getRawVolume () const noexcept { return m_volume; }
37
+ };
38
+
39
+ static_assert (std::is_same_v<uint8_t , unsigned char > || std::numeric_limits<uint8_t >::digits == 8 ,
40
+ " Assumption: uint8_t is 8-bit." );
41
+
42
+ TEST (IMidiChannelVoiceTests, GetChannelNumReflectsUnderlyingChannel) {
43
+ HSD::IMidiChannel channel{};
44
+ channel.channel = 9 ; // Typical percussion channel in MIDI (0-based 9 == channel 10)
45
+ channel.volume = 100 ; // Arbitrary
46
+
47
+ TestMidiChannelVoice voice (&channel);
48
+ EXPECT_EQ (voice.getChannelNum (), 9u );
49
+
50
+ // Mutate underlying channel object and ensure reflection
51
+ channel.channel = 2 ;
52
+ EXPECT_EQ (voice.getChannelNum (), 2u );
53
+ }
54
+
55
+ TEST (IMidiChannelVoiceTests, SetVolumesUpdatesRawAndRealVolume_WithChannelAtMax127) {
56
+ HSD::IMidiChannel channel{};
57
+ channel.channel = 1 ;
58
+ channel.volume = 127 ; // Max channel volume
59
+
60
+ TestMidiChannelVoice voice (&channel);
61
+
62
+ // When channel volume = 127, real volume should equal requested volume, clamped to 127.
63
+ voice.setVolumes (0 );
64
+ EXPECT_EQ (voice.test_getRawVolume (), 0u );
65
+ EXPECT_EQ (voice.test_getRealVolume (), 0u );
66
+
67
+ voice.setVolumes (64 );
68
+ EXPECT_EQ (voice.test_getRawVolume (), 64u );
69
+ EXPECT_EQ (voice.test_getRealVolume (), 64u );
70
+
71
+ voice.setVolumes (127 );
72
+ EXPECT_EQ (voice.test_getRawVolume (), 127u );
73
+ EXPECT_EQ (voice.test_getRealVolume (), 127u );
74
+
75
+ // Values >127 can appear due to uint8_t domain (0..255). Base code clamps via std::min(..., 127).
76
+ voice.setVolumes (static_cast <uint8_t >(200 ));
77
+ EXPECT_EQ (voice.test_getRawVolume (), static_cast <uint8_t >(200 ));
78
+ EXPECT_EQ (voice.test_getRealVolume (), 127u ) << " Should saturate at 127" ;
79
+ }
80
+
81
+ TEST (IMidiChannelVoiceTests, RealVolumeScalesByChannelVolume_AndUsesIntegerDivision) {
82
+ HSD::IMidiChannel channel{};
83
+ channel.channel = 3 ;
84
+ // Use a mid channel volume to test scaling and truncation
85
+ channel.volume = 64 ; // approx 50% of 127
86
+
87
+ TestMidiChannelVoice voice (&channel);
88
+
89
+ // raw=64, channel=64 => (64 * 64) / 127 = 4096/127 = 32 (integer truncation)
90
+ voice.setVolumes (64 );
91
+ EXPECT_EQ (voice.test_getRawVolume (), 64u );
92
+ EXPECT_EQ (voice.test_getRealVolume (), 32u ) << " Expected truncated integer division result" ;
93
+
94
+ // raw=127, channel=64 => (127 * 64) / 127 = 64
95
+ voice.setVolumes (127 );
96
+ EXPECT_EQ (voice.test_getRealVolume (), 64u );
97
+
98
+ // raw=1, channel=64 => (1 * 64) / 127 = 0
99
+ voice.setVolumes (1 );
100
+ EXPECT_EQ (voice.test_getRealVolume (), 0u ) << " Small products should truncate to zero" ;
101
+ }
102
+
103
+ TEST (IMidiChannelVoiceTests, RealVolumeBecomesZeroWhenChannelVolumeIsZero) {
104
+ HSD::IMidiChannel channel{};
105
+ channel.channel = 7 ;
106
+ channel.volume = 0 ;
107
+
108
+ TestMidiChannelVoice voice (&channel);
109
+
110
+ for (uint16_t v = 0 ; v <= 255 ; ++v) {
111
+ voice.setVolumes (static_cast <uint8_t >(v));
112
+ EXPECT_EQ (voice.test_getRealVolume (), 0u ) << " Channel volume zero should mute all" ;
113
+ }
114
+ }
115
+
116
+ TEST (IMidiChannelVoiceTests, SaturatesAt127ForLargeProducts) {
117
+ HSD::IMidiChannel channel{};
118
+ channel.channel = 4 ;
119
+ channel.volume = 127 ;
120
+
121
+ TestMidiChannelVoice voice (&channel);
122
+
123
+ // Any raw volume >= 127 should clamp to 127 when channel volume is max
124
+ for (uint16_t v = 127 ; v <= 255 ; ++v) {
125
+ voice.setVolumes (static_cast <uint8_t >(v));
126
+ EXPECT_EQ (voice.test_getRealVolume (), 127u );
127
+ }
128
+
129
+ // Also verify saturation when channel volume < 127 but product still exceeds 127.
130
+ channel.volume = 100 ; // Change the same channel instance (voice references it)
131
+ // raw=200, ch=100 -> 20000/127 = 157 (floor) -> min(157, 127) = 127
132
+ voice.setVolumes (static_cast <uint8_t >(200 ));
133
+ EXPECT_EQ (voice.test_getRealVolume (), 127u );
134
+ }
135
+
136
+ TEST (IMidiChannelVoiceTests, ChangingChannelVolumeRecalculatesOnNextSetVolumesCall) {
137
+ HSD::IMidiChannel channel{};
138
+ channel.channel = 5 ;
139
+ channel.volume = 100 ;
140
+
141
+ TestMidiChannelVoice voice (&channel);
142
+
143
+ voice.setVolumes (50 ); // 50*100/127 = 39
144
+ EXPECT_EQ (voice.test_getRealVolume (), 39u );
145
+
146
+ // Increase channel volume; real volume should only update when setVolumes is called again
147
+ channel.volume = 127 ;
148
+ // Real volume remains unchanged until we call setVolumes (contract inferred from impl)
149
+ EXPECT_EQ (voice.test_getRealVolume (), 39u );
150
+
151
+ // Trigger recompute by setting the same value again
152
+ voice.setVolumes (50 );
153
+ EXPECT_EQ (voice.test_getRealVolume (), 50u );
154
+ }
155
+
156
+ //
157
+ // Notes:
158
+ // - Testing library/framework: GoogleTest (gtest) assumed based on conventional C++ repos.
159
+ // Please adapt includes/macros if your repository uses Catch2 (TEST_CASE/REQUIRE) or doctest (TEST_CASE/CHECK).
160
+ // - External dependencies are not invoked; this is a pure computation test.
161
+ // - We use a minimal derived class to expose computed internal state for validation,
162
+ // avoiding white-box access to private members in production code.
163
+ //
0 commit comments