Files
SpotiFLAC-Mobile/test/lyrics_timeline_test.dart

199 lines
5.6 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/utils/lyrics_parser.dart';
import 'package:spotiflac_android/utils/lyrics_timeline.dart';
void main() {
test('backing stays under its lead without changing the timing order', () {
final lines = LyricsParser.parse('''
[00:01.00]v1:<00:01.00>Lead<00:06.00>
[bg:<00:04.00>Echo<00:06.00>]
[bg:<00:05.00>Again<00:06.00>]
[00:03.00]v2:<00:03.00>Guest<00:06.00>
''').lines;
final layout = LyricDisplayLayout(lines);
expect(lines.map((line) => line.text), ['Lead', 'Guest', 'Echo', 'Again']);
expect(layout.lineOrder.map((i) => lines[i].text), [
'Lead',
'Echo',
'Again',
'Guest',
]);
expect(layout.rowForLine, [0, 3, 1, 2]);
expect(layout.leadForLine, [0, 1, 0, 0]);
expect(layout.focusForLine, [0, 1, 1, 1]);
expect(activeLyricIndices(lines, const Duration(seconds: 5), 3), {
0,
1,
2,
3,
});
});
test('background onset keeps focus on the lead and gaps remain separate', () {
final lines = lyricsTimelineWithGaps(
LyricsParser.parse('''
[00:05.00]v2:<00:05.00>Lead<00:10.00>
[bg:<00:06.00>Echo<00:11.00>]
[00:15.00]Next
''').lines,
);
final layout = LyricDisplayLayout(lines);
expect(lines.map((line) => line.text), ['', 'Lead', 'Echo', '', 'Next']);
expect(layout.lineOrder, [0, 1, 2, 3, 4]);
expect(layout.focusForLine, [0, 1, 1, 3, 4]);
expect(lines[3].time, const Duration(seconds: 11));
});
test(
'a backing pickup can start before the lead without losing its parent',
() {
final lines = LyricsParser.parse('''
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:m="http://www.w3.org/ns/ttml#metadata">
<body><p begin="1s" end="6s" m:agent="v2"><span m:role="x-bg" begin="1s" end="5s">Pickup</span><span begin="2s" end="6s">Lead</span></p></body></tt>
''').lines;
final layout = LyricDisplayLayout(lines);
expect(lines.map((line) => line.text), ['Pickup', 'Lead']);
expect(layout.lineOrder, [1, 0]);
expect(layout.focusForLine, [1, 1]);
},
);
test(
'backing vocals do not dim an unlabelled lead that is still singing',
() {
final lines = LyricsParser.parse('''
[00:01.00]<00:01.00>Main<00:05.00>
[bg:<00:02.00>Echo<00:03.00>]
''').lines;
expect(activeLyricIndices(lines, const Duration(seconds: 2), 1), {0, 1});
expect(activeLyricIndices(lines, const Duration(seconds: 4), 1), {0});
},
);
test(
'overlapping singers stay active until their ends, including seek back',
() {
final lines = LyricsParser.parse('''
[00:01.00]v1:<00:01.00>Lead<00:05.00>
[00:02.00]v2:<00:02.00>Guest<00:04.00>
[00:06.00]v3:Third
''').lines;
Set<int> active(int seconds) {
final position = Duration(seconds: seconds);
return activeLyricIndices(
lines,
position,
LyricsParser.activeIndex(lines, position),
);
}
expect(active(0), isEmpty);
expect(active(1), {0});
expect(active(3), {0, 1});
expect(active(4), {0});
expect(active(5), isEmpty);
expect(active(6), {2});
expect(active(3), {0, 1});
expect(active(1), {0});
},
);
List<(int, int)> gaps(String text) =>
lyricsTimelineWithGaps(LyricsParser.parse(text).lines)
.where((line) => line.text.isEmpty)
.map((line) => (line.time.inMilliseconds, line.end!.inMilliseconds))
.toList();
test('intro and explicit LRC breaks count down, never the outro', () {
const text = '''
[00:00.00]
[00:09.00]First
[00:12.00]
[00:14.00]
[00:21.00]Second
[00:25.00]
[00:30.00]
''';
expect(gaps(text), [(0, 9000), (12000, 21000)]);
expect(
lyricsTimelineWithGaps(LyricsParser.parse(text).lines).last.text,
'Second',
);
});
test('TTML line ends identify breaks without empty timestamps', () {
expect(
gaps('''
<tt><body><div>
<p begin="9s" end="12s">First</p>
<p begin="21s" end="24s">Second</p>
<p begin="25s" end="28s">Third</p>
</div></body></tt>
'''),
[(0, 9000), (12000, 21000)],
);
});
test('enhanced LRC word ends retain held vocals before a break', () {
expect(
gaps('''
[00:00.00]<00:00.00>Held<00:16.00>
[00:20.00]<00:20.00>Last<00:24.00>
'''),
[(16000, 20000)],
);
});
test('overlapping lines cannot start a countdown during a held vocal', () {
expect(
gaps('''
<tt><body><div>
<p begin="0s" end="16s">Held</p>
<p begin="8s" end="10s">Backing vocal</p>
<p begin="20s" end="24s">Last</p>
</div></body></tt>
'''),
[(16000, 20000)],
);
});
test('plain LRC does not invent an end time from distance between lines', () {
expect(gaps('[00:00]Held\n[00:30]Last'), isEmpty);
expect(gaps('[00:00]\n[00:30]'), isEmpty);
expect(gaps('Unsynced text'), isEmpty);
});
test('short rests and immediate vocals have no flashing countdown', () {
expect(gaps('[00:02]First\n[00:04]\n[00:06]Last'), isEmpty);
expect(gaps('[00:00]First\n[00:04]\n[00:07]Last'), [(4000, 7000)]);
});
test('offset correction also moves the countdown boundaries', () {
expect(gaps('[offset:500]\n[00:09]First\n[00:12]\n[00:21]Last'), [
(0, 8500),
(11500, 20500),
]);
});
test('word ends protect against an early or invalid paragraph end', () {
expect(
gaps('''
<tt><body><div>
<p begin="0s" end="2s"><span begin="0s" end="8s">Held</span></p>
<p begin="12s" end="14s">Last</p>
</div></body></tt>
'''),
[(8000, 12000)],
);
expect(
gaps('''
<tt><body><div>
<p begin="0s" end="2s"><span begin="5s">Unknown end</span></p>
<p begin="12s" end="14s">Last</p>
</div></body></tt>
'''),
isEmpty,
);
});
}