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定期的に画面を更新するのには適していません。
そのためにを使用CADisplayLinkし、メイン実行ループでスケジュールします。

また、常にスクロールして表示したい場合は、を使用しませんUIScrollViewUIView表示リンクの各コールバックで境界をサブクラス化して更新するだけです。

于 2012-04-21T06:30:02.240 に答える
0

まさか、アップルの古いメトロノームのサンプルが残ってるなんて!! 私はそのことをどこにも見つけることができません。しかし、とにかく、メトロノームのサンプルは、あなたの質問が存在するのとまったく同じ理由で非推奨になりました.その実装は音楽的に見掛け倒しでした. メトロノームは、オブジェクトの位置と多くの不必要なセレクターに依存していたため、不安定なタイムキーパーになりました。その上、NSTimer とスレッドは、音楽的な精度を意図したものではありませんでした。私のアドバイスは、それを破棄しますが、最初にGithubに載せてください.

過去に使っていたのはこちら。これは著作権で保護された作品であるため、使用量に注意し、適切なクレジットを与えることを忘れないでください: Metronome .

于 2012-04-21T06:15:30.670 に答える