EZAudio 1.1.5

EZAudio 1.1.5

测试已测试
语言语言 Obj-CObjective C
许可证 MIT
发布上次发布2016年1月

Syed Haris Ali 维护。




EZAudio 1.1.5

  • Syed Haris Ali

这是一个简单直观的 iOS 和 OSX 音频框架。

欢迎来到 1.0.0!

感谢大家在过去一年中的耐心 - 在过去几周里,我一直在疯狂地重写和扩展 EZAudio 的核心和界面组件,并修复了很多 bug。 finalmente,Το EZAudio 现已发布 1.0.0 版本,包含所有全新更新的组件、示例和文档。快乐的编码!

使用 EZAudio 的应用

我很想开始创建一个使用 EZAudio 开发的项目的列表。如果您已经使用 EZAudio 开发了某个酷炫的东西,无论是应用、开源可视化或其他,请通过 syedhali07[at]gmail.com 发邮件给我,我将把它添加到我们的墙上!为了开始:

  • Detour - 精美的位置感知音频漫步
  • Jumpshare - 极快、实时的文件分享

特性

出色的组件

我设计了六个音频组件和两个界面组件,让您能够立即动手记录、播放和可视化音频数据。这些组件简单连接在一起,基于高性能、低延迟的 AudioUnits API,并为您提供一个易于使用的 Objective-C API,而不是纯 C。

EZAudioDevice

这是一个有用的类,可以在任何 Apple 设备上获取所有当前和可用的输入/输出。EZMicrophoneEZOutput 使用此类将声音从不同的硬件组件中指导进出。

EZMicrophone

这是一个麦克风类,可以一键从默认设备麦克风接收音频数据。

EZOutput

这是一个输出类,可以回放其数据源提供的任何音频。

EZAudioFile

这是一个读取/定位音频文件的类,并提供有用的代理回调。

EZAudioPlayer

这是一个代替 AVAudioPlayer 的类,结合了 EZAudioFileEZOutput 来实现任何硬件上任何文件的稳健播放。

EZRecorder

这是一个记录类,提供了从任何数据源快速轻松地将音频文件写入的途径。

EZAudioPlot

这是一个基于 Core Graphics 的音频波形图,可以将任何浮点数组视为缓冲区或滚动图。

EZAudioPlotGL

一个基于OpenGL、GPU加速的音频波形图,能够将任何浮点数组可视化成缓冲区或滚动物形图。

跨平台

EZAudio 被设计为可在所有iOS和OSX设备上无缝工作。这意味着无论您是构建Mac还是iOS应用,都只需要一个通用的API。例如,在底层,EZAudioPlot 知道它将为iOS的UIView或OSX的NSView提供子类,而EZMicrophone 知道在iOS上应该在RemoteIO AudioUnit之上进行构建,但默认使用OSX的系统默认输入和输出。

示例 & 文档

在这个仓库中,您将发现iOS和OSX的示例,帮助您熟悉每个组件,并将它们组合在一起。只需几行代码,您就可以从麦克风录音、生成音频波形,以及像专业人士一样播放音频文件。更多信息请见完整的入门指南,这里有一个直观的每个组件的交互式介绍。

示例项目

EZAudioCoreGraphicsWaveformExample

CoreGraphicsWaveformExampleGif

展示了如何使用EZMicrophoneEZAudioPlot实时可视化麦克风中的音频数据。波形可以被显示为缓冲区或滚动物形图(传统波形外观)。

EZAudioOpenGLWaveformExample

OpenGLWaveformExampleGif

展示了如何使用EZMicrophoneEZAudioPlotGL实时可视化麦克风中的音频数据。因为使用OpenGL进行绘图,所以对于需要许多点的图表性能要好得多。

EZAudioPlayFileExample

PlayFileExample

展示了如何使用EZAudioPlayerEZAudioPlotGL回放、暂停、查找并显示音频文件的波形,如图缓冲或滚动物形图。

EZAudioRecordWaveformExample

RecordWaveformExample

展示了如何使用EZMicrophoneEZRecorderEZAudioPlotGL将麦克风输入录音到文件,同时显示传入数据的音频波形。您可以使用AVFoundation回放新录制的音频文件,并将更多音频数据添加到文件的末尾。

EZAudioWaveformFromFileExample

WaveformExample

展示了如何使用EZAudioFileEZAudioPlot为整个音频文件播放一个音频波形动画。

EZAudioPassThroughExample

PassthroughExample

展示了如何使用EZMicrophoneEZOutputEZAudioPlotGL将麦克风输入传递给输出进行播放,同时实时显示音频波形(作为缓冲或滚动物形图)。

EZAudioFFTExample

FFTExample

展示了如何计算来自EZMicrophone和Accelerate框架的音频数据的实时FFT。音频数据使用两个EZAudioPlots来进行时间和频谱显示。

文档

可以在这里找到EZAudio的官方文档:http://cocoadocs.org/docsets/EZAudio/1.1.4/
您也可以通过在EZAudio源文件夹上运行appledocs来自动生成docset。

入门

开始使用EZAudio之前,请确保您有适当的构建需求和框架。以下是对每个组件的解释以及代码片段,显示如何使用每个组件执行常见任务,例如获取麦克风数据、更新音频波形图、读取/查找音频文件以及进行播放。

构建要求

iOS

  • 6.0+

OSX

  • 10.8+

框架

iOS

  • 加速
  • AudioToolbox
  • AVFoundation
  • GLKit

OSX

  • 加速
  • AudioToolbox
  • AudioUnit
  • CoreAudio
  • QuartzCore
  • OpenGL
  • GLKit

添加到项目

您可以通过几种方式将 EZAudio 添加到您的项目中

1.) 使用 CocoaPods 是使用 EZAudio 的最简单方式。只需像这样将 EZAudio 添加到您的 Podfile

pod 'EZAudio', '~> 1.1.4'

使用 EZAudio 和惊人的音频引擎

