import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/services/automix_analysis.dart'; Uint8List drumPattern(double bpm, {double phase = 0.17}) { const sampleRate = 11025; final bytes = ByteData(sampleRate * 24 * 2); final random = math.Random(7); for (var i = 0; i < sampleRate * 24; i++) { final time = i / sampleRate; final sinceBeat = (time - phase) % (60 / bpm); final transient = time < phase ? 0.0 : math.exp(-sinceBeat * 50); final value = 0.75 * transient * math.sin(time * 2 * math.pi * 100) + (random.nextDouble() - 0.5) * 0.003; bytes.setInt16(i * 2, (value * 32767).round(), Endian.little); } return bytes.buffer.asUint8List(); } void main() { for (final bpm in [90.0, 120.0, 124.0, 160.0]) { test('detects $bpm BPM and beat phase from audio', () { final grid = analyzeAutoMixPcm(drumPattern(bpm)); expect(grid.reliable, isTrue, reason: '${grid.bpm}, ${grid.confidence}'); expect(grid.bpm, closeTo(bpm, 0.75)); expect(grid.beatAtOrAfter(0), closeTo(0.17, 0.04)); }); } test('silence and unstructured noise do not invent a reliable beat', () { expect(analyzeAutoMixPcm(Uint8List(11025 * 24 * 2)).reliable, isFalse); final random = math.Random(11); final noise = ByteData(11025 * 24 * 2); for (var i = 0; i < noise.lengthInBytes; i += 2) { noise.setInt16(i, random.nextInt(32000) - 16000, Endian.little); } expect(analyzeAutoMixPcm(noise.buffer.asUint8List()).reliable, isFalse); }); AutoMixBeatGrid grid(double bpm, {double confidence = 0.9}) => AutoMixBeatGrid( bpm: bpm, phase: 0.17, confidence: confidence, firstSound: 0.1, ); AutoMixPlan plan(double from, double to, {double confidence = 0.9}) => AutoMixPlan.create( outgoingDuration: const Duration(seconds: 180), incomingDuration: const Duration(seconds: 200), outro: grid(from, confidence: confidence), intro: grid(to), outroOffset: 156, )!; test('compatible tempi align outgoing and incoming beats', () { final transition = plan(120, 124); expect(transition.beatMatched, isTrue); expect(transition.rate, closeTo(120 / 124, 0.00001)); final beat = (transition.start.inMicroseconds / 1e6 - 156.17) / 0.5; expect(beat, closeTo(beat.round(), 0.00001)); expect(transition.incomingStart.inMilliseconds, 170); expect( transition.start + transition.duration, lessThanOrEqualTo(const Duration(seconds: 180)), ); }); test('incompatible or uncertain beats use unstretched crossfade', () { for (final transition in [ plan(120, 160), plan(120, 124, confidence: 0.2), ]) { expect(transition.beatMatched, isFalse); expect(transition.rate, 1); expect(transition.incomingStart, Duration.zero); } expect( AutoMixPlan.create( outgoingDuration: const Duration(seconds: 10), incomingDuration: const Duration(minutes: 2), ), isNull, ); }); test('equal-power overlap restores normal tempo smoothly', () { final transition = plan(120, 124); final start = autoMixEnvelope(Duration.zero, transition); expect(start.outgoing, 1); expect(start.incoming, 0); final middle = autoMixEnvelope(transition.duration ~/ 2, transition); expect( middle.outgoing * middle.outgoing + middle.incoming * middle.incoming, closeTo(1, 0.00001), ); expect(middle.rate, transition.rate); final recovery = autoMixEnvelope( transition.duration + const Duration(seconds: 4), transition, ); expect(recovery.rate, closeTo((transition.rate + 1) / 2, 0.00001)); final end = autoMixEnvelope( transition.duration + const Duration(seconds: 8), transition, ); expect(end.complete, isTrue); expect(end.rate, 1); expect(end.incoming, 1); expect(end.outgoing, closeTo(0, 0.00001)); }); test('AutoMix is off for new and existing settings, and persists opt-in', () { expect(const AppSettings().autoMix, isFalse); expect(AppSettings.fromJson({}).autoMix, isFalse); final enabled = const AppSettings().copyWith(autoMix: true); expect(AppSettings.fromJson(enabled.toJson()).autoMix, isTrue); expect(enabled.copyWith(autoMix: false).autoMix, isFalse); }); }