mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
feat(lyrics): group smaller backing vocals beneath their lead
This commit is contained in:
1 parent
443a9a2bdf
commit
fedf5f137a
6 files changed
+346
-39
No files matched your search
@@ -47,8 +47,36 @@ ${_tag('translation', 2000, 'Second translation')}
|
||||
expect(backing.isBackground, isTrue);
|
||||
expect(backing.time.inMilliseconds, 2000);
|
||||
expect(backing.end?.inMilliseconds, 4000);
|
||||
expect(backing.vocalGroup, lyrics.lines.first.vocalGroup);
|
||||
});
|
||||
|
||||
test(
|
||||
'backing groups survive sorting, offsets and repeated backing parts',
|
||||
() {
|
||||
final lyrics = LyricsParser.parse('''
|
||||
[offset:100]
|
||||
[00:01.00]v2:<00:01.00>Lead<00:06.00>
|
||||
[bg:<00:04.00>Echo<00:05.00>]
|
||||
[bg:[00:05.00]<00:05.00>Again<00:06.00>]
|
||||
[00:03.00]v1:<00:03.00>Other lead<00:06.00>
|
||||
''');
|
||||
expect(lyrics.lines.map((line) => line.text), [
|
||||
'Lead',
|
||||
'Other lead',
|
||||
'Echo',
|
||||
'Again',
|
||||
]);
|
||||
final lead = lyrics.lines.first;
|
||||
expect(lyrics.lines[1].vocalGroup, isNot(lead.vocalGroup));
|
||||
for (final backing in lyrics.lines.skip(2)) {
|
||||
expect(backing.vocalGroup, lead.vocalGroup);
|
||||
expect(backing.voice?.id, 'v2');
|
||||
expect(backing.isBackground, isTrue);
|
||||
}
|
||||
expect(lyrics.lines[2].time.inMilliseconds, 3900);
|
||||
},
|
||||
);
|
||||
|
||||
test('TTML resolves inherited voices and explicit groups by namespace', () {
|
||||
final lyrics = LyricsParser.parse('''
|
||||
<t:tt xmlns:t="http://www.w3.org/ns/ttml" xmlns:m="http://www.w3.org/ns/ttml#metadata">
|
||||
@@ -94,6 +122,8 @@ ${_tag('translation', 2000, 'Second translation')}
|
||||
expect(lyrics.lines.map((line) => line.end?.inSeconds), [3, 4, 3]);
|
||||
expect(lyrics.lines.last.isBackground, isTrue);
|
||||
expect(lyrics.lines.last.voice?.id, 'v1');
|
||||
expect(lyrics.lines.last.vocalGroup, lyrics.lines.first.vocalGroup);
|
||||
expect(lyrics.lines[1].vocalGroup, isNot(lyrics.lines.first.vocalGroup));
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,61 @@ 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',
|
||||
() {
|
||||
|
||||
@@ -2593,6 +2593,99 @@ void main() {
|
||||
}
|
||||
|
||||
for (final mornye in [false, true]) {
|
||||
for (final ttml in [false, true]) {
|
||||
testWidgets(
|
||||
'backing vocals stay smaller under the lead with independent timing (Mornye: $mornye, TTML: $ttml)',
|
||||
(tester) async {
|
||||
metadataOverrides['lyrics'] = ttml
|
||||
? '''
|
||||
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:m="http://www.w3.org/ns/ttml#metadata">
|
||||
<head><metadata><m:agent xml:id="v1" type="person"/><m:agent xml:id="v2" type="person"/></metadata></head>
|
||||
<body><p begin="1s" end="7s" m:agent="v2"><span begin="1s" end="6s">Lead</span><span m:role="x-bg" m:agent="v1" begin="3s" end="7s">Echo</span></p><p begin="8s" end="10s">Next</p></body></tt>
|
||||
'''
|
||||
: '''
|
||||
[00:01.00]v2:<00:01.00>Lead<00:06.00>
|
||||
[bg:v1:<00:03.00>Echo<00:07.00>]
|
||||
[00:08.00]<00:08.00>Next<00:10.00>
|
||||
''';
|
||||
final playback = StreamController<PlaybackState>.broadcast();
|
||||
addTearDown(playback.close);
|
||||
await pumpNowPlaying(
|
||||
tester,
|
||||
theme: mornye ? MornyeTheme.build(Brightness.dark) : null,
|
||||
size: const Size(390, 1100),
|
||||
playbackEvents: playback.stream,
|
||||
);
|
||||
mediaItems.add(item('first'));
|
||||
await tester.pumpAndSettle();
|
||||
if (mornye) {
|
||||
await tester.tap(find.byIcon(CupertinoIcons.quote_bubble));
|
||||
} else {
|
||||
await tester.drag(find.byType(PageView), const Offset(-350, 0));
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
final lead = tester.widget<Text>(find.text('Lead'));
|
||||
final backing = tester.widget<Text>(find.text('Echo'));
|
||||
expect(
|
||||
backing.style!.fontSize,
|
||||
lessThan(lead.style!.fontSize! * 0.85),
|
||||
);
|
||||
expect(backing.textAlign, TextAlign.right);
|
||||
if (mornye) {
|
||||
expect(
|
||||
tester.getTopLeft(find.text('Echo')).dy -
|
||||
tester.getBottomLeft(find.text('Lead')).dy,
|
||||
closeTo(6, 0.1),
|
||||
);
|
||||
}
|
||||
expect(find.textContaining('[bg:'), findsNothing);
|
||||
expect(find.textContaining('v1:'), findsNothing);
|
||||
|
||||
Finder timed(String text) => find.descendant(
|
||||
of: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is Semantics && widget.properties.label == text,
|
||||
),
|
||||
matching: find.byType(CustomPaint),
|
||||
);
|
||||
Future<void> seek(int milliseconds) async {
|
||||
playback.add(
|
||||
PlaybackState(
|
||||
playing: false,
|
||||
processingState: AudioProcessingState.ready,
|
||||
updatePosition: Duration(milliseconds: milliseconds),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
double offset() =>
|
||||
tester.widget<ListView>(find.byType(ListView)).controller!.offset;
|
||||
|
||||
await seek(2000);
|
||||
expect(timed('Lead'), findsOneWidget);
|
||||
expect(timed('Echo'), findsNothing);
|
||||
final leadOffset = offset();
|
||||
await seek(4000);
|
||||
expect(timed('Lead'), findsOneWidget);
|
||||
expect(timed('Echo'), findsOneWidget);
|
||||
expect(offset(), closeTo(leadOffset, 0.1));
|
||||
expect(
|
||||
tester.getSize(timed('Echo')).height,
|
||||
lessThan(tester.getSize(timed('Lead')).height * 0.8),
|
||||
);
|
||||
await seek(6500);
|
||||
expect(timed('Lead'), findsNothing);
|
||||
expect(timed('Echo'), findsOneWidget);
|
||||
expect(offset(), closeTo(leadOffset, 0.1));
|
||||
await seek(2000);
|
||||
expect(timed('Lead'), findsOneWidget);
|
||||
expect(timed('Echo'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'singers keep their side before and during overlapping vocals ($mornye)',
|
||||
(tester) async {
|
||||
|
||||
Reference in new issue
Block a user