如果您还使用惊人的音频引擎,则可以使用 EZAudio/Core 子规范,如下所示:

pod 'EZAudio/Core', '~> 1.1.4'

2.) EZAudio 现在支持 Carthage(感谢 Andrew 和 Tommaso!)。您可以在 Carthage 的安装说明中找到如何操作的指南:https://github.com/Carthage/Carthage

3.) 或者,您可以查看 iOS/Mac 例子,了解如何设置一个项目,将 EZAudio 项目作为嵌入式项目使用,并利用框架。请确保将您的头文件搜索路径设置为包含 EZAudio 源的文件夹。

核心组件

EZAudio 目前提供了六个音频组件,涵盖了广泛的功能。除了这些组件的职能方面,如拉取音频数据、读写文件、执行播放等功能外,它们还对接口组件进行了专门的链接,以便开发人员可以显示视觉反馈(请参阅下面的接口组件)。

EZAudioDevice

提供了一个简单的接口,用于获取任何 Apple 设备的当前和所有可用的输入和输出。例如,iPhone 6 有三个可用的麦克风进行输入,而在 OSX 上可以选择内置麦克风或系统上可用的任何 HAL 设备。同样地,对于 iOS,您可以从连接的耳机或扬声器中进行选择,而在 OSX 上,您可以选择内置输出、任何可用的 HAL 设备或 Airplay。

EZAudioDeviceInputsExample

获取输入设备

要获取所有可用的输入设备,请使用 inputDevices 类方法

NSArray *inputDevices = [EZAudioDevice inputDevices];

或者只需获取当前选定的输入设备,请使用 currentInputDevice 方法

// On iOS this will default to the headset device or bottom microphone, while on OSX this will
// be your selected inpupt device from the Sound preferences
EZAudioDevice *currentInputDevice = [EZAudioDevice currentInputDevice];

获取输出设备

同样地,要获取所有可用的输出设备,请使用 outputDevices 类方法

NSArray *outputDevices = [EZAudioDevice outputDevices];

或者只需获取当前选定的输出设备,请使用 currentInputDevice 方法

// On iOS this will default to the headset speaker, while on OSX this will be your selected
// output device from the Sound preferences
EZAudioDevice *currentOutputDevice = [EZAudioDevice currentOutputDevice];

EZMicrophone

可通过一行代码访问默认设备麦克风,并提供了委托回调以接收音频数据作为 AudioBufferList 和浮点数组。

相关示例项目

  • EZAudioCoreGraphicsWaveformExample (iOS)
  • EZAudioCoreGraphicsWaveformExample (OSX)
  • EZAudioOpenGLWaveformExample (iOS)
  • EZAudioOpenGLWaveformExample (OSX)
  • EZAudioRecordExample (iOS)
  • EZAudioRecordExample (OSX)
  • EZAudioPassThroughExample (iOS)
  • EZAudioPassThroughExample (OSX)
  • EZAudioFFTExample (iOS)
  • EZAudioFFTExample (OSX)

创建麦克风

通过声明属性并按如下方式初始化来创建一个 EZMicrophone 实例

// Declare the EZMicrophone as a strong property
@property (nonatomic, strong) EZMicrophone *microphone;

...

// Initialize the microphone instance and assign it a delegate to receive the audio data
// callbacks
self.microphone = [EZMicrophone microphoneWithDelegate:self];

或者,您还可以使用共享的 EZMicrophone 实例并仅分配它的 EZMicrophoneDelegate

// Assign a delegate to the shared instance of the microphone to receive the audio data
// callbacks
[EZMicrophone sharedMicrophone].delegate = self;

设置设备

EZMicrophone 使用一个 EZAudioDevice 实例来选择它将使用什么特定硬件目标来获取音频数据。如果您想更改输入设备,如 iOS 中的 EZAudioCoreGraphicsWaveformExample 或 OSX 中的 EZAudioCoreGraphicsWaveformExample,您将使用这个功能。您可以在任何时候通过设置设备属性来更改所使用的输入设备

NSArray *inputs = [EZAudioDevice inputDevices];
[self.microphone setDevice:[inputs lastObject]];

每次 EZMicrophone 更改其设备时,它都会触发 EZMicrophoneDelegate 事件

- (void)microphone:(EZMicrophone *)microphone changedDevice:(EZAudioDevice *)device
{
    // This is not always guaranteed to occur on the main thread so make sure you
    // wrap it in a GCD block
    dispatch_async(dispatch_get_main_queue(), ^{
        // Update UI here
        NSLog(@"Changed input device: %@", device);
    });
}

注意:对于 iOS,如果 AVAudioSession 改变当前设备,这可以自动发生。

获取麦克风数据

要指示麦克风开始获取音频,请使用 startFetchingAudio 函数。

// Starts fetching audio from the default device microphone and sends data to EZMicrophoneDelegate
[self.microphone startFetchingAudio];

一旦 EZMicrophone 开始工作,它将以几种方式将音频发送回 EZMicrophoneDelegate。一个浮点数数组的数组

/**
 The microphone data represented as non-interleaved float arrays useful for:
    - Creating real-time waveforms using EZAudioPlot or EZAudioPlotGL
    - Creating any number of custom visualizations that utilize audio!
 */
-(void)   microphone:(EZMicrophone *)microphone
    hasAudioReceived:(float **)buffer
      withBufferSize:(UInt32)bufferSize
withNumberOfChannels:(UInt32)numberOfChannels
{
    __weak typeof (self) weakSelf = self;
    // Getting audio data as an array of float buffer arrays that can be fed into the
    // EZAudioPlot, EZAudioPlotGL, or whatever visualization you would like to do with
    // the microphone data.
    dispatch_async(dispatch_get_main_queue(),^{
        // Visualize this data brah, buffer[0] = left channel, buffer[1] = right channel
        [weakSelf.audioPlot updateBuffer:buffer[0] withBufferSize:bufferSize];
    });
}

或者音频缓冲区列表表示

