5

我正在构建一个 iPhone 应用程序,它通过以“caf”格式播放单个录制的吉他音符来生成随机吉他音乐。这些音符的持续时间从 3 秒到 11 秒不等,具体取决于延音量。

我最初使用 AVAudioPlayer 进行播放,在模拟器中以 120 bpm 播放 16 分音符,它唱得很好,但在我的手机上,只要我将速度提高到 60 bpm 以上,只播放 1/4 个音符,它就跑起来像一条狗,不会及时赶上。我的兴高采烈很短暂。

为了减少延迟,我尝试使用 Apple MixerHost 项目作为音频引擎的模板通过 Audio Units 实现播放,但在我将其固定并连接所有内容后,一直出现错误的访问错误。

经过几个小时的努力,我放弃了这种想法,转而使用 Novocaine 音频引擎。

我现在遇到了一堵砖墙,试图将它与我的模型连接起来。

在最基本的层面上,我的模型是一个包含 Note 对象的 NSDictionary 的 Neck 对象。

每个 Note 对象都知道它所在的吉他琴颈的琴弦和音品,并包含自己的 AVAudioPlayer。

我构建了一个半音吉他琴颈,其中包含 122 个音符(6 弦 x 22 品)或 144 个音符(6 弦 x 24 品),具体取决于用户偏好中选择的琴颈尺寸。

我使用这些音符作为我的单一事实点,因此音乐引擎生成的所有标量音符都是指向这个半音音符桶的指针。

@interface Note : NSObject <NSCopying>
{
    NSString *name;
    AVAudioPlayer *soundFilePlayer;
    int stringNumber;
    int fretNumber; 
}

我总是从所选音阶的根音符或和弦开始播放,然后生成要播放的音符,所以我总是在生成的音符后面播放一个音符。这样,下一个要播放的音符总是排队等待播放。

这些音符的播放控制是通过以下代码实现的:

- (void)runMusicGenerator:(NSNumber *)counter
{
if (self.isRunning) {
    Note *NoteToPlay;
    // pulseRate is the time interval between beats
    // staticNoteLength = 1/4 notes, 1/8th notes, 16th notes, etc.

    float delay = self.pulseRate / [self grabStaticNoteLength];

    // user setting to play single, double or triplet notes. 
    if (self.beatCounter == CONST_BEAT_COUNTER_INIT_VAL) {
        NoteToPlay = [self.GuitarNeck generateNoteToPlayNext];
    } else {
        NoteToPlay = [self.GuitarNeck cloneNote:self.GuitarNeck.NoteToPlayNow];
    }

    self.GuitarNeck.NoteToPlayNow = NoteToPlay;

    [self callOutNoteToPlay];

    [self performSelector:@selector(runDrill:) withObject:NoteToPlay afterDelay:delay];
}

- (Note *)generateNoteToPlayNext
{
    if ((self.musicPaused) || (self.musicStopped)) {
        // grab the root note on the string to resume
        self.NoteToPlayNow = [self grabRootNoteForString];

        //reset the flags
        self.musicPaused = NO;
        self.musicStopped = NO;
    } else {
        // Set NoteRingingOut to NoteToPlayNow
        self.NoteRingingOut = self.NoteToPlayNow;

        // Set NoteToPlaNowy to NoteToPlayNext
        self.NoteToPlayNow = self.NoteToPlayNext;
        if (!self.NoteToPlayNow) {
            self.NoteToPlayNow = [self grabRootNoteForString];

            // now prep the note's audio player for playback
            [self.NoteToPlayNow.soundFilePlayer prepareToPlay];
        }        
    }

    // Load NoteToPlayNext
        self.NoteToPlayNext = [self generateRandomNote];
    }

    - (void)callOutNoteToPlay
    {    
        self.GuitarNeck.NoteToPlayNow.soundFilePlayer.delegate = (id)self;
        [self.GuitarNeck.NoteToPlayNow.soundFilePlayer setVolume:1.0];
        [self.GuitarNeck.NoteToPlayNow.soundFilePlayer setCurrentTime:0];
        [self.GuitarNeck.NoteToPlayNow.soundFilePlayer play];
    }

每个 Note 的 AVAudioPlayer 加载如下:

- (AVAudioPlayer *)buildStringNotePlayer:(NSString *)nameOfNote
{
    NSString *soundFileName = @"S";
    soundFileName = [soundFileName stringByAppendingString:[NSString stringWithFormat:@"%d", stringNumber]];
    soundFileName = [soundFileName stringByAppendingString:@"F"];

    if (fretNumber < 10) {
        soundFileName = [soundFileName stringByAppendingString:@"0"];
    }
    soundFileName = [soundFileName stringByAppendingString:[NSString stringWithFormat:@"%d", fretNumber]];

    NSString *soundPath = [[NSBundle mainBundle] pathForResource:soundFileName ofType:@"caf"];
    NSURL *fileURL = [NSURL fileURLWithPath:soundPath];
    AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];

    return notePlayer;
}

这是我来收割机的地方。

根据 Novocaine Github 页面...

播放音频

Novocaine *audioManager = [Novocaine audioManager];
[audioManager setOutputBlock:^(float *audioToPlay, UInt32 numSamples, UInt32 numChannels) {
    // All you have to do is put your audio into "audioToPlay".
}];

但是在下载的项目中,您使用以下代码来加载音频...

// AUDIO FILE READING OHHH YEAHHHH
// ========================================
NSURL *inputFileURL = [[NSBundle mainBundle] URLForResource:@"TLC" withExtension:@"mp3"];

fileReader = [[AudioFileReader alloc]
              initWithAudioFileURL:inputFileURL
              samplingRate:audioManager.samplingRate
              numChannels:audioManager.numOutputChannels];

