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];
}
// ...
おそらくこれは最も洗練されたコードではありませんが、スクロールを除いて、すべてが機能します。コードを省略したところはたくさんありますが、邪魔にならないことはわかっています(コメントアウトして、プログラムを完全にむき出しのままにしておくと、タイマーとスクロールだけで、音はありません。スムーズではありません。)タイマーが問題であると確信しています。
ヘルプ/指示は非常に高く評価されています。