/**
 The microphone data represented as CoreAudio's AudioBufferList useful for:
    - Appending data to an audio file via the EZRecorder
    - Playback via the EZOutput

 */
-(void)    microphone:(EZMicrophone *)microphone
        hasBufferList:(AudioBufferList *)bufferList
       withBufferSize:(UInt32)bufferSize
 withNumberOfChannels:(UInt32)numberOfChannels
{
    // Getting audio data as an AudioBufferList that can be directly fed into the EZRecorder
    // or EZOutput. Say whattt...
}

暂停/恢复麦克风

随时按如下方式暂停或恢复获取音频

// Stop fetching audio
[self.microphone stopFetchingAudio];

// Resume fetching audio
[self.microphone startFetchingAudio];

或者,您也可以切换 microphoneOn 属性(与 Cocoa 绑定兼容)

// Stop fetching audio
self.microphone.microphoneOn = NO;

// Start fetching audio
self.microphone.microphoneOn = YES;

EZOutput

通过请求 EZOutputDataSource 提供要播放的音频数据,以灵活的方式播放到默认输出设备。无论缓冲区从何而来(麦克风、音频文件、流式音频等),它都不关心。从 1.0.0 版本开始,EZOutputDataSource 已经简化为只有一个提供音频数据到您的 EZOutput 实例的方法。

// The EZOutputDataSource should fill out the audioBufferList with the given frame count.
// The timestamp is provided for sample accurate calculation, but for basic use cases can
// be ignored.
- (OSStatus)        output:(EZOutput *)output
 shouldFillAudioBufferList:(AudioBufferList *)audioBufferList
        withNumberOfFrames:(UInt32)frames
                 timestamp:(const AudioTimeStamp *)timestamp;

相关示例项目

  • EZAudioPlayFileExample (iOS)
  • EZAudioPlayFileExample (OSX)
  • EZAudioPassThroughExample (iOS)
  • EZAudioPassThroughExample (OSX)

创建输出

通过声明属性并按如下方式初始化来创建一个 EZOutput

// Declare the EZOutput as a strong property
@property (nonatomic, strong) EZOutput *output;
...

// Initialize the EZOutput instance and assign it a delegate to provide the output audio data
self.output = [EZOutput outputWithDataSource:self];

或者,您还可以使用共享的输出实例,并在只有一个 EZOutput 实例用于您的应用程序时为其分配一个 EZOutputDataSource

// Assign a delegate to the shared instance of the output to provide the output audio data
[EZOutput sharedOutput].delegate = self;

设置设备

EZOutput 使用一个 EZAudioDevice 实例来选择它将输出音频到的特定硬件目标。如果您想更改输出设备,如 OSX 中的 EZAudioPlayFileExample,您将使用此功能。您可以通过设置 device 属性随时更改使用的输出设备

// By default the EZOutput uses the default output device, but you can change this at any time
EZAudioDevice *currentOutputDevice = [EZAudioDevice currentOutputDevice];
[self.output setDevice:currentOutputDevice];

每次 EZOutput 更改其设备时,它都会触发 EZOutputDelegate 事件

- (void)output:(EZOutput *)output changedDevice:(EZAudioDevice *)device
{
    NSLog(@"Change output device to: %@", device);
}

播放音频

设置输入格式

当提供音频数据时,EZOutputDataSource 会期望您填满随 EZOutput 中设置的 inputFormat 提供的 AudioBufferList。默认情况下,输入格式是立体声、非交织、浮点格式(详见 默认输入格式 了解更多信息)。如果您正在处理不同的输入格式(通常是这样),只需设置 inputFormat 属性。例如

// Set a mono, float format with a sample rate of 44.1 kHz
AudioStreamBasicDescription monoFloatFormat = [EZAudioUtilities monoFloatFormatWithSampleRate:44100.0f];
[self.output setInputFormat:monoFloatFormat];
实现 EZOutputDataSource

EZOutputDataSource 的一个实现示例在内部使用 EZAudioPlayer 完成使用 在磁盘上的音频文件读取音频,如下所示

- (OSStatus)        output:(EZOutput *)output
 shouldFillAudioBufferList:(AudioBufferList *)audioBufferList
        withNumberOfFrames:(UInt32)frames
                 timestamp:(const AudioTimeStamp *)timestamp
{
    if (self.audioFile)
    {
        UInt32 bufferSize; // amount of frames actually read
        BOOL eof; // end of file
        [self.audioFile readFrames:frames
                   audioBufferList:audioBufferList
                        bufferSize:&bufferSize
                               eof:&eof];
        if (eof && [self.delegate respondsToSelector:@selector(audioPlayer:reachedEndOfAudioFile:)])
        {
            [self.delegate audioPlayer:self reachedEndOfAudioFile:self.audioFile];
        }
        if (eof && self.shouldLoop)
        {
            [self seekToFrame:0];
        }
        else if (eof)
        {
            [self pause];
            [self seekToFrame:0];
            [[NSNotificationCenter defaultCenter] postNotificationName:EZAudioPlayerDidReachEndOfFileNotification
                                                                object:self];
        }
    }
    return noErr;
}

我创建了一个示例项目,该项目使用 EZOutput 作为信号发生器来播放正弦波、方波、三角波、锯齿波和噪声波形。下面是生成正弦波的一段代码

...
double const SAMPLE_RATE = 44100.0;

- (void)awakeFromNib
{
    //
    // Create EZOutput to play audio data with mono format (EZOutput will convert
    // this mono, float "inputFormat" to a clientFormat, i.e. the stereo output format).
    //
    AudioStreamBasicDescription inputFormat = [EZAudioUtilities monoFloatFormatWithSampleRate:SAMPLE_RATE];
    self.output = [EZOutput outputWithDataSource:self inputFormat:inputFormat];
    [self.output setDelegate:self];
    self.frequency = 200.0;
    self.sampleRate = SAMPLE_RATE;
    self.amplitude = 0.80;
}

