0

我正在开发一个使用 UIScrollView 在屏幕上滚动乐谱的应用程序。由于我需要以固定的时间间隔发生这种情况,并且还要以均匀的间隔播放短声音,因此我的代码基于 Apple 提供的现已弃用的“节拍器”示例。

问题是滚动并不顺利 - 它非常生涩。我的运行日志表明我正在使用的 NSTimer 并没有真正以准确的间隔触发(或者可能部分代码执行时间过长)。

录取:我是音乐家,不是专业程序员。我阅读了有关 GCD 的 Apple 文档(这似乎是比节拍器示例中的线程执行同时事件更好的方法),但我真的不知道如何将它应用到我的项目中。

我没有使用分页。内容大小比屏幕大小大得多:ScrollView 滚动,只是锯齿状。

我的代码执行良好,但滚动非常生涩。任何帮助将不胜感激,特别是如果它符合 KSS 原则!

//  PlayView.m
#import "PlayView.h"
#include <stdlib.h>

NSInteger xWidth;
int xChange = 0;
float timeInterval;
AVAudioPlayer *audioPlayer1;
AVAudioPlayer *audioPlayer2;
float tempo;
int subdivisions;
int timesPlayed = 1;
int actualTimesPlayed = 0;

// ...

//RUN WHEN THE USER PRESSES THE PLAY BUTTON
-(void)start {
  // Used in calculating the speed of timer firing.
  // xWidth is the spacing between images (pixels)
  subdivisions = (int)(xWidth);

  // Keeps track of where we are in the measure
  beatNumber = 0;

  // Keeps track of how many measures we already played
  timesPlayed = 1;
  actualTimesPlayed = 0;

  // Let the device idle without dimming the screen
  UIApplication *myApp=[UIApplication sharedApplication];
  myApp.idleTimerDisabled=YES;  

  [self startDriverThread];    
}

// Taken straight from 'Metronome'
- (void)startDriverThread {
  if (soundPlayerThread != nil) {
    [soundPlayerThread cancel];
    [self waitForSoundDriverThreadToFinish];
  }

  NSThread *driverThread = [[NSThread alloc] initWithTarget:self  selector:@selector(startDriverTimer:) object:nil];
  self.soundPlayerThread = driverThread;
  [driverThread release];

  [self.soundPlayerThread start];
}

// Taken straight from 'Metronome'
- (void)waitForSoundDriverThreadToFinish {
  while (soundPlayerThread && ![soundPlayerThread isFinished]) { 
    // Wait for the thread to finish. I've experimented with different values
    [NSThread sleepForTimeInterval:timeInterval];
  }
}

// Taken straight from 'Metronome'
- (void)stopDriverThread {
  [self.soundPlayerThread cancel];
  [self waitForSoundDriverThreadToFinish];
  self.soundPlayerThread = nil;
}

