Shazam实时歌词功能的幕后花絮

作为重新设计的一部分,我有机会研究了一些非常令人兴奋且在技术上具有挑战性的部分,包括新版本的实时歌词同步功能,或者我们喜欢称其为“同步歌词”的方式。 你们中的某些人可能已经使用Shazam很久了,他们知道此功能不是新功能,我们确实在一段时间前就在应用程序中使用了此功能。 这就是我们交付给用户的魔力的全部。 不久之后,我们将其重新带回去,并使其变得更好。

在这篇博客中,我将分享一些有关开发过程的见解以及我们已经做出的一些决定。

设计! 设计! 设计!

当我们在应用程序中构建新功能时,我们的设计师始终会提供他们如何想象其工作原理的原型。 我们确实关心细节,因此确保每个细节正确都是很重要的。 归根结底,这会让我们的设计师感到满意,但更重要的是,我们的用户也会感到满意。

在歌词同步的情况下,原型是开发过程的基本要素。 在下面,您可以看到我所学习和复制的那本书。

这部影片上发生的事情很少值得注意。 在突出显示的线和未聚焦的线之间有动画,以及实际的运动。 为简化起见,我将其称为白色文本突出显示的行 。 这条线将失去重点 之后的路线将成为重点。 现在,将它们分解为关键动画:

  • 在行改变的任何给定时刻,突出显示的行的alpha值从1.0更改为0.0 。 (1个动画)。
  • 高亮显示的行必须在屏幕上完全相同的位置居中。
  • 列表滚动一行(1个动画)。
  • 在滚动过程中,即将成为焦点的线(意味着它必须从列表中淡出)将alpha值从1.0更改为0.0 ,而旧的突出显示的线则出现在逆动画的顶部(淡入到清单)。 想象一下,就像您在突出显示的行周围有一个安全区域一样,在列表滚动期间,您必须淡出刚击到安全区域底部的行,然后淡出将保留在顶部的行。 请参见下图,其中红色是突出显示的线条(2个动画)周围的安全区域。

在动画方面,并不复杂。 总共4个动画,它们全部更改一些alpha值。 容易吧?

除了动画之外,使设计更加复杂的是突出显示的行始终是静态的,而其余的行(称为歌词列表)仅是移动的,至少这就是我们要复制的效果。 我与Android团队进行了一些内部讨论,以了解实现这种行为的方法。 在我开始实施之前,考虑了两种可能性。

同步歌词#1

一种可能是将歌词列表视为两个单独的集合视图[1]和一个静态视图。 我喜欢将此静态视图称为焦点视图 。 在这种情况下,我们将一个集合视图视为已通过的歌词,将焦点视图视为突出显示的行,将第二个集合视图视为将要出现的歌词。

优点

  • 焦点视图在两个集合视图之间是静态的,因此我们无需计算视图的位置并同步其后的文本。
  • 易于在视图上设置布局约束。

缺点:

  • 我们将歌词列表视为两个单独的集合视图,因此我们需要从底部集合视图中删除第一行,并将突出显示的行重新插入到顶部集合视图中。 在调试和编写漂亮的代码时,这可能会引起头痛。
  • 该功能的要求之一是在屏幕上实现用户交互(稍后进行介绍)。 对于两个集合视图,这将非常困难。 我们可能需要创建第三个集合视图…pphhhh,什么令人头痛,对吗?
  • 此外,另一个要求是在两行文本之间插入一个广告(稍后再阅读)。 使用这种布局很难在同步过程中跳过广告。

同步歌词#2

另一种可能性,即我们实际实现的可能性,将歌词列表视为单个集合视图,并将焦点视图置于其上方。 这样,列表可以在焦点视图保持静态的同时在后台移动。 我们只需要确保将列表中突出显示的行隐藏起来,而仅将其显示在焦点视图上,并且由于焦点视图是透明的,因此用户将其视为集合视图的一部分。 完善!