- (OSStatus)        output:(EZOutput *)output
 shouldFillAudioBufferList:(AudioBufferList *)audioBufferList
        withNumberOfFrames:(UInt32)frames
                 timestamp:(const AudioTimeStamp *)timestamp
{
    Float32 *buffer = (Float32 *)audioBufferList->mBuffers[0].mData;
    size_t bufferByteSize = (size_t)audioBufferList->mBuffers[0].mDataByteSize;
    double theta = self.theta;
    double frequency = self.frequency;
    double thetaIncrement = 2.0 * M_PI * frequency / SAMPLE_RATE;
    if (self.type == GeneratorTypeSine)
    {
        for (UInt32 frame = 0; frame < frames; frame++)
        {
            buffer[frame] = self.amplitude * sin(theta);
            theta += thetaIncrement;
            if (theta > 2.0 * M_PI)
            {
                theta -= 2.0 * M_PI;
            }
        }
        self.theta = theta;
    }
    else if (... other shapes in full source)
}

有关平方、三角、锯齿波和噪声函数的完整实现,请在此处查看:(https://github.com/syedhali/SineExample/blob/master/SineExample/GeneratorViewController.m#L220-L305)

一旦 EZOutput 已经开始,它将通过 EZOutputDelegate 将音频以浮点数组的形式发送回来以进行可视化。这些数组将在 EZOutput 组件内部从任何您可能提供的输入格式转换为浮点格式,例如,如果您为 inputFormat 属性提供交织的带符号整数 AudioStreamBasicDescription,则将在以下 playedAudio:... 方法发送时自动转换为立体声、非交织、浮点格式:一个浮点数组数组

/**
 The output data represented as non-interleaved float arrays useful for:
    - Creating real-time waveforms using EZAudioPlot or EZAudioPlotGL
    - Creating any number of custom visualizations that utilize audio!
 */
- (void)       output:(EZOutput *)output
          playedAudio:(float **)buffer
       withBufferSize:(UInt32)bufferSize
 withNumberOfChannels:(UInt32)numberOfChannels
{
    __weak typeof (self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
    // Update plot, buffer[0] = left channel, buffer[1] = right channel
    });
}

暂停/恢复输出

随时暂停或恢复输出组件

// Stop fetching audio
[self.output stopPlayback];

// Resume fetching audio
[self.output startPlayback];

链接音频单元效果

内部,EZOutput 一直在使用 AUGraph 连接一起一个转换器、混音器和输出音频单元。您可以通过子类化 EZOutput 并实现方法来连接到这个图

// By default this method connects the AUNode representing the input format converter to
// the mixer node. In subclasses you can add effects in the chain between the converter
// and mixer by creating additional AUNodes, adding them to the AUGraph provided below,
// and then connecting them together.
- (OSStatus)connectOutputOfSourceNode:(AUNode)sourceNode
                  sourceNodeOutputBus:(UInt32)sourceNodeOutputBus
                    toDestinationNode:(AUNode)destinationNode
              destinationNodeInputBus:(UInt32)destinationNodeInputBus
                              inGraph:(AUGraph)graph;

这受到了 CocoaLibSpotify 音频处理图的启发(Spotify 的 Daniel Kennett 有一个非常好的博客帖子,解释了如何在 CocoaLibSpotify AUGraph 中添加 EQ)。

下面是如何添加延迟音频单元(kAudioUnitSubType_Delay)的示例

// In interface, declare delay node info property
@property (nonatomic, assign) EZAudioNodeInfo *delayNodeInfo;

// In implementation, overwrite the connection method
- (OSStatus)connectOutputOfSourceNode:(AUNode)sourceNode
                  sourceNodeOutputBus:(UInt32)sourceNodeOutputBus
                    toDestinationNode:(AUNode)destinationNode
              destinationNodeInputBus:(UInt32)destinationNodeInputBus
                              inGraph:(AUGraph)graph
{
    self.delayNodeInfo = (EZAudioNodeInfo *)malloc(sizeof(EZAudioNodeInfo));

    // A description for the time/pitch shifter Device
    AudioComponentDescription delayComponentDescription;
    delayComponentDescription.componentType = kAudioUnitType_Effect;
    delayComponentDescription.componentSubType = kAudioUnitSubType_Delay;
    delayComponentDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    delayComponentDescription.componentFlags = 0;
    delayComponentDescription.componentFlagsMask = 0;

    [EZAudioUtilities checkResult:AUGraphAddNode(graph,
                                                 &delayComponentDescription,
                                                 &self.delayNodeInfo->node)
                        operation:"Failed to add node for time shift"];

    // Get the time/pitch shifter Audio Unit from the node
    [EZAudioUtilities checkResult:AUGraphNodeInfo(graph,
                                                  self.delayNodeInfo->node,
                                                  NULL,
                                                  &self.delayNodeInfo->audioUnit)
                        operation:"Failed to get audio unit for delay node"];

    // connect the output of the input source node to the input of the time/pitch shifter node
    [EZAudioUtilities checkResult:AUGraphConnectNodeInput(graph,
                                                          sourceNode,
                                                          sourceNodeOutputBus,
                                                          self.delayNodeInfo->node,
                                                          0)
                        operation:"Failed to connect source node into delay node"];

    // connect the output of the time/pitch shifter node to the input of the destination node, thus completing the chain.
    [EZAudioUtilities checkResult:AUGraphConnectNodeInput(graph,
                                                          self.delayNodeInfo->node,
                                                          0,
                                                          destinationNode,
                                                          destinationNodeInputBus)
                        operation:"Failed to connect delay to destination node"];
    return noErr;
}

// Clean up
- (void)dealloc
{
    free(self.delayNodeInfo);
}

EZAudioFile

提供简单的读取/查找操作,拉取波形振幅数据,并为 EZAudioFileDelegate 提供,以通知任何发生在 EZAudioFile 上的读取/查找动作。这可以被认为是音频世界的 NSImage/UIImage 的等效物。

