同步UIView动画速度与音频播放

4
我有一个垂直的一像素视图,我想把它移动到代表音符播放的网格上。根据歌曲的BPM(每分钟拍数),动画应该越来越慢或者越来越快。音符挤在一起,宽度为25px。
我现在的做法是每当x个采样通过时,将视图向右移动1px:
int bpm = 90;
int interval = (44100 * 60.0f / song.bpm) / 25.0f; // = 1176
// sample rate=44100 multiplied by 60 (seconds) divided by number of pixels

当BPM=90时,我每播放1176个样本就将UIView向右移动1像素。这个方法效果还不错,但如果BPM超过180,就会出现问题。此时视图落后于播放的音乐,当BPM增加到240时,视图就完全偏离了。

如何利用视图的内置动画功能,并根据上述计算确定动画速度?我希望在音乐开始播放时启动动画,这样就不需要手动逐像素地移动它了。


从您对BPM的描述中,我不确定您每秒钟期望更新多少行 - 您能澄清一下吗? - David H
基本上,我的代码中有一个静态成员,用于跟踪播放的样本数。当BPM为90时,这意味着每1176个样本更新1像素。采样率为44100(每秒),因此每秒有37.5次更新。当BPM = 120时,应该在播放882个样本后进行一次更新,因此每秒有44100/882 = 50次更新。 - user393964
你提到了白色垂直线,但没有提到背景。在音乐播放时,它是否也会“移动”(动画)?每秒50次更新,该线将在略超过6秒的时间内移动iPhone屏幕的宽度。 - David H
在竖直线下方有可以按的按钮(滚动视图在其上方移动),但它不是动画的。我知道,这个动画相当快。 - user393964
3个回答

4
如果我理解正确,您想做的是:
  1. 绘制静态背景图像。
  2. 在其上方绘制一条从左到右移动的垂直线。
  3. 将该线的x位置与音频播放时间同步。
需要解决两个问题:
  • 绘图性能
  • 音频同步
要正确实现绘图性能,有两种实现方式:
  1. 使用核心动画层和动画(比2.容易实现)
  2. 使用OpenGL来绘制整个视图(可能比1.稍微快一些,并且延迟略小于1.)
在OpenGL中实现视图的代码太长了,无法作为示例发布,但这里提供了使用核心动画选项的代码:
// In the UIViewController:

- (CALayer *)markerLayer
{
    static NSString *uniqueName = @"com.mydomain.MarkerLayer";
    for (CALayer *layer in self.view.layer.sublayers) {
        if ([layer.name isEqualToString:uniqueName])
            return layer;
    }

    CALayer *markerLayer = [CALayer layer];
    markerLayer.backgroundColor = [UIColor whiteColor].CGColor;
    markerLayer.name = uniqueName;
    markerLayer.anchorPoint = CGPointZero;
    [self.view.layer addSublayer:markerLayer];
    return markerLayer;
}

- (void)startAnimationWithDuration:(NSTimeInterval)duration
{
    [CATransaction begin];
    [CATransaction setDisableActions:YES];

    CALayer *markerLayer = [self markerLayer];

    markerLayer.bounds = (CGRect){.size = {1, self.view.bounds.size.height}};
    markerLayer.position = CGPointZero;
    [CATransaction commit];

    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
    animation.fromValue = @0.0f;
    animation.toValue = [NSNumber numberWithFloat:self.view.bounds.size.width];
    animation.duration = duration;
    [markerLayer addAnimation:animation forKey:@"audioAnimation"];
}

音频同步非常依赖于播放方式,比如使用AVFoundation或Core Audio。为了精准同步,您需要一种来自音频引擎的回调,告诉您当前播放位置或开始播放时间。
也许仅在开始音频之前立即调用startAnimationWithDuration:就足够了,但这似乎有点脆弱,并且取决于音频框架的延迟。

实际上,背景图像并不是静态的。我链接的图像是一个滚动视图,在其上发生此动画(每个小正方形都是一个按钮)。但我不认为这会改变这个解决方案的任何内容? - user393964
顺便提一下,我正在使用音频单元进行播放,并且如果需要的话,我可以对主线程进行异步消息调用。 - user393964

