@@ -162,16 +162,122 @@ BOOST_AUTO_TEST_CASE (TempoTargetTapsFromNoteOn)
162162 BOOST_REQUIRE_CLOSE ((double ) session.getProperty (tags::tempo), 120.0 , 0.0001 );
163163 BOOST_REQUIRE_EQUAL (flashes, 2 );
164164
165- // Note-off and CC never tap : tempo stays put and the flash does not fire.
165+ // Note-off never taps : tempo stays put and the flash does not fire.
166166 session.setProperty (tags::tempo, 100.0 , nullptr );
167167 target.apply (MidiMessage::noteOff (1 , 60 ), false );
168- target.apply (MidiMessage::controllerEvent (1 , 7 , 127 ), false );
169168 BOOST_REQUIRE_CLOSE ((double ) session.getProperty (tags::tempo), 100.0 , 0.0001 );
170169 BOOST_REQUIRE_EQUAL (flashes, 2 );
171170
172171 conn.disconnect ();
173172}
174173
174+ namespace {
175+
176+ /* * A tempo target plus the session tree and flash counter it writes to, so the
177+ controller cases below stay about the trigger rule and nothing else. */
178+ struct TempoFixture
179+ {
180+ TempoFixture (const juce::String& mode = " above" , int value = 67 )
181+ : target (session, shared, tapped, mode, value)
182+ {
183+ session.setProperty (tags::tempo, 100.0 , nullptr );
184+ conn = tapped.connect ([this ] { ++flashes; });
185+ }
186+
187+ ~TempoFixture () { conn.disconnect (); }
188+
189+ /* * Apply a CC on the mapped controller at the given arrival time. */
190+ void cc (int value, double ms = 0.0 )
191+ {
192+ auto m = MidiMessage::controllerEvent (1 , 7 , value);
193+ m.setTimeStamp (ms);
194+ target.apply (m, false );
195+ }
196+
197+ double tempo () const { return (double ) session.getProperty (tags::tempo); }
198+
199+ ValueTree session { types::Session };
200+ TapTempo shared;
201+ Signal<void ()> tapped;
202+ SignalConnection conn;
203+ int flashes = 0 ;
204+ TempoTarget target;
205+ };
206+
207+ } // namespace
208+
209+ BOOST_AUTO_TEST_CASE (TempoTargetTapsFromCCAboveThreshold)
210+ {
211+ // Default rule: a tap fires when the value crosses up to or through 67.
212+ // Twisting back and forth taps once per upward pass, so 500 ms between
213+ // crossings is 120 BPM.
214+ TempoFixture f;
215+
216+ f.cc (0 , 0.0 ); // below the threshold: not a tap
217+ BOOST_REQUIRE_EQUAL (f.flashes , 0 );
218+
219+ f.cc (80 , 100.0 ); // crossed up: seeds the run
220+ BOOST_REQUIRE_EQUAL (f.flashes , 1 );
221+ BOOST_REQUIRE_CLOSE (f.tempo (), 100.0 , 0.0001 );
222+
223+ f.cc (100 , 200.0 ); // still above: no second tap
224+ f.cc (20 , 400.0 ); // falling back down: no tap
225+ BOOST_REQUIRE_EQUAL (f.flashes , 1 );
226+
227+ f.cc (90 , 600.0 ); // crossed up again, 500 ms after the first tap
228+ BOOST_REQUIRE_EQUAL (f.flashes , 2 );
229+ BOOST_REQUIRE_CLOSE (f.tempo (), 120.0 , 0.0001 );
230+ }
231+
232+ BOOST_AUTO_TEST_CASE (TempoTargetCCThresholdIsConfigurable)
233+ {
234+ TempoFixture f (" above" , 100 );
235+
236+ f.cc (80 , 0.0 ); // above 67 but below the configured threshold
237+ BOOST_REQUIRE_EQUAL (f.flashes , 0 );
238+
239+ f.cc (100 , 100.0 ); // exactly at the threshold counts
240+ BOOST_REQUIRE_EQUAL (f.flashes , 1 );
241+ }
242+
243+ BOOST_AUTO_TEST_CASE (TempoTargetCCTouchedZero)
244+ {
245+ TempoFixture f (" zero" );
246+
247+ f.cc (127 , 0.0 ); // never taps: only arriving at 0 does
248+ BOOST_REQUIRE_EQUAL (f.flashes , 0 );
249+
250+ f.cc (0 , 100.0 );
251+ BOOST_REQUIRE_EQUAL (f.flashes , 1 );
252+
253+ f.cc (0 , 200.0 ); // parked at 0: no repeat
254+ BOOST_REQUIRE_EQUAL (f.flashes , 1 );
255+
256+ f.cc (64 , 500.0 );
257+ f.cc (0 , 600.0 ); // back to 0, 500 ms after the first tap
258+ BOOST_REQUIRE_EQUAL (f.flashes , 2 );
259+ BOOST_REQUIRE_CLOSE (f.tempo (), 120.0 , 0.0001 );
260+ }
261+
262+ BOOST_AUTO_TEST_CASE (TempoTargetCCTouchedMax)
263+ {
264+ TempoFixture f (" max" );
265+
266+ f.cc (0 , 0.0 );
267+ BOOST_REQUIRE_EQUAL (f.flashes , 0 );
268+
269+ f.cc (127 , 100.0 );
270+ BOOST_REQUIRE_EQUAL (f.flashes , 1 );
271+
272+ f.cc (127 , 200.0 ); // parked at 127: no repeat
273+ BOOST_REQUIRE_EQUAL (f.flashes , 1 );
274+
275+ f.cc (10 , 500.0 );
276+ f.cc (127 , 600.0 );
277+ BOOST_REQUIRE_EQUAL (f.flashes , 2 );
278+ BOOST_REQUIRE_CLOSE (f.tempo (), 120.0 , 0.0001 );
279+ }
280+
175281BOOST_AUTO_TEST_CASE (InvalidTargets)
176282{
177283 ProcessorPtr obj = new ParamTestNode (1 );
0 commit comments