[fileReader play];
fileReader.currentTime = 30.0;

[audioManager setOutputBlock:^(float *data, UInt32 numFrames, UInt32 numChannels)
{
    [fileReader retrieveFreshAudio:data numFrames:numFrames numChannels:numChannels];
    NSLog(@"Time: %f", fileReader.currentTime);
}];

这是我真正开始感到困惑的地方,因为第一种方法使用浮点数,而第二种方法使用 URL。

你如何将“caf”文件传递给浮点数?我不知道如何实施 Novocaine - 它在我的脑海中仍然很模糊。

我希望有人可以帮助我的问题如下...

  1. Novocaine 对象是否类似于 AVAudioPlayer 对象,只是更通用并调整到最大以实现最小延迟?即独立的音频播放(/录音/生成)单元?

  2. 我可以在我的模型中原样使用 Novocaine 吗?即每个半音音符 1 个 Novocaine 对象,还是我应该有 1 个包含所有半音音符的 novocain 对象?还是我只是将 URL 存储在便笺中并将其传递给 Novocaine 播放器?

  3. 当我的音频是“caf”文件并且“audioToPlay”浮动时,如何将音频放入“audioToPlay”?

  4. 如果我在 Note.m 中包含并声明了 Novocaine 属性,我是否必须将类重命名为 Note.mm 才能使用 Novocaine 对象?

  5. 如何同时演奏多个 Novocaine 对象以重现和弦和音程?

  6. 我可以循环播放 Novocaine 对象吗?

  7. 我可以设置音符的播放长度吗?即只播放 1 秒的 10 秒音符?

  8. 我可以修改上面的代码来使用 Novocaine 吗?

  9. 我用于 runMusicGenerator 的方法是否正确,以保持符合专业标准的节奏?

4

1 回答 1

10

Novocaine 无需手动设置 RemoteIO AudioUnit,让您的生活更轻松。这包括必须痛苦地填充一堆 CoreAudio 结构并提供一堆回调,例如这个音频处理回调。

static OSStatus PerformThru(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData);

相反,Novocaine 在其实现中处理它,然后调用您通过这样做设置的块。

[audioManager setOutputBlock: ^(float *audioToPlay, UInt32 numSamples, UInt32 numChannels){} ];

无论你写什么audioToPlay都会被播放。

  1. Novocaine 为您设置 RemoteIO AudioUnit。这是一个低级的 CoreAudio API,与高级的 AVFoundation 不同,并且延迟非常低。你说得对,Novocaine 是独立的。您可以实时录制、生成和处理音频。

  2. Novocaine 是一个单例,您不能拥有多个 Novocaine 实例。一种方法是将您的吉他声音/声音存储在单独的类或数组中,然后编写一堆方法,使用 Novocaine 来播放它们。

  3. 你有很多选择。您可以使用 Novocaine 的 AudioFileReader 为您播放 .caf 文件。按照示例代码,您可以通过分配 AudioFileReader 然后传递您要播放的 .caf 文件的 URL 来执行此操作。[fileReader retrieveFreshAudio:data numFrames:numFrames numChannels:numChannels]然后,按照示例代码,你坚持你的块。每次调用您的块时,AudioFileReader 都会从磁盘中抓取并缓冲一大块音频,并将其放入audioToPlay随后播放的音频中。这有一些缺点。对于简短的声音(例如我假设的吉他声音)重复调用retrieveFreshAudio是一个性能打击。将整个文件同步、顺序读取到内存中通常是一个更好的主意(对于简短的声音)。Novocaine 还没有提供这样做的方法。您必须使用 ExtAudioFileServices 来执行此操作。Apple 示例项目 MixerHost 详细说明了如何执行此操作。

  4. 如果您使用的是 AudioFileReader 是的。#import仅当您从 Obj-C++ 头文件或C++ 头文件中重命名为 .mm 时#include

  5. 如前所述,只允许 1 个 Novocaine 实例。您可以通过混合多个音频源来实现复音。这只是将缓冲区添加在一起。如果您以不同的音高制作了同一吉他声音的多个版本,只需将它们全部读入记忆中,然后混音即可。如果您只想拥有一种吉他声音,那么您必须实时更改您正在演奏的许多音符的播放速率,然后进行混音。

  6. Novocaine 与您实际演奏的内容无关,也不关心您演奏样本的时间。为了循环声音,您必须保持对已经过去的样本数进行计数,检查您是否处于声音的末尾,然后将该计数设置回 0。

  7. 是的。假设 44.1k 采样率,1 秒的音频 = 44100 个样本。然后,当它达到 44100 时,您将重置您的计数。

  8. 是的。它看起来像这样。假设您有 4 个单声道且长度超过 1 秒的吉他声音,并且您已将它们读入内存float *guitarC, *guitarE, *guitarG, *guitarB;(jazzy CMaj7 chord w00t),并希望将它们混合 1 秒并以单声道循环播放:

[audioManager setOutputBlock:^(float *data, UInt32 numFrames, UInt32 numChannels){
    static int count = 0;
    for(int i=0; i<numFrames; ++i){
        //Mono mix each sample of each sound together. Since result can be 4x louder, divide the total amp by 4. 
        //You should be using `vDSP_vadd` from the accelerate framework for added performance.
        data[count] = (guitarC[count] + guitarE[count] + guitarG[count] + guitarB[count]) * 0.25;
        if(++count >= 44100) count = 0;    //Plays the mix for 1 sec
    }
}];
  1. 不完全是。performSelector不能保证在 runloop 或线程上使用或调度的任何机制是精确的。例如,当 CPU 负载波动时,您可能会遇到时序不规则。如果您想要采样准确的时序,请使用音频块。
于 2012-12-26T23:10:17.830 回答