我正在开发一个使用UIScrollView在屏幕上滚动乐谱的应用程序。因为我需要以规则的时间间隔发生这种情况,并且在均匀间隔的间隔内播放短音,所以我的代码基于Apple提供的现已弃用的“节拍器”示例。
问题是滚动没有顺利进行 - 它非常生涩。我的运行日志表明我正在使用的NSTimer并没有真正以准确的间隔触发(或者代码的某些部分执行时间太长)。
录取:我是音乐家,不是专业程序员。我阅读了关于GCD的Apple文档(这似乎是比Metronome示例中的线程执行同步事件更好的方法),但我真的无法弄清楚如何将它应用到我的项目中。
我没有使用分页。内容大小远大于屏幕大小: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];
}
// ...
也许这不是最优雅的代码,但除了滚动之外,一切都是功能性的。在我省略代码的地方有很多事情,但我知道这些东西不会妨碍(当我评论它并让程序完全没有骨头 - 只是计时器和滚动,没有声音 - 它仍然是不顺利。)我确信计时器是问题所在。
非常感谢任何帮助/指导。
答案 0 :(得分:1)
NSTimer
不适合定期更新屏幕
使用CADisplayLink
并在主运行循环中安排它。
此外,如果您想要查看以不断滚动,我不会使用UIScrollView
。只需子类UIView
并在每个显示链接的回调中更新它的边界。
答案 1 :(得分:0)
没办法,你还有苹果老的节拍器样品!我在任何地方都找不到那个东西。但无论如何,节拍器样本已被弃用,原因与您的问题存在的原因完全相同:它的实施在音乐上是粗制滥造的。节拍器依赖于物体的位置和许多不必要的选择器,这使得它成为一个不稳定的计时器。此外,NSTimers和线程从未意味着音乐精度。我的建议,废弃它,但首先把它放在Github上。
这是我过去使用的内容。请记住,这是受版权保护的作品,所以要小心你使用了多少,并记住给予他们适当的信任:Metronome。