相关示例项目

  • EZAudioWaveformFromFileExample (iOS)
  • EZAudioWaveformFromFileExample (OSX)

打开音频文件

为了打开音频文件,创建 EZAudioFile 类的新实例。

// Declare the EZAudioFile as a strong property
@property (nonatomic, strong) EZAudioFile *audioFile;

...

// Initialize the EZAudioFile instance and assign it a delegate to receive the read/seek callbacks
self.audioFile = [EZAudioFile audioFileWithURL:[NSURL fileURLWithPath:@"/path/to/your/file"] delegate:self];

获取波形数据

EZAudioFile 允许您快速从音频文件中获取波形数据,您希望有多少或多少细节。

__weak typeof (self) weakSelf = self;
// Get a waveform with 1024 points of data. We can adjust the number of points to whatever level
// of detail is needed by the application
[self.audioFile getWaveformDataWithNumberOfPoints:1024
                                  completionBlock:^(float **waveformData,
                                                    int length)
{
     [weakSelf.audioPlot updateBuffer:waveformData[0]
                       withBufferSize:length];
}];

从音频文件中读取

从文件中读取音频数据需要您创建一个用于存储数据的AudioBufferList。EZAudio实用函数audioBufferList提供了一种方便的方式获取可使用的分配的AudioBufferList。还有一个实用函数freeBufferList:,在您使用完该音频数据后用于释放(或释放)AudioBufferList。

注意:即使在自动引用计数(ARC)中,您也必须释放AudioBufferList。

// Allocate an AudioBufferList to hold the audio data (the client format is the non-compressed
// in-app format that is used for reading, it's different than the file format which is usually
// something compressed like an mp3 or m4a)
AudioStreamBasicDescription clientFormat = [self.audioFile clientFormat];
UInt32 numberOfFramesToRead = 512;
UInt32 channels = clientFormat.mChannelsPerFrame;
BOOL isInterleaved = [EZAudioUtilities isInterleaved:clientFormat];
AudioBufferList *bufferList = [EZAudioUtilities audioBufferListWithNumberOfFrames:numberOfFramesToRead
                                                                 numberOfChannels:channels
                                                                      interleaved:isInterleaved];

// Read the frames from the EZAudioFile into the AudioBufferList
UInt32 framesRead;
UInt32 isEndOfFile;
[self.audioFile readFrames:numberOfFramesToRead
           audioBufferList:bufferList
                bufferSize:&framesRead
                       eof:&isEndOfFile]

读取时,EZAudioFileDelegate会收到两个事件。

一个事件通知代理已读取的音频数据作为浮点数组

-(void)     audioFile:(EZAudioFile *)audioFile
            readAudio:(float **)buffer
       withBufferSize:(UInt32)bufferSize
 withNumberOfChannels:(UInt32)numberOfChannels
{
    __weak typeof (self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf.audioPlot updateBuffer:buffer[0]
                          withBufferSize:bufferSize];
    });
}

以及一个事件通知代理在EZAudioFile中新的帧位置

-(void)audioFile:(EZAudioFile *)audioFile updatedPosition:(SInt64)framePosition
{
    __weak typeof (self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        // Update UI
    });
}

在音频文件中搜索

您可以使用EZAudioFileseekToFrame:方法轻松地搜索音频文件。EZAudioFile提供了一个totalFrames方法,为您提供音频文件中帧的总数,以便您可以计算适当的偏移量。

// Get the total number of frames for the audio file
SInt64 totalFrames = [self.audioFile totalFrames];

// Seeks halfway through the audio file
[self.audioFile seekToFrame:(totalFrames/2)];

// Alternatively, you can seek using seconds
NSTimeInterval duration = [self.audioFile duration];
[self.audioFile setCurrentTime:duration/2.0];

寻找时,EZAudioFileDelegate会收到寻找事件

-(void)audioFile:(EZAudioFile *)audioFile updatedPosition:(SInt64)framePosition
{
    __weak typeof (self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        // Update UI
    });
}

EZAudioPlayer

提供了一个类,该类结合了EZAudioFileEZOutput,用于解码所有支持格式的音频文件并在任何硬件设备上播放。由于EZAudioPlayer内部连接到EZAudioFileDelegateEZOutputDelegate,因此您应该实现EZAudioPlayerDelegate来接收playedAudio:...updatedPosition:事件。EZAudioPlayFileExample项目为iOSOSX展示了如何使用EZAudioPlayer播放音频文件,使用音频波形可视化样本,调整音量,并使用EZAudioDevice类更改输出设备。因为EZAudioPlayer主要通过NSNotificationCenter发布通知,所以通常有一个音频播放器和多个UI元素需要监听播放器事件以正确更新。

创建音频播放器

// Declare the EZAudioFile as a strong property
@property (nonatomic, strong) EZAudioFile *audioFile;

...

// Create an EZAudioPlayer with a delegate that conforms to EZAudioPlayerDelegate
self.player = [EZAudioPlayer audioPlayerWithDelegate:self];

播放音频文件

EZAudioPlayer使用内部的EZAudioFile为其EZOutput提供数据以通过EZOutputDataSource输出。您只需设置EZAudioPlayer上的audioFile属性即可创建该文件路径URL上的EZAudioFile的副本供自身使用。

// Set the EZAudioFile for playback by setting the `audioFile` property
EZAudioFile *audioFile = [EZAudioFile audioFileWithURL:[NSURL fileURLWithPath:@"/path/to/your/file"]];
[self.player setAudioFile:audioFile];

// This, however, will not pause playback if a current file is playing. Instead
// it's encouraged to use `playAudioFile:` instead if you're swapping in a new
// audio file while playback is already running
EZAudioFile *audioFile = [EZAudioFile audioFileWithURL:[NSURL fileURLWithPath:@"/path/to/your/file"]];
[self.player playAudioFile:audioFile];

