节拍器。计时器、音乐和动画
Metronome. Timer, music and animations
我开发了一个应用程序,用户可以在其中放置声音然后播放构建的序列的几个单元格。有一个节拍器,它可以滴答作响。用户可以设置节拍器速度,这与设置传递到下一个单元格的速度相同。
我已经通过 "timer" 和处理程序实现了这种机制,它突出显示当前单元格并播放声音。一切正常。但是当我为一些视图设置动画时,我的计时器会出错。当动画完成时,计时器会按预期工作。
我该如何解决这个问题?
我尝试通过NSTimer
、dispatch_after
、performSelector:afterDelay:
、CADisplayLink
和dispatch_source_t
实现定时器。无论如何我在动画过程中遇到问题。我什至尝试通过 CADisplayLink
实现我自己的动画,我在其中计算动画视图帧,这也没有帮助。
我发现唯一 100% 可靠的方法是通过 CoreAudio 或 AudioToolbox 进行设置:https://developer.apple.com/documentation/audiotoolbox 一个音频流数据提供程序,由 iOS 定期调用以向音频系统提供音频样本。
乍一看可能令人望而生畏,但一旦设置完毕,您就可以完全准确地控制音频生成的内容。
这是我使用 AudioToolbox 设置 AudioUnit 的代码:
static AudioComponentInstance _audioUnit;
static int _outputAudioBus;
...
#pragma mark - Audio Unit
+(void)_activateAudioUnit
{
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
if([self _createAudioUnitInstance]
&& [self _setupAudioUnitOutput]
&& [self _setupAudioUnitFormat]
&& [self _setupAudioUnitRenderCallback]
&& [self _initializeAudioUnit]
&& [self _startAudioUnit]
)
{
[self _adjustOutputLatency];
// NSLog(@"Audio unit initialized");
}
}
+(BOOL)_createAudioUnitInstance
{
// Describe audio component
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
// Get audio units
OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioUnit);
[self _logStatus:status step:@"instantiate"];
return (status == noErr );
}
+(BOOL)_setupAudioUnitOutput
{
UInt32 flag = 1;
OSStatus status = AudioUnitSetProperty(_audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
_outputAudioBus,
&flag,
sizeof(flag));
[self _logStatus:status step:@"set output bus"];
return (status == noErr );
}
+(BOOL)_setupAudioUnitFormat
{
AudioStreamBasicDescription audioFormat = {0};
audioFormat.mSampleRate = 44100.00;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 2;
audioFormat.mBitsPerChannel = 16;
audioFormat.mBytesPerPacket = 4;
audioFormat.mBytesPerFrame = 4;
OSStatus status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
_outputAudioBus,
&audioFormat,
sizeof(audioFormat));
[self _logStatus:status step:@"set audio format"];
return (status == noErr );
}
+(BOOL)_setupAudioUnitRenderCallback
{
AURenderCallbackStruct audioCallback;
audioCallback.inputProc = playbackCallback;
audioCallback.inputProcRefCon = (__bridge void *)(self);
OSStatus status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
_outputAudioBus,
&audioCallback,
sizeof(audioCallback));
[self _logStatus:status step:@"set render callback"];
return (status == noErr);
}
+(BOOL)_initializeAudioUnit
{
OSStatus status = AudioUnitInitialize(_audioUnit);
[self _logStatus:status step:@"initialize"];
return (status == noErr);
}
+(void)start
{
[self clearFeeds];
[self _startAudioUnit];
}
+(void)stop
{
[self _stopAudioUnit];
}
+(BOOL)_startAudioUnit
{
OSStatus status = AudioOutputUnitStart(_audioUnit);
[self _logStatus:status step:@"start"];
return (status == noErr);
}
+(BOOL)_stopAudioUnit
{
OSStatus status = AudioOutputUnitStop(_audioUnit);
[self _logStatus:status step:@"stop"];
return (status == noErr);
}
+(void)_logStatus:(OSStatus)status step:(NSString *)step
{
if( status != noErr )
{
NSLog(@"AudioUnit failed to %@, error: %d", step, (int)status);
}
}
最后,一旦启动,我注册的音频回调将是提供音频的回调:
static OSStatus playbackCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
@autoreleasepool {
AudioBuffer *audioBuffer = ioData->mBuffers;
// .. fill in audioBuffer with Metronome sample data, fill the in-between ticks with 0s
}
return noErr;
}
您可以使用像 Audacity 这样的声音编辑器:https://www.audacityteam.org/download/mac/ 编辑您的文件并将其保存为 RAW PCM mono/stereo 数据文件,或者您可以使用 AVFoundation 库之一来检索音频样本来自任何支持的音频文件格式。将您的样本加载到缓冲区中,跟踪您在音频回调帧之间中断的位置,并输入与 0 交错的节拍器样本。
这样做的好处是您现在可以依靠 iOS 的 AudioToolbox 来确定代码的优先级,这样音频和视图动画就不会相互干扰。
干杯,祝你好运!
我找到了解决办法,玩苹果 AVAudioEngine
example HelloMetronome。我理解了主要思想。 我必须在 UI 中安排声音 和处理回调。使用任何计时器开始播放声音和更新 UI 是绝对错误的。
我开发了一个应用程序,用户可以在其中放置声音然后播放构建的序列的几个单元格。有一个节拍器,它可以滴答作响。用户可以设置节拍器速度,这与设置传递到下一个单元格的速度相同。 我已经通过 "timer" 和处理程序实现了这种机制,它突出显示当前单元格并播放声音。一切正常。但是当我为一些视图设置动画时,我的计时器会出错。当动画完成时,计时器会按预期工作。 我该如何解决这个问题?
我尝试通过NSTimer
、dispatch_after
、performSelector:afterDelay:
、CADisplayLink
和dispatch_source_t
实现定时器。无论如何我在动画过程中遇到问题。我什至尝试通过 CADisplayLink
实现我自己的动画,我在其中计算动画视图帧,这也没有帮助。
我发现唯一 100% 可靠的方法是通过 CoreAudio 或 AudioToolbox 进行设置:https://developer.apple.com/documentation/audiotoolbox 一个音频流数据提供程序,由 iOS 定期调用以向音频系统提供音频样本。
乍一看可能令人望而生畏,但一旦设置完毕,您就可以完全准确地控制音频生成的内容。
这是我使用 AudioToolbox 设置 AudioUnit 的代码:
static AudioComponentInstance _audioUnit;
static int _outputAudioBus;
...
#pragma mark - Audio Unit
+(void)_activateAudioUnit
{
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
if([self _createAudioUnitInstance]
&& [self _setupAudioUnitOutput]
&& [self _setupAudioUnitFormat]
&& [self _setupAudioUnitRenderCallback]
&& [self _initializeAudioUnit]
&& [self _startAudioUnit]
)
{
[self _adjustOutputLatency];
// NSLog(@"Audio unit initialized");
}
}
+(BOOL)_createAudioUnitInstance
{
// Describe audio component
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
// Get audio units
OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioUnit);
[self _logStatus:status step:@"instantiate"];
return (status == noErr );
}
+(BOOL)_setupAudioUnitOutput
{
UInt32 flag = 1;
OSStatus status = AudioUnitSetProperty(_audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
_outputAudioBus,
&flag,
sizeof(flag));
[self _logStatus:status step:@"set output bus"];
return (status == noErr );
}
+(BOOL)_setupAudioUnitFormat
{
AudioStreamBasicDescription audioFormat = {0};
audioFormat.mSampleRate = 44100.00;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 2;
audioFormat.mBitsPerChannel = 16;
audioFormat.mBytesPerPacket = 4;
audioFormat.mBytesPerFrame = 4;
OSStatus status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
_outputAudioBus,
&audioFormat,
sizeof(audioFormat));
[self _logStatus:status step:@"set audio format"];
return (status == noErr );
}
+(BOOL)_setupAudioUnitRenderCallback
{
AURenderCallbackStruct audioCallback;
audioCallback.inputProc = playbackCallback;
audioCallback.inputProcRefCon = (__bridge void *)(self);
OSStatus status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
_outputAudioBus,
&audioCallback,
sizeof(audioCallback));
[self _logStatus:status step:@"set render callback"];
return (status == noErr);
}
+(BOOL)_initializeAudioUnit
{
OSStatus status = AudioUnitInitialize(_audioUnit);
[self _logStatus:status step:@"initialize"];
return (status == noErr);
}
+(void)start
{
[self clearFeeds];
[self _startAudioUnit];
}
+(void)stop
{
[self _stopAudioUnit];
}
+(BOOL)_startAudioUnit
{
OSStatus status = AudioOutputUnitStart(_audioUnit);
[self _logStatus:status step:@"start"];
return (status == noErr);
}
+(BOOL)_stopAudioUnit
{
OSStatus status = AudioOutputUnitStop(_audioUnit);
[self _logStatus:status step:@"stop"];
return (status == noErr);
}
+(void)_logStatus:(OSStatus)status step:(NSString *)step
{
if( status != noErr )
{
NSLog(@"AudioUnit failed to %@, error: %d", step, (int)status);
}
}
最后,一旦启动,我注册的音频回调将是提供音频的回调:
static OSStatus playbackCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
@autoreleasepool {
AudioBuffer *audioBuffer = ioData->mBuffers;
// .. fill in audioBuffer with Metronome sample data, fill the in-between ticks with 0s
}
return noErr;
}
您可以使用像 Audacity 这样的声音编辑器:https://www.audacityteam.org/download/mac/ 编辑您的文件并将其保存为 RAW PCM mono/stereo 数据文件,或者您可以使用 AVFoundation 库之一来检索音频样本来自任何支持的音频文件格式。将您的样本加载到缓冲区中,跟踪您在音频回调帧之间中断的位置,并输入与 0 交错的节拍器样本。
这样做的好处是您现在可以依靠 iOS 的 AudioToolbox 来确定代码的优先级,这样音频和视图动画就不会相互干扰。
干杯,祝你好运!
我找到了解决办法,玩苹果 AVAudioEngine
example HelloMetronome。我理解了主要思想。 我必须在 UI 中安排声音 和处理回调。使用任何计时器开始播放声音和更新 UI 是绝对错误的。