4

我有一个垂直的单像素视图,我想在表示正在播放的音符的网格上移动。根据歌曲的 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 时,视图就偏离了。

如何使用视图的内置动画功能来做到这一点,并将动画速度基于我上面发布的计算?我想在音乐开始播放后开始动画,所以我不需要逐个像素地手动移动它。

4

3 回答 3

4

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

  1. 绘制静态背景图像。
  2. 在从左到右移动的顶部画一条垂直线。
  3. 将行的 x 位置与音频播放时间同步。

有两个问题需要解决:

  • 绘图性能
  • 音频同步

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

  1. 使用 Core Animation 层和动画(比 2 更容易实现。)
  2. 使用 OpenGL 绘制整个视图(可能比 1 快一点,延迟少一点。)

在 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:但这似乎有点脆弱,并且取决于音频框架的延迟。

于 2012-08-23T17:28:10.443 回答
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, in R/G/B space)

#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 项目,向您展示如何使用它,并通过控件来改变速度等:

在此处输入图像描述

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

虽然这似乎是比原版更好的技术,但它实际上看起来比原版更“不稳定”。可能最佳解决方案是使用图层(而不是视图)然后对其进行动画处理,并在其移动时更新其目标和速率,而不是启动和重新启动。可能第二个项目可以使用第一个项目的技术,使用相同的管理速率概念以 30fps 的速率简单地重绘自身。不幸的是,我没有时间去追求这个,因为我现在看到这个很快就要到期了。

编辑:4 好吧,我做了另一个更新,以 30fps 的速度重绘,使用上面的控制器,它工作得很好——它还没有完成,但你可以在第三次旋转中看到它非常流畅

于 2012-08-21T01:16:40.843 回答
0

你可能想要:

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

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

于 2012-08-29T04:46:18.693 回答