使用 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 项目。