使用 AVAudioEngine 构建循环音序器
Building a looping sequencer with AVAudioEngine
我正在使用 iOS 8 中的新 AVAudioEngine 功能构建一个简单的鼓机,但我无法思考如何构建循环机制。
似乎唯一的方法就是拥有一个 AVAudioPlayerNodes
的旋转缓冲区,并有某种工作可以在未来不断地安排缓冲区回放。这个对吗?
如果节点(及其缓冲区)只需要调度一次就好了。然后节点可以全部reset
,然后每次播放头到达序列末尾时再次从头开始play
。
我尝试了后者,方法是创建一个空的样本缓冲区,将其安排在音序器的末尾,并附加到它的 completionHandler
,但它似乎不起作用。
self.audioEngine = [[AVAudioEngine alloc] init];
self.instrumentNodes = [[NSMutableArray alloc] initWithCapacity:12];
AVAudioMixerNode *mixer = self.audioEngine.mainMixerNode;
NSError *error;
if (![self.audioEngine startAndReturnError:&error]) {
NSLog(@"Error starting engine: %@", error);
} else {
NSLog(@"Started engine");
NSURL *url = [[NSBundle mainBundle] URLForResource:@"Bleep_C3" withExtension:@"caf"];
AVAudioFile *file = [[AVAudioFile alloc] initForReading:url error:nil];
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:file.processingFormat frameCapacity:(AVAudioFrameCount)file.length];
[file readIntoBuffer:buffer error:nil];
double sampleRate = buffer.format.sampleRate;
AVAudioPCMBuffer *blankBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:buffer.format frameCapacity:0];
AVAudioPlayerNode *loopingNode = [[AVAudioPlayerNode alloc] init];
[self.audioEngine attachNode:loopingNode];
[self.audioEngine connect:loopingNode to:mixer format:[mixer outputFormatForBus:0]];
__block void (^schedule_loops)() = ^{
for (NSUInteger i = 0; i < 16; i++) {
double sampleTime = sampleRate * (0.2 * i);
double loopTime = sampleRate * (0.2 * 16);
AVAudioPlayerNode *playerNode = [[AVAudioPlayerNode alloc] init];
[self.audioEngine attachNode:playerNode];
[self.audioEngine connect:playerNode to:mixer format:[mixer outputFormatForBus:0]];
[playerNode scheduleBuffer:buffer atTime:[AVAudioTime timeWithSampleTime:sampleTime atRate:sampleRate] options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{
[playerNode stop];
[playerNode reset];
[self.audioEngine disconnectNodeOutput:playerNode];
}];
[playerNode play];
if (i == 15) {
[loopingNode scheduleBuffer:blankBuffer atTime:[AVAudioTime timeWithSampleTime:loopTime atRate:sampleRate] options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{
NSLog(@"Looping");
[loopingNode stop];
schedule_loops();
}];
[loopingNode play];
}
}
};
schedule_loops();
}
通过从文件中读取鼓 loop/beat(正如您在上面所做的那样)或以编程方式创建它来创建可循环缓冲区。
然后使用 AVAudioPlayerNodeBufferLoops 选项安排该缓冲区,它会自动为您循环。
你可以有多个播放器节点,但我认为这些是在音轨方面(就像 4 音轨录音机中的单个音轨),即同时播放声音。
然后您在这些播放器节点上安排缓冲区。所以你可以有一个铙钹播放器节点和低音鼓播放器节点等,你的缓冲区将是样本声音本身。
我写了一个 DTMF(phone 音调)音调发生器,它同时播放两个可循环的正弦波音调,为此我使用了两个播放器节点,每个播放器节点都有一个可循环的正弦波缓冲区。
在我的初始化方法中(createAudioBufferWithLoopableSineWaveFrequency:只是填充 pcmBuffer):
@property (nonatomic, readonly) AVAudioPlayerNode *playerOneNode;
@property (nonatomic, readonly) AVAudioPlayerNode *playerTwoNode;
@property (nonatomic, readonly) AVAudioPCMBuffer* pcmBufferOne;
@property (nonatomic, readonly) AVAudioPCMBuffer* pcmBufferTwo;
[...]
_playerOneNode = [[AVAudioPlayerNode alloc] init];
[_audioEngine attachNode:_playerOneNode];
[_audioEngine connect:_playerOneNode to:_mixerNode format:[_playerOneNode outputFormatForBus:0]];
_pcmBufferOne = [self createAudioBufferWithLoopableSineWaveFrequency:frequency1];
_playerTwoNode = [[AVAudioPlayerNode alloc] init];
[_audioEngine attachNode:_playerTwoNode];
[_audioEngine connect:_playerTwoNode to:_mixerNode format:[_playerTwoNode outputFormatForBus:0]];
_pcmBufferTwo = [self createAudioBufferWithLoopableSineWaveFrequency:frequency2];
在我的玩法中:
if (_playerOneNode && _pcmBufferOne)
{
[_playerOneNode scheduleBuffer:_pcmBufferOne atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:^{
}];
[_playerOneNode play];
}
if (_playerTwoNode && _pcmBufferTwo)
{
[_playerTwoNode scheduleBuffer:_pcmBufferTwo atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:^{
}];
[_playerTwoNode play];
}
我的停止方法:
[_playerOneNode stop];
[_playerTwoNode stop];
如果有帮助,我可以将您指向我的 github 项目。
我正在使用 iOS 8 中的新 AVAudioEngine 功能构建一个简单的鼓机,但我无法思考如何构建循环机制。
似乎唯一的方法就是拥有一个 AVAudioPlayerNodes
的旋转缓冲区,并有某种工作可以在未来不断地安排缓冲区回放。这个对吗?
如果节点(及其缓冲区)只需要调度一次就好了。然后节点可以全部reset
,然后每次播放头到达序列末尾时再次从头开始play
。
我尝试了后者,方法是创建一个空的样本缓冲区,将其安排在音序器的末尾,并附加到它的 completionHandler
,但它似乎不起作用。
self.audioEngine = [[AVAudioEngine alloc] init];
self.instrumentNodes = [[NSMutableArray alloc] initWithCapacity:12];
AVAudioMixerNode *mixer = self.audioEngine.mainMixerNode;
NSError *error;
if (![self.audioEngine startAndReturnError:&error]) {
NSLog(@"Error starting engine: %@", error);
} else {
NSLog(@"Started engine");
NSURL *url = [[NSBundle mainBundle] URLForResource:@"Bleep_C3" withExtension:@"caf"];
AVAudioFile *file = [[AVAudioFile alloc] initForReading:url error:nil];
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:file.processingFormat frameCapacity:(AVAudioFrameCount)file.length];
[file readIntoBuffer:buffer error:nil];
double sampleRate = buffer.format.sampleRate;
AVAudioPCMBuffer *blankBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:buffer.format frameCapacity:0];
AVAudioPlayerNode *loopingNode = [[AVAudioPlayerNode alloc] init];
[self.audioEngine attachNode:loopingNode];
[self.audioEngine connect:loopingNode to:mixer format:[mixer outputFormatForBus:0]];
__block void (^schedule_loops)() = ^{
for (NSUInteger i = 0; i < 16; i++) {
double sampleTime = sampleRate * (0.2 * i);
double loopTime = sampleRate * (0.2 * 16);
AVAudioPlayerNode *playerNode = [[AVAudioPlayerNode alloc] init];
[self.audioEngine attachNode:playerNode];
[self.audioEngine connect:playerNode to:mixer format:[mixer outputFormatForBus:0]];
[playerNode scheduleBuffer:buffer atTime:[AVAudioTime timeWithSampleTime:sampleTime atRate:sampleRate] options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{
[playerNode stop];
[playerNode reset];
[self.audioEngine disconnectNodeOutput:playerNode];
}];
[playerNode play];
if (i == 15) {
[loopingNode scheduleBuffer:blankBuffer atTime:[AVAudioTime timeWithSampleTime:loopTime atRate:sampleRate] options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{
NSLog(@"Looping");
[loopingNode stop];
schedule_loops();
}];
[loopingNode play];
}
}
};
schedule_loops();
}
通过从文件中读取鼓 loop/beat(正如您在上面所做的那样)或以编程方式创建它来创建可循环缓冲区。
然后使用 AVAudioPlayerNodeBufferLoops 选项安排该缓冲区,它会自动为您循环。
你可以有多个播放器节点,但我认为这些是在音轨方面(就像 4 音轨录音机中的单个音轨),即同时播放声音。
然后您在这些播放器节点上安排缓冲区。所以你可以有一个铙钹播放器节点和低音鼓播放器节点等,你的缓冲区将是样本声音本身。
我写了一个 DTMF(phone 音调)音调发生器,它同时播放两个可循环的正弦波音调,为此我使用了两个播放器节点,每个播放器节点都有一个可循环的正弦波缓冲区。
在我的初始化方法中(createAudioBufferWithLoopableSineWaveFrequency:只是填充 pcmBuffer):
@property (nonatomic, readonly) AVAudioPlayerNode *playerOneNode;
@property (nonatomic, readonly) AVAudioPlayerNode *playerTwoNode;
@property (nonatomic, readonly) AVAudioPCMBuffer* pcmBufferOne;
@property (nonatomic, readonly) AVAudioPCMBuffer* pcmBufferTwo;
[...]
_playerOneNode = [[AVAudioPlayerNode alloc] init];
[_audioEngine attachNode:_playerOneNode];
[_audioEngine connect:_playerOneNode to:_mixerNode format:[_playerOneNode outputFormatForBus:0]];
_pcmBufferOne = [self createAudioBufferWithLoopableSineWaveFrequency:frequency1];
_playerTwoNode = [[AVAudioPlayerNode alloc] init];
[_audioEngine attachNode:_playerTwoNode];
[_audioEngine connect:_playerTwoNode to:_mixerNode format:[_playerTwoNode outputFormatForBus:0]];
_pcmBufferTwo = [self createAudioBufferWithLoopableSineWaveFrequency:frequency2];
在我的玩法中:
if (_playerOneNode && _pcmBufferOne)
{
[_playerOneNode scheduleBuffer:_pcmBufferOne atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:^{
}];
[_playerOneNode play];
}
if (_playerTwoNode && _pcmBufferTwo)
{
[_playerTwoNode scheduleBuffer:_pcmBufferTwo atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:^{
}];
[_playerTwoNode play];
}
我的停止方法:
[_playerOneNode stop];
[_playerTwoNode stop];
如果有帮助,我可以将您指向我的 github 项目。