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

时间:2012-08-21 00:20:03

标签: iphone ios animation uiview

我有一个垂直的单像素视图,我想在一个代表正在播放的音符的网格上移动。根据歌曲的BPM(每分钟节拍数),动画应该更慢或更快。这些音符彼此相邻挤压,宽度为25像素。

我现在这样做的方法是每次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 1px。这种方法运行得相当好,但是如果BPM高于180,则会给我带来问题。此时,视图落后于播放的音乐,当BPM上升到240时,视图就会消失。

如何使用视图的内置动画功能执行此操作,并根据我上面发布的计算动画速度?我想在音乐开始播放后启动动画,所以我不喜欢需要逐个像素地手动移动它。

3 个答案:

答案 0 :(得分:4)

如果我理解正确,你想做的是:

  1. 绘制静态背景图片。
  2. 在从左向右移动的垂直线上画一条垂直线。
  3. 将线路的x位置与音频播放时间同步。
  4. 有两个问题需要解决:

    • 绘画表现
    • 音频同步

    要获得正确的绘图性能,需要考虑两种实现:

    1. 使用核心动画图层和动画(比2更容易实现。)
    2. 使用OpenGL绘制整个视图(可能比1更快,延迟时间更短。)
    3. 在OpenGL中实现视图的代码太多,不能作为示例发布,但这里是Core Animation选项的代码:

      // 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:就足够了,但这看起来很脆弱,取决于音频框架的延迟。

答案 1 :(得分:1)

一种解决方案:您拥有如图所示的背景视图 - 蓝色框。确保此视图不透明。定义一个新的UIView子类并将其放在我称之为背景视图的地方 - 这个视图将是非透明的 - 让我们称之为标记视图。

标记视图随时会知道绘制线的x偏移量。它将提供一个drawRect方法,将rect参数设置为清晰颜色(alpha = 0)。然后,您将使用Quartz调用单个垂直线绘制(如果某个布尔值表示这样做)。

当您的控制器启动时,它会告诉此视图不绘制线条。启动时,将布尔值更改为YES,将其赋予x坐标,然后告诉该视图setNeedsDisplayInRect:CGRectMake(theXoffset,0,1,heightOfView)];

即使有一些延迟,当视图最终确实绘制时,x值应该是最新的,因为你正在“实时”更新。

编辑:

  • 通过标记,我的意思是垂直线 - 它“标记”了这个地方没有?

  • alpha 0表示(在这种情况下)大多数叠加视图(“marker”视图)将是一个清晰的颜色,这意味着每个像素都是清晰的(alpha = 0),以便不阻挡视图在它下面。此视图绘制的唯一像素是垂直线。

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

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

如果这一切看起来令人生畏,那么也许别人会有一个更简单的解决方案,或者你可以提供赏金,看看是否有人(像我一样)会为你准备好整个事情。

EDIT2:

这是执行指标线的视图的代码(我使用红线​​来更好地看到它,此数组设置颜色:{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 project,向您展示如何使用它,使用控件来改变速度等:

enter image description here

EDIT3:所以我开始修改使用动画,但不幸的是它没有完全出炉。我们的想法是采用标记线(3像素宽)并使其成为单个视图(3像素宽)。一个新的SlidingLineController从它的所有者获取更新(将由音频回调驱动),告诉控制器x应该在什么时间(所以它的速率)。然后,控制器使用该速率为标记设置动画,以便在指定时间显示它应该在的位置。随着更新的进行,动画速率可能会略微加快或减慢,以便绝对位置始终接近您想要的位置。

虽然这看起来比原版技术更好,但它实际上看起来比原版更“波涛汹涌”。可能最佳的解决方案是使用图层(不是视图)然后为其设置动画,并在其移动时更新其目标和速率,而不是启动和重新启动。可能这个Second Project可以使用从第一个开始的技术,使用相同的管理速率概念以30fps的速率简单地重绘自己。不幸的是,由于我现在看到它即将到期,我没有时间去追求这个。

编辑:4好吧,我做了另一次更新,以30fps的速度重绘,使用上面的控制器,它工作得很好 - 它没有完成,但你可以看到in the third spin它非常流畅

答案 2 :(得分:0)

你可能想要:

  • 声明一个原子int变量,用于存储样本中的播放位置
  • 在播放期间以合适的频率(例如,低于屏幕刷新率)在主线程上安装NSTimer
  • 在渲染音频时,将原子int的位置提前适当的数量。
  • 当您的计时器触发时,读取音频线程写入的位置。如果更改,请执行适当的更新操作,例如调用setNeedsDisplay或重新定位您的歌曲位置视图。

我建议这样做,因为如果最终推送到多个渲染器,您的优化尝试可能会适得其反。