随着音频的播放,EZAudioPlayerDelegate将接收到playedAudio:...updatedPosition:...(如果音频文件达到文件尾,将会有reachedEndOfAudioFile:事件)。典型的EZAudioPlayerDelegate实现可能如下:

- (void)  audioPlayer:(EZAudioPlayer *)audioPlayer
          playedAudio:(float **)buffer
       withBufferSize:(UInt32)bufferSize
 withNumberOfChannels:(UInt32)numberOfChannels
          inAudioFile:(EZAudioFile *)audioFile
{
    __weak typeof (self) weakSelf = self;
    // Update an EZAudioPlot or EZAudioPlotGL to reflect the audio data coming out
    // of the EZAudioPlayer (post volume and pan)
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf.audioPlot updateBuffer:buffer[0]
                          withBufferSize:bufferSize];
    });
}

//------------------------------------------------------------------------------

- (void)audioPlayer:(EZAudioPlayer *)audioPlayer
    updatedPosition:(SInt64)framePosition
        inAudioFile:(EZAudioFile *)audioFile
{
    __weak typeof (self) weakSelf = self;
    // Update any UI controls including sliders and labels
    // display current time/duration
    dispatch_async(dispatch_get_main_queue(), ^{
        if (!weakSelf.positionSlider.highlighted)
        {
            weakSelf.positionSlider.floatValue = (float)framePosition;
            weakSelf.positionLabel.integerValue = framePosition;
        }
    });
}

搜索

您可以使用与EZAudioFile类似的方式来搜索音频文件。也就是说,使用seekToFrame:currentTime属性。

// Get the total number of frames and seek halfway
SInt64 totalFrames = [self.player totalFrames];
[self.player seekToFrame:(totalFrames/2)];

// Alternatively, you can seek using seconds
NSTimeInterval duration = [self.player duration];
[self.player setCurrentTime:duration/2.0];

设置播放参数

由于EZAudioPlayer封装了EZOutput,因此您可以调整播放的音量和平衡参数。

// Make it half as loud, 0 = silence, 1 = full volume. Default is 1.
[self.player setVolume:0.5];

// Make it only play on the left, -1 = left, 1 = right. Default is 0.0 (center)
[self.player setPan:-1.0];

获取音频文件参数

EZAudioPlayerEZAudioFile 进行封装,并提供了获取当前时间、持续时间、帧索引、总帧数等值的高级接口。

NSTimeInterval  currentTime          = [self.player currentTime];
NSTimeInterval  duration             = [self.player duration];
NSString       *formattedCurrentTime = [self.player formattedCurrentTime]; // MM:SS formatted
NSString       *formattedDuration    = [self.player formattedDuration];    // MM:SS formatted
SInt64          frameIndex           = [self.player frameIndex];
SInt64          totalFrames          = [self.player totalFrames];

此外,EZOutput 属性也以高级形式提供。

EZAudioDevice *outputDevice = [self.player device];
BOOL           isPlaying    = [self.player isPlaying];
float          pan          = [self.player pan];
float          volume       = [self.player volume];

通知

EZAudioPlayer 提供以下通知(截至 1.1.2 版本)。

/**
 Notification that occurs whenever the EZAudioPlayer changes its `audioFile` property. Check the new value using the EZAudioPlayer's `audioFile` property.
 */
FOUNDATION_EXPORT NSString * const EZAudioPlayerDidChangeAudioFileNotification;

/**
 Notification that occurs whenever the EZAudioPlayer changes its `device` property. Check the new value using the EZAudioPlayer's `device` property.
 */
FOUNDATION_EXPORT NSString * const EZAudioPlayerDidChangeOutputDeviceNotification;

/**
 Notification that occurs whenever the EZAudioPlayer changes its `output` component's `pan` property. Check the new value using the EZAudioPlayer's `pan` property.
 */
FOUNDATION_EXPORT NSString * const EZAudioPlayerDidChangePanNotification;

/**
 Notification that occurs whenever the EZAudioPlayer changes its `output` component's play state. Check the new value using the EZAudioPlayer's `isPlaying` property.
 */
FOUNDATION_EXPORT NSString * const EZAudioPlayerDidChangePlayStateNotification;

/**
 Notification that occurs whenever the EZAudioPlayer changes its `output` component's `volume` property. Check the new value using the EZAudioPlayer's `volume` property.
 */
FOUNDATION_EXPORT NSString * const EZAudioPlayerDidChangeVolumeNotification;

/**
 Notification that occurs whenever the EZAudioPlayer has reached the end of a file and its `shouldLoop` property has been set to NO.
 */
FOUNDATION_EXPORT NSString * const EZAudioPlayerDidReachEndOfFileNotification;

/**
 Notification that occurs whenever the EZAudioPlayer performs a seek via the `seekToFrame` method or `setCurrentTime:` property setter. Check the new `currentTime` or `frameIndex` value using the EZAudioPlayer's `currentTime` or `frameIndex` property, respectively.
 */
FOUNDATION_EXPORT NSString * const EZAudioPlayerDidSeekNotification;

EZRecorder

提供了一种将任何音频源录制到音频文件中的方法。这个功能与其他组件配合得很好,可以在录制过程中绘制音频波形,以提供视觉反馈,了解正在发生什么。通过 EZRecorderDelegate 提供的方法可以监听写事件和 EZRecorder 的最终关闭事件(详情见下文)。

相关示例项目

  • EZAudioRecordExample (iOS)
  • EZAudioRecordExample (OSX)

创建录音器

要创建 EZRecorder,您必须提供以下至少三个事项:表示音频文件写入路径的 NSURL(现有文件将被覆盖)、表示您将提供音频数据的格式的 clientFormat 和表示磁盘上音频数据文件格式的 EZRecorderFileTypeAudioStreamBasicDescription