// Modification of 'Metronome'
- (void)startDriverTimer:(id)info {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // Give the sound thread high priority to keep the timing steady.
    [NSThread setThreadPriority:1.0];
    BOOL continuePlaying = YES;

    while (continuePlaying) {  // Loop until cancelled.

      // An autorelease pool to prevent the build-up of temporary objects.
      NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init]; 

      // Reset the beat number to 0 at the end of each musical measure
      if (beatNumber == (subdivisions*4)) {
        beatNumber = 0; }

      // Incrementation of where we are in the bar on each firing of the timer
      beatNumber++;

      // On each beat, play a sound
      if(beatNumber % subdivisions == 0) {
        [self playSound]; }

      // On each firing of the timer, run the 'animateScreen' function, which scrolls the UIScrollView and performs some other simple tasks
      [self performSelectorOnMainThread:@selector(animateScreen) withObject:nil waitUntilDone:NO];

      // xChange is the number of pixels scrolled, but only start scrolling two and a half beats into the first bar (in order to keep the main image event at the center of the screen in each measure
      if ((actualTimesPlayed == 0 && beatNumber >= 2.5*subdivisions) || xChange > 0) {
          xChange += 1; }    

      // The time interval at which the timer fires is calculated by dividing the tempo (beats per minute, entered by the user; between 60-94) and 60 (seconds) This alone would result in one firing of the timer per beat, but we need at double this speed for some of the calculations 'animateScreen' does in between the beats, and really many more so the scrolling is smooth.
      // 
      // EXAMPLE: (60s/92bpm)/17% (image spacing) of 320pixels (screen width) = 0.012077
      timeInterval = (60/tempo)/subdivisions; 

      NSDate *curtainTime = [[NSDate alloc] initWithTimeIntervalSinceNow:(timeInterval)];
      NSDate *currentTime = [[NSDate alloc] init];

    // Wake up periodically to see if we've been cancelled.
    while (continuePlaying && ([currentTime compare:curtainTime] != NSOrderedDescending)) { 
        if ([soundPlayerThread isCancelled] == YES) {
            continuePlaying = NO;                
        }

        // Don't fully understand this; I've tried changing it to various values with no luck
        [NSThread sleepForTimeInterval:timeInterval];
        [currentTime release];
        currentTime = [[NSDate alloc] init];
    }
    [curtainTime release];      
    [currentTime release];      
    [loopPool drain];
}
[pool drain];

}

- (void)playSound {        
  if(beatNumber % subdivisions == 0){
    if (beatNumber == subdivisions) {
        [audioPlayer1 play];
    }

    else {
        [audioPlayer2 play];
    }
  }
}

- (void)animateScreen {        
  // BEAT 1
  if (beatNumber == (subdivisions)) {   
    // do some stuff
    // ...
  }    

  // THE SECOND  EIGTH OF 1
  if (beatNumber == (int)(subdivisions*1.25) && actualTimesPlayed >0) {
    // do some more stuff
    // ..
  }

  // BEAT 2
  if (beatNumber == (2*subdivisions)) {
    // even more stuff
  }

  // BEAT 3
  if (beatNumber == (3*subdivisions)) {
    // ... more
  }

  // BEAT 4
  if (beatNumber == (4*subdivisions)) {
    // yet more stuff
    // ...

    actualTimesPlayed++;
    timesPlayed++;
    if (timesPlayed == 3) {
        timesPlayed = 1; }     
  }    

  // On the "And of 4"
  if (beatNumber == subdivisions/2 && actualTimesPlayed > 0) {
    // STUFF
  }

  //Scroll over
  [theScroller setContentOffset:CGPointMake(xChange, 0) animated:NO];    
}

// ...

也许这不是最优雅的代码,但除了滚动之外,一切都正常。在我省略代码的地方发生了很多事情,但我知道事情并没有妨碍(当我将其注释掉并让程序完全裸露时 - 只是计时器和滚动,没有声音 - 它仍然是不顺利。)我相信计时器是问题所在。

非常感谢任何帮助/指导。

4

2 回答 2

1

NSTimer不适合定期更新屏幕。
使用 aCADisplayLink并将其安排在主运行循环中。

另外,如果您想查看以不断滚动,我不会使用UIScrollView. 只需子类UIView化并在每个显示链接的回调中更新它的边界。

于 2012-04-21T06:30:02.240 回答
0

没办法,你还有苹果的旧节拍器样本!!我在任何地方都找不到那个东西。但无论如何,节拍器样本被弃用的原因与您的问题存在的原因完全相同:它的实现在音乐上是劣质的。节拍器依赖于物体的位置和许多不必要的选择器,这使它成为一个不稳定的计时器。此外,NSTimers 和线程从来都不是为了音乐精度。我的建议,放弃它,但先把它放在 Github 上。

这是我过去使用过的。请记住,它是受版权保护的作品,所以要小心你使用了多少,并记得给他们适当的信用:节拍器

于 2012-04-21T06:15:30.670 回答