#1的弊端如何? 这个版本可以解决那些问题吗? 其实,是。 我们不需要任何插入或删除,因此非常好。 同样,如果考虑到用户交互和广告需求,用户将能够轻松滚动单个集合视图。 我们唯一需要做的就是删除焦点视图,以便在滚动时它不会与文本冲突。 最后,我们可以通过这种方式在任意两行之间插入广告,而跳过广告将意味着我们将列表移至焦点视图下并在其后显示该行。 做完了!

在下面的图像中,您可以看到已实现的视图层次结构。

让我们同步它!

在一种情况下,当用户标记歌曲时,我们会收到包含为用户呈现的内容的响应。 这是我在文章开头提到的跟踪页面。 另外,对于使用Musixmatch API的已同步歌词,我们还有一个单独的响应。

可用数据

以下数据应足以创建确定已标记歌曲中歌词当前行的算法。

  • tagDate :成功标签的时间戳。
  • tagOffset :同步时间和tagDate之间的偏移量,因此我们知道同步时间和tag之间的时间。
  • songOffset :歌曲当前在标签处的偏移量,由响应给出。
  • startOffsettagOffsetsongOffset总和。
  • lyricLine.timeOffset :歌词中特定行的偏移,因此我们知道何时显示它。 由Musixmatch提供。

根据上面的数据,我们可以使用startOffset跳过我们已经在歌曲中传递的行。 我们也可以通过从lyricLine.timeOffset减去songOffsetlyricLine.timeOffset每一行的lyricLine.timeOffset

  float tagOffset = [[NSDate date] timeIntervalSinceDate:tagDate]; 
float startOffset = tagOffset + songOffset;
float relativeTimeOffset = lyricLine.timeOffset-songOffset;

排程器

对于同步的歌词计划程序,我们使用NSTimer [2]类来计划歌词的每一行以在特定日期触发。 触发日期是使用relativeTimeOffsettagDate之间的时间间隔计算的。 最后,我们可以使用此时间间隔和开始同步的确切时间来安排计时器。

  NSDate * lyricLineDate = [NSDate dateWithTimeInterval:relativeTimeOffset sinceDate:tagDate]; 
NSTimeInterval fireTimeInterval = [lyricLineDate timeIntervalSinceDate:[NSDate date]];
  self.timer = [NSTimercheduledTimerWithTimeInterval:fireTimeInterval目标:自我选择器:@selector(timerFired :) userInfo:nil repeats:NO]; 

可以为歌曲中尚未通过的歌词中的所有行重复设置此计时器。

轻微修改

最终,在计算了歌曲中的当前位置并基于relativeTimeOffset安排了计时器触发之后,我们实现了相当准确的同步。 为了确保该功能可以用作真正的卡拉OK机😉,我们使用偏移量和动画对时序进行了微调,因此每行都将显示在前面,而不是以后。 这样,您就可以为即兴卡拉OK战斗做好准备。 🎤

这就是全部?

不完全的。 还记得我告诉您有关用户互动和广告需求的信息吗?

首先,我们必须确保在同步过程中我们支持屏幕上的用户交互。 这使事情稍微复杂些,因为我们必须确保一旦用户滚动列表,我们的焦点视图就会从视图层次结构中删除,并且歌词同步将继续在列表上。 此外,一旦用户停止了交互至少2秒钟,该列表应自动滚动回到歌词中当前突出显示的行,并应将焦点视图放回原处。

第二,我们想在两行中间放置一个广告。 另一方面,这要求我们围绕如何在屏幕上找到广告的位置以及应将其插入的位置创建一些逻辑。 主要要求是用户在访问屏幕时应至少看到55%的广告,以便为我们提供一些指导,以在歌词中找到满足此值的两行内容。

结论

总之,这篇文章应该对我们如何在Shazam应用程序中实现新的同步歌词功能进行高级别概述。 在这背后还有更多的考虑因素和我们面临的挑战,但是希望下次您使用该应用程序时,您将对它在幕后的工作方式有一个更好的了解。

您可以在最新版本的Shazam中查看该功能。 随时发布反馈,建议或有关如何改进它的任何想法。