// Provide a file path url to write to, a client format (always linear PCM, this is the format
// coming from another component like the EZMicrophone's audioStreamBasicDescription property),
// and a EZRecorderFileType constant representing either a wav (EZRecorderFileTypeWAV),
// aiff (EZRecorderFileTypeAIFF), or m4a (EZRecorderFileTypeM4A) file format. The advantage of
// this is that the `fileFormat` property will be automatically filled out for you.
+ (instancetype)recorderWithURL:(NSURL *)url
                   clientFormat:(AudioStreamBasicDescription)clientFormat
                       fileType:(EZRecorderFileType)fileType;

// Alternatively, you can provide a file path url to write to, a client format (always linear
// PCM, this is the format coming from another component like the EZMicrophone's
// audioStreamBasicDescription property), a `fileFormat` representing your custom
// AudioStreamBasicDescription, and an AudioFileTypeID that corresponds with your `fileFormat`.
+ (instancetype)recorderWithURL:(NSURL *)url
                   clientFormat:(AudioStreamBasicDescription)clientFormat
                     fileFormat:(AudioStreamBasicDescription)fileFormat
                audioFileTypeID:(AudioFileTypeID)audioFileTypeID;

首先,声明一个 EZRecorder 实例(您将为每个写入的音频文件有一个这样的实例)。

// Declare the EZRecorder as a strong property
@property (nonatomic, strong) EZRecorder *recorder;

然后使用上述两种初始化方法之一进行初始化。例如,使用 EZRecorderFileType 快捷初始化器可以如此创建实例

// Example using an EZMicrophone and a string called kAudioFilePath representing a file
// path location on your computer to write out a M4A file.
self.recorder = [EZRecorder recorderWithURL:[NSURL fileURLWithPath:@"/path/to/your/file.m4a"]
                               clientFormat:[self.microphone audioStreamBasicDescription]
                                   fileType:EZRecorderFileTypeM4A];

或者要配置自己的自定义文件格式,例如写入 8000 Hz、iLBC 文件

// Example using an EZMicrophone, a string called kAudioFilePath representing a file
// path location on your computer, and an iLBC file format.
AudioStreamBasicDescription iLBCFormat = [EZAudioUtilities iLBCFormatWithSampleRate:8000];
self.recorder = [EZRecorder recorderWithURL:[NSURL fileURLWithPath:@"/path/to/your/file.caf"]
                               clientFormat:[self.microphone audioStreamBasicDescription]
                                 fileFormat:iLBCFormat
                            audioFileTypeID:kAudioFileCAFType];

录制一些音频

初始化 EZRecorder 后,可以通过传递 AudioBufferList 及其缓冲区大小来追加数据。

// Append the microphone data coming as a AudioBufferList with the specified buffer size
// to the recorder
-(void)    microphone:(EZMicrophone *)microphone
        hasBufferList:(AudioBufferList *)bufferList
       withBufferSize:(UInt32)bufferSize
 withNumberOfChannels:(UInt32)numberOfChannels
{
    // Getting audio data as a buffer list that can be directly fed into the EZRecorder. This is
    // happening on the audio thread - any UI updating needs a GCD main queue block.
    if (self.isRecording)
    {
        // Since we set the recorder's client format to be that of the EZMicrophone instance,
        // the audio data coming in represented by the AudioBufferList can directly be provided
        // to the EZRecorder. The EZRecorder will internally convert the audio data from the
        // `clientFormat` to `fileFormat`.
        [self.recorder appendDataFromBufferList:bufferList
                                 withBufferSize:bufferSize];
    }
}
响应写音频数据后的 EZRecorder

成功使用 EZRecorder 写入音频数据后,将通知 EZRecorderDelegate 事件,使其可以通过

// Triggers after the EZRecorder's `appendDataFromBufferList:withBufferSize:` method is called
// so you can update your interface accordingly.
- (void)recorderUpdatedCurrentTime:(EZRecorder *)recorder
{
    __weak typeof (self) weakSelf = self;
    // This will get triggerd on the thread that the write occured on so be sure to wrap your UI
    // updates in a GCD main queue block! However, I highly recommend you first pull the values
    // you'd like to update the interface with before entering the GCD block to avoid trying to
    // fetch a value after the audio file has been closed.
    NSString *formattedCurrentTime = [recorder formattedCurrentTime]; // MM:SS formatted
    dispatch_async(dispatch_get_main_queue(), ^{
        // Update label
        weakSelf.currentTimeLabel.stringValue = formattedCurrentTime;
    });
}

关闭音频文件

录制完成后,务必调用 closeAudioFile 方法,以确保在再次读取音频文件之前,写入磁盘上的音频文件已经正确关闭。

// Close the EZRecorder's audio file BEFORE reading
[self.recorder closeAudioFile];

这将触发 EZRecorder 的代理方法

- (void)recorderDidClose:(EZRecorder *)recorder
{
    recorder.delegate = nil;
}

界面组件

EZAudio 当前提供两个嵌入式音频波形组件,有助于简化音频可视化的过程。

EZAudioPlot

提供了一个使用 CoreGraphics 进行绘制的音频波形图。在 iOS 中,它是 UIView 的子类,而在 OSX 中,它是 NSView 的子类。截至 1.0.0 版本,波形图使用 CALayers 绘制,排版在 GPU 上完成。因此,性能有了显著的提升,实时(即每秒 60 帧)绘图时的 CPU 使用率现在约为 2-3%,而此前为 20-30%。

相关示例项目

  • EZAudioCoreGraphicsWaveformExample (iOS)
  • EZAudioCoreGraphicsWaveformExample (OSX)
  • EZAudioRecordExample (iOS)
  • EZAudioRecordExample (OSX)
  • EZAudioWaveformFromFileExample (iOS)
  • EZAudioWaveformFromFileExample (OSX)
  • EZAudioFFTExample (iOS)
  • EZAudioFFTExample (OSX)

创建音频图

您可以在界面构建器中通过将UIView(iOS)或NSView(OSX)拖动到内容区域中来创建一个音频图表。然后更改UIView/NSView的自定义类为EZAudioPlot