1
一种解决方案:您可以将背景视图设置为蓝色框所示,并确保此视图是不透明的。定义一个新的UIView子类并将其放置在我称之为背景视图上方 - 此视图将是非不透明的 - 让我们称之为标记视图。
标记视图将随时知道要绘制线条的x偏移量。它将提供一个drawRect方法,该方法将将rect参数设置为清晰的颜色(alpha = 0)。然后,您将使用Quartz调用绘制单个垂直线条(如果某个布尔值表示要执行此操作)。
当控制器启动时,它会告诉此视图不要绘制线条。当您启动时,将布尔值更改为YES,给出x坐标,然后告诉该视图setNeedsDisplayInRect:CGRectMake(theXoffset, 0, 1, heightOfView)];
即使存在一些延迟,当视图最终绘制x值时,它也应该是当前的,因为您正在“实时”更新它。
编辑:
  • 所谓的标记,是指垂直线 - 它“标记”了位置,对吧?

  • alpha 0 意味着(在这种情况下)大多数叠加视图,即“标记”视图,将是清晰的颜色,意味着每个像素都是透明的(alpha=0),以便不会阻挡其下方的视图。此视图绘制的唯一像素是垂直线。

  • BOOL,“x”等 - 视图的区域属性。您可能需要更多,但不需要太多。然后,这些属性会在调用“drawRect:”时指示视图应该执行什么操作。

  • 您需要学习一些关于 Quartz 的知识(这是 Apple 用于实际绘制的技术)。在 drawRect 中,您将获得当前的 CGContextRef,并在几乎所有调用中使用它,设置要绘制的线条宽度(默认为1),设置线条颜色,移动到某个点,将线条绘制到其他某个点(这将是一个“路径”),然后描边路径。与大多数其他解决方案相比,这将非常快速(但本身可以加速)。

如果这一切似乎太令人生畏,那么也许其他人会有一个更简单的解决方案,或者您可以提供赏金,看看是否有人(比如我)会为您编写整个代码。

编辑2:

这是执行指示器线的视图的代码(我使用了红线来更好地看到它,这个数组设置了颜色:{1,0,0,在R/G/B空间中)

#import "SlideLine.h"

#define LINE_WIDTH 1.0f

@implementation SlideLine
@synthesize showLine, x;

- (void)drawRect:(CGRect)rect
{
    if(!showLine) return;

    CGRect bounds = self.bounds;

    CGContextRef c = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(c, LINE_WIDTH);

    // draws two slightly transparent lines on either side to soften the look
    for(int i=0; i<3; ++i) {
        CGFloat val[4] = { 1, 0, 0, i==1 ? 1 : 0.5f}; // half alpha for each side line
        CGContextSetStrokeColor(c, val);

        CGFloat xx = x - LINE_WIDTH/2 - 1 + i;
        CGContextMoveToPoint(c, xx, 0);
        CGContextAddLineToPoint(c, xx, bounds.size.height);
        CGContextStrokePath(c);
    }
}

- (void)setShowLine:(BOOL)val
{
    showLine = val;
    [self setNeedsDisplay];
}

@end

我创建了一个Xcode项目,向您展示如何使用它,并提供控件以调整速度等参数:

enter image description here

编辑3:我开始修改使用动画,但不幸的是它还没有完全完成。想法是将标记线(3像素宽)变成单个视图(3像素宽)。一个新的SlidingLineController从其所有者那里获得更新(由音频回调驱动),告诉控制器在什么时间x应该在哪里(因此它是一个速率)。然后,控制器使用该速率来动画化标记,以便在指定的时间它在应该在的位置。随着更新的到来,动画速率可能会略微加快或减慢,以使绝对位置始终接近您想要的位置。

虽然这似乎比原来更好的技术,但实际上看起来比原来更“断断续续”。最优解可能是使用一个层(而不是视图),然后对其进行动画处理,并在其移动时更新其目标和速率,而不是启动和重新启动。可能这个第二个项目可以使用第一个项目的技术,以每秒30帧的速率简单地重新绘制自己,使用管理速率的相同概念。不幸的是,我现在看到这很快就会过期,所以没有时间去追求这个方案。

编辑:4 好的,我又更新了一下,以每秒30帧的速度重新绘制,使用上述控制器,效果相当不错——虽然还没有完成,但你可以在第三个旋转中看到它非常流畅。


我并不是很理解标记视图子类。为什么 alpha 需要为 0,你所说的 quartz 调用是什么意思?我也不太清楚布尔值在哪里定义。我猜它 不是 标记视图的成员,否则我需要从该对象内部检查它的值,对吗? - user393964
我决定采用悬赏方式,如果你想尝试一下。我无法以这种方式使其工作。 - user393964
我刚在 iPhone 4 上以每秒 60 次的速度运行它,非常流畅。在 120 次时会出现卡顿。 - David H
3
使用一个1像素的图层来代替drawrect并移动该图层来表示线条。这将导致更低的CPU消耗。为CGContext分配内存总是很昂贵的,特别是在动画期间,应尽可能避免。 - Jonathan Cichon
@JonathanCichon,实际上它是3像素宽-我在两侧使用略微透明的线来软化外观。实际上,我目前的想法是创建视图一次,然后动画其alpha或其框架,这更或多或少是相同的建议。 - David H
显示剩余2条评论

0

你可能需要:

  • 声明一个原子int变量,用于存储样本中的播放位置
  • 在主线程上安装一个NSTimer,以适当的频率进行播放(例如小于屏幕的刷新率)。
  • 在渲染音频时,通过适当的数量推进原子int的位置。
  • 当你的计时器触发时,读取音频线程写入的位置。如果有变化,做一些适当的事情来更新你的UI,比如调用setNeedsDisplay或重新定位你的歌曲位置视图。

我建议这样做,因为如果你最终要推送到和从多个渲染器中传输数据,你的优化尝试可能会适得其反。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接