EZAudioPlotInterfaceBuilder

或者,您也可以通过编程的方式创建音频图表

// Programmatically create an audio plot
EZAudioPlot *audioPlot = [[EZAudioPlot alloc] initWithFrame:self.view.frame];
[self.view addSubview:audioPlot];

自定义音频图表

所有图表都提供更改背景颜色、波形颜色、图表类型(缓冲或滚动)、切换填充和虚线、以及切换镜像和非镜像(关于x轴)的能力。iOS上的颜色类型为UIColor,而OSX上的颜色类型为NSColor。

// Background color (use UIColor for iOS)
audioPlot.backgroundColor = [NSColor colorWithCalibratedRed:0.816
                                                      green:0.349
                                                       blue:0.255
                                                      alpha:1];
// Waveform color (use UIColor for iOS)
audioPlot.color = [NSColor colorWithCalibratedRed:1.000
                                            green:1.000
                                             blue:1.000
                                            alpha:1];
// Plot type
audioPlot.plotType = EZPlotTypeBuffer;
// Fill
audioPlot.shouldFill = YES;
// Mirror
audioPlot.shouldMirror = YES;

IBInspectable属性

从iOS 8开始,您可以通过IBInspectable属性直接在界面构建器中调整背景颜色、颜色、增益、shouldFill和shouldMirror参数

EZAudioPlotInspectableAttributes

更新音频图表

所有图表只有一个更新函数,即updateBuffer:withBufferSize:,该函数需要一个float数组及其长度。

// The microphone component provides audio data to its delegate as an array of float buffer arrays.
- (void)   microphone:(EZMicrophone *)microphone
     hasAudioReceived:(float **)buffer
       withBufferSize:(UInt32)bufferSize
 withNumberOfChannels:(UInt32)numberOfChannels
{
    /**
     Update the audio plot using the float array provided by the microphone:
       buffer[0] = left channel
       buffer[1] = right channel
     Note: Audio updates happen asynchronously so we need to make sure
         sure to update the plot on the main thread
     */
    __weak typeof (self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf.audioPlot updateBuffer:buffer[0] withBufferSize:bufferSize];
    });
}

EZAudioPlotGL

提供使用OpenGL进行绘图的音频波形图表。此类的API与上面提到的EZAudioPlot的完全相同。在iOS上,这是一个GLKView的子类,而在OSX上,这是一个NSOpenGLView的子类。在大多数情况下,这是您想要的图表,它是GPU加速的,可以处理大量点,而在每秒显示60帧时表现惊人(EZAudioPlot在1024以上时开始出现问题),并且可以在所有设备上表现出色。唯一的缺点是只能在屏幕上同时有一个OpenGL图表。然而,您可以在视图层次结构中将OpenGL图表与Core Graphics图表结合起来(请参阅EZAudioRecordExample以获得如何做到这一点的示例)。

相关示例项目

  • EZAudioOpenGLWaveformExample (iOS)
  • EZAudioOpenGLWaveformExample (OSX)
  • EZAudioPlayFileExample (iOS)
  • EZAudioPlayFileExample (OSX)
  • EZAudioRecordExample (iOS)
  • EZAudioRecordExample (OSX)
  • EZAudioPassThroughExample (iOS)
  • EZAudioPassThroughExample (OSX)

创建OpenGL音频图表

您可以在界面构建器中将UIView(iOS)或NSView(OSX)拖动到内容区域中来创建音频图表。然后,将UIView/NSView的自定义类更改为EZAudioPlotGL

EZAudioPlotGLInterfaceBuilder

或者,您可以通过编程的方式创建EZAudioPlotGL

// Programmatically create an audio plot
EZAudioPlotGL *audioPlotGL = [[EZAudioPlotGL alloc] initWithFrame:self.view.frame];
[self.view addSubview:audioPlotGL];

自定义OpenGL音频图表

所有图表都提供更改背景颜色、波形颜色、图表类型(缓冲或滚动)、切换填充和虚线、以及切换镜像和非镜像(关于x轴)的能力。iOS上的颜色类型为UIColor,而OSX上的颜色类型为NSColor。

// Background color (use UIColor for iOS)
audioPlotGL.backgroundColor = [NSColor colorWithCalibratedRed:0.816
                                                        green:0.349
                                                         blue:0.255
                                                        alpha:1];
// Waveform color (use UIColor for iOS)
audioPlotGL.color = [NSColor colorWithCalibratedRed:1.000
                                              green:1.000
                                               blue:1.000
                                              alpha:1];
// Plot type
audioPlotGL.plotType = EZPlotTypeBuffer;
// Fill
audioPlotGL.shouldFill = YES;
// Mirror
audioPlotGL.shouldMirror = YES;

IBInspectable属性

从iOS 8开始,您可以通过IBInspectable属性直接在界面构建器中调整背景颜色、颜色、增益、shouldFill和shouldMirror参数

EZAudioPlotGLInspectableAttributes

更新OpenGL音频图表

所有图表只有一个更新函数,即updateBuffer:withBufferSize:,该函数需要一个float数组及其长度。

// The microphone component provides audio data to its delegate as an array of float buffer arrays.
- (void)   microphone:(EZMicrophone *)microphone
     hasAudioReceived:(float **)buffer
       withBufferSize:(UInt32)bufferSize
 withNumberOfChannels:(UInt32)numberOfChannels
{
    /**
     Update the audio plot using the float array provided by the microphone:
       buffer[0] = left channel
       buffer[1] = right channel
     Note: Audio updates happen asynchronously so we need to make sure
         sure to update the plot on the main thread
     */
    __weak typeof (self) weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf.audioPlotGL updateBuffer:buffer[0] withBufferSize:bufferSize];
    });
}

许可证

EZAudio在MIT许可证下提供。有关更多信息,请参阅LICENSE文件。

联系方式和贡献者

Syed Haris Ali
www.syedharisali.com
syedhali07[at]gmail.com

致谢

以下人员很棒