AudioUnit 基础知识

Overwrite

个人理解的名词解释:

  • Audio Unit:音频单元,通常指一个音频单元实例,或者 Audio Unit 技术。
  • Audio Component:音频组件,指音频单元(Audio Unit)类的类型。
  • Audio Processing Graph:音频处理图,即音频图。
  • Audio Node:音频结点,在音频处理图中担任音频单元的标识,在音频处理图执行打开操作后间接实例化对应的音频单元
  • Element:音频单元中的具有特定功能的元件
    • Input element:特指连接音频输入硬件(如麦克风)的元件
    • Output element:特指连接音频输出硬件(如扬声器)的元件
  • Bus:与 element 的概念相同,不过在强调信号流的时候使用
  • Scope:音频单元中音频流的端口或范围
    • Input scope:音频流的输入端口
    • Output scope : 音频流的输出端口
    • Global scope:音频单元的全局范围
    • 音频流流向:Input scope -> Output scope
Audio_frameworks_2x.png

如上图所示,AudioUnit是iOS中音频最底层的API,仅在高性能,专业处理声音的需求下使用.

1. Audio Unit 提供快速、模块化的音频处理

使用场景

  • 以最低延迟的方式同步音频的输入输出,如VoIP应用

  • 手动合成音视频,如音乐游戏、乐器音乐合成软件

  • 使用特定的 Audio Unit 功能,如回声消除、混音、音调均衡

  • 将多种音频处理模块灵活组装应用

1.1 Audio Unit 的用途

用途 Audio Unit
音效(Effect) iPod Equalizer
音频混合(Mixing) 3D Mixer、Multichannel Mixer
音频输入输出(I/O) Remote I/O、Voice-Processing I/O、Generic Output
格式转换(Format conversion) Format Converter
  • Effect Unit

    • iPod Equalizer:提供一组预设的均衡曲线,如低音增强(Bass Booster)、流行(Pop)和口语(Spoken Word)等等
  • Mixer Units

    • 3D Mixer:OpenAL底层构建的基础,如果需要3D Mixer unit特性,建议直接使用OpenAL,因为它提供了很多封装好的功能强大的API。

    • Multichannel Mixer:为多个音频提供混音功能,而且支持不同声道声音混合,最后以立体声输出。你可以单独打开或关闭其中一个输入音频的声音、调节音量、播放速度等等。总的来说,这就是一个多音频输入,单音频输出的 Audio Unit。

  • I/O Units

    • (常用)Remote I/O:它直接连接输入和输出的音频硬件,以低延迟的方式访问单个接收或发出的音频采样数据。并且提供了硬件音频格式到应用设置音频格式的格式转换功能(Format Converter)。

    • Voice-Processing I/O:通过声学的回声消除拓展了Remote I/O unit,常用于VoIP或语音通信的应用。它还提供了自动增益校正、语音处理质量调整和静音功能。

    • Generic Output:它不连接任何音频硬件而是提供一个将处理链的输出传递给应用程序的途径,通常用于离线音频处理。

  • Format Converter Unit

    • Format Converter:在 I/O Units 中间接使用于的音频格式转换部分。

    • Converter unit:支持转换线性PCM音频数据类型(linear PCM)

1.2 Audio Unit 的两套 API

iOS 中操控 Audio Unit 的有两套API,一套是用于直接操控单个 Audio Unit,另一套通过音频处理图(audio processing graphs)的方式操控多个Audio Unit。

  • Audio Unit API

  • Audio Graph API

    两套API有部分相同的功能的函数,开发者在使用的时候可以混合使用:

    • 获取 Audio Units 的动态可链接库的引用

    • 实例化 Audio Units

    • 连接 Audio Units 并注册回调函数

    • 启动和停止音频流

    1.3 获取 Audio Units 实例

    1 - 1 创建音频组件描述来标识一个 Audio Unit

AudioComponentDescription ioUnitDescription;
 
ioUnitDescription.componentType          = kAudioUnitType_Output;
ioUnitDescription.componentSubType       = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufacturer  = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags         = 0;
ioUnitDescription.componentFlagsMask     = 0;

AudioComponentDescription:一个用来描述并唯一标识一个音频组件的结构体

标识字段(Identifier keys):

  • componentType:类型,audio unit 的主要功能
  • componentSubType:子类型,audio unit 的详细功能
  • componentManufacturer:制造商,一般都是kAudioUnitManufacturer_Apple
  • componentFlags & componentFlagsMask:提供位移枚举的方式标识特定描述,一般都设0忽略

更多 Identifier keys 见 Identifier Keys for Audio Units

1 - 2 获取 Audio Unit 对象实例

Audio Unit API

AudioComponent foundIoUnitReference = AudioComponentFindNext (
                                          NULL,
                                          &ioUnitDescription
                                      );
AudioUnit ioUnitInstance;
AudioComponentInstanceNew (
    foundIoUnitReference,
    &ioUnitInstance
);

AudioComponent类型:音频组件类型,本质是一个指定特定音频组件的类指针。一个单独的音频组件,可用于实例化多个相同类型的音频单元实例。

AudioComponentFindNext函数:该函数用于在系统设备可用组件中查找最相近的组件,并将其返回其类指针。可用组件的顺序可能会根据系统或者Audio Unit 的版本进行变化,详情见kAudioComponentRegistrationsChangedNotification

  • 第一个参数(inComponent):设置为NULL表示使用系统定义的顺序查找第一个匹配的音频组件。如果你将上一个使用的音频组件传给该参数,则该函数将从这个组件开始继续寻找下一个与之描述匹配的音频组件。根据可用列表的顺序进行实例化,有助于性能提升。
  • 第二个参数(inDesc):音频组件描述结构体地址,用做组件查找的依据。
  • 返回值:符合条件的音频组件,即音频单元类指针。

AudioComponentInstanceNew函数:该函数用于创建音频组件实例,即一个音频单元(audio unit)。

  • 第一个参数(inComponent):音频组件,用于告诉系统需要的音频单元类型,该参数不可为空。
  • 第二个参数(outInstance):音频单元实例,用于接收实例化的音频单元。
  • 返回值(OSStatus):结果状态返回值,无错误返回 noErr(即0),否则返回错误码。

Audio Graph API

// 声明并实例化一个音频处理图
AUGraph processingGraph;
NewAUGraph(&processingGraph);
 
// 添加音频单元结点到图中,然后实例化这些结点
AUNode ioNode;
AUGraphAddNode (
    processingGraph,
    &ioUnitDescription,
    &ioNode
);

// 间接执行音频单元的实例化
AUGraphOpen (processingGraph); 

// 获取新实例化的 I/O 单元引用
AudioUnit ioUnit;
AUGraphNodeInfo (
    processingGraph,
    ioNode,
    NULL,
    &ioUnit
);

NewAUGraph函数:实例化一个音频处理图

AUGraphAddNode函数:通过音频组件描述添加一个音频结点到音频处理图中。

AUGraphOpen函数:打开指定的音频处理图(头文件中表示音频单元在此时并没有初始化,该操作并不会有资源被申请?)。

AUGraphNodeInfo函数:返回指定音频结点的信息。

  • 输入参数(inGraph&inNode):音频处理图与输入结点
  • 输出参数(outDescription&outAudioUnit):音频组件描述与音频单元,设置NULL表示不获取该信息。
  • 返回值(OSStatus):函数的执行成功与错误码。

1.4 Audio Units 的 Scopes 与 Elements

一个音频单元由以下的ScopesElements组成:

audioUnitScopes_2x.png

[图片上传中...(IO_unit_2x.png-b104de-1593881466577-0)]

  • scope:音频单元内部的一个编程上下文,本文主要称之为音频流动的“端口”或“范围”。
    • Input scopeOutput scope直接参与音频数据流通过音频单元的过程。让音频数据从Input scope进入,并从Global scope离开。可以这两个scope中可以配置一些属性和参数,例如``kAudioUnitProperty_ElementCountkAudioOutputUnitProperty_EnableIOkMultiChannelMixerParam_Volume`等等。
      • Input scope:音频输入端口,在此处都需要向Element输入音频数据;
      • Output scope:音频输出端口,在此处都需要将Element的音频数据输出到其他地方;
    • Global scope:全局范围并没有与Element嵌套,用于配置音频单元中与输入输出概念无关的属性,例如kAudioUnitProperty_MaximumFramesPerSlice等。
  • element:音频单元内部一个嵌套在Scope中的编程上下文,本文称之为“元件”或“总线”。
    • 嵌套在Input scopeOutput scopeElement类似一个在物理音频设备上的信号总线。因此在文档中'element''bus'都代表上图中的ElementX。一般在强调信号流时使用'bus',在强调音频单元的特定功能方面时使用'element'
    • 对于Input Output scope中的Element都是从0开始索引的;对于Global scope直接用0索引。
    • 上图只是表示一个普通音频单元的内部架构,但是对于不同音频单元会有不同的内部结构,比如一个音频混合单元,它会有多个输入的element,一个输出的element

1.5 Audio Units 的属性配置

一个音频单元的属性是通过一个键值对来设置的,其中键是通过一个唯一的整数(UInt32)来标识的。苹果预留了0 ~ 63999的区间标识属性键,剩下的区间可提供给第三方音频单元使用。我们可以通过AudioUnitSetProperty函数进行属性配置,调用方式由以下代码块所示。

UInt32 busCount = 2;
 
OSStatus result = AudioUnitSetProperty (
    mixerUnit,                         // 属性设置目标音频单元
    kAudioUnitProperty_ElementCount,   // 属性键
    kAudioUnitScope_Input,             // 属性设置所在域
    0,                                 // 属性设置所在元件
    &busCount,                         // 属性值地址
    sizeof(busCount)                  // 属性值的字节大小
);

常用属性

  • kAudioOutputUnitProperty_EnableIO:用于在 I/O Unit 上启用或禁用输入或输出。默认输出已启用,输入已禁用。
  • kAudioUnitProperty_ElementCount:配置mixer unit上的输入elements的数量
  • kAudioUnitProperty_MaximumFramesPerSlice:为了指定音频数据的最大帧数,音频单元应该准备好响应于回调函数调用而产生。对于大多数音频设备,在大多数情况下,您必须按照参考文档中的说明设置此属性。如果不这样做,屏幕锁定时您的音频将停止。
  • kAudioUnitProperty_StreamFormat:指定特定音频单元输入或输出总线的音频流数据格式。

大多数属性只能在音频单元没有初始化时指定,但是某些特定属性可以在音频单元运行时设置,如``Voice-Processing I/O unitkAUVoiceIOProperty_MuteOutput静音功能、iPod EQ unitkAudioUnitProperty_PresentPreset`当前模式功能。

属性相关函数

  • AudioUnitGetPropertyInfo:用于判断指定音频单元中该属性是否可用,可用则提供值的大小。
  • AudioUnitGetPropertyAudioUnitSetProperty:获取、设置属性。
  • AudioUnitAddPropertyListenerAudioUnitRemovePropertyListenerWithUserData:监听、移除监听特定属性。

1.6 Audio Units 的参数配置

相比与音频单元属性,音频单元参数在音频单元工作的过程中都是可以调整的。音频单元参数也是以键值对的方式表示的。

  • 键是以枚举值的形式展示,在同一种类的音频单元中枚举值是唯一的,但在所有音频单元中的有重复。
  • 值统一都是32位的浮点数(Float32)类型的。值的允许范围以及含义由音频单元的实现确定,详情见以下文档(Audio Unit Parameters Reference)。

参数相关函数(其使用方式类似属性获取与设置)

  • AudioUnitGetParameter:获取指定参数值
  • AudioUnitSetParameter:设置指定参数值

我们可以通过UIKit中的UISliderUISwitch控件在音频单元工作的过程中改变其参数值,达到用户交互的效果。

1.7 I/O Units 的基本特性

I/O Units 是一个十分常用的音频单元,它在许多地方都比较特别。它包含了两个element,内部结构如下图所示:

IO_unit_2x.png

I/O Unit 的两个elements是两个独立的实体,在使用的时候我们需要通过设置属性(kAudioOutputUnitProperty_EnableIO)单独启用和禁用element

  • Element 1Input scope部分是对开发者不透明的,它直接连接着音频输入的硬件设备(麦克风),可在其Output scope部分获取音频数据。一般 I/O unit 的Element 1又称为input element
  • Element 0Ouput scope部分是对开发者不透明的,它直接连接着音频输出的硬件设备(扬声器),可在其Input scope部分传入音频数据。一般 I/O unit 的Element 0又称为output element

因此 I/O unit 在音频处理图中担任着音频流处理的起始点和终点,拥有开启和关闭音频流的能力。

2. Audio Processing Grapha 管理 Audio Units

Audio Processing Grapha 即一个音频处理图,是 Core-Foundation风格的数据结构——AUGrapha。通过 AUGrapha 我们可以构建和管理一个音频处理链条。一个音频处理图可以利用多个音频单元和多个呈现回调函数创建任何你所想象的音频处理方案。

  • AUGraph:音频图,本质是一个音频图的指针类型,该类保证了线程安全。例如播放音频时,可以保证安全地插入一个均衡器(equalizer)或者在混合器(mixer)输入端更换回调函数。事实上,AUGraph提供了iOS平台上用于音频应用程序的动态配置API。
    • 更多:在构建一个音频图的时候,必须配置好图中的所有音频单元,AUGraph相关的API并不能完全胜任,因此需要同时使用音频图和音频单元两套API。
  • AUNode:音频结点,本质是一个SInt32类型,在音频图中的标识一个独立的音频单元。在配置使用音频图时,为了可读性,一般使用一个音频结点来代表图中的包含的音频单元,而不是直接使用音频单元。
    • 更多:音频结点除了可以代表一个音频单元外,还能代表音频图里面的子图,但是I/O unit连接的子图必须只能使用 Generic Output unit 而不是 I/O unit。因为一个物理设备最多只能只能连接一个音频单元。

总的来说,构建音频处理图需要以下三步:

  1. 添加音频结点到音频图中
  2. 通过音频结点获取音频单元,直接配置音频单元的属性参数
  3. 将音频结点连接起来

2.1 Audio Processing Graphs 中的 I/O Unit

无论是录音、回放和同步I/O流,所有音频处理图都有一个 I/O unit。音频图中的 I/O unit 负责音频流的输入和输出,其他音频单元负责其他音频流处理工作。

  • 音频图通过AUGraphStartAUGraphStop方法启动和停止音频流
  • 通过AudioOutputUnitStartAudioOutputUnitStop方法传达启动和停止信息给I/O unit

2.2 Audio Processing Graphs 线程安全

音频处理图API提供一个“to-do list”保存需要做的操作,在开发者配置完成后再告诉音频图去实现。以下是音频处理图支持的一些常见重新配置以及相关功能:

  • AUGraphAddNodeAUGraphRemoveNode:添加移除音频结点
  • AUGraphConnectNodeInputAUGraphDisconnectNodeInput:添加移除音频结点之间的连接
  • AUGraphSetNodeInputCallback:设置连接音频单元输入总线(input bus)的回调函数

下面将以一个音频合成播放的音频处理图重新配置为例,讲述音频处理图运行中的线程安全。首先构建一个音频处理图包含 Multichannel Mixer unit 和 Remote I/O unit,用于播放合成两种输入源的混音效果。运行中的音频处理图中,开发者将两个输入源的数据送给 Mixer unit 的 input bus,mixer 的输出端连接着 I/O unit 的output element,最终将音频传给硬件输出。音频处理图如下:

AudioProcessingGraphBeforeEQ_2x.png

现在,在其中一个音频流中插入一个“音频均衡器”,则音频处理图如下:

AudioProcessingGraphWithEQ_2x.png

以下是完成重新配置的步骤:

  1. 通过调用AUGraphDisconnectNodeInput断开mixer unitinput 1的“鼓声”回调。
  2. 通过配置“音频组件描述”,然后调用AUGraphAddNode将一个包含一个包含iPod EQ unit的音频结点到音频图中的音频结点添加到音频图中。(此时,iPod EQ unit已具有实例化对象但未被初始化,新增的音频结点也只是存在音频图中并未参与音频流)
  3. 配置和初始化iPod EQ unit
    • 调用AudioUnitGetProperty函数从Mixer unit的输入端获取当前使用的流格式(kAudioUnitProperty_StreamFormat)
    • 调用AudioUnitSetProperty函数两次,分别设置iPod EQ unit输入端和输出端的流格式
    • 调用AudioUnitInitialize函数为iPod EQ unit分配内存和准备处理音频使用。(注:这个函数是线程不安全的,需要当iPod EQ unit尚未主动参与进音频处理图时,即没有调用AUGraphUpdate函数前使用。
  4. 通过调用AUGraphSetNodeInputCallback将“鼓声”回调函数添加到iPod EQ unitinput端。

上面1,2,4步使用AUGraph开头的函数,都会被添加到的任务执行列表(“to-do list”)中。然后通过调用AUGraphUpdate执行这些未开始任务。如果成功返回,则代表音频图已经被动态重新配置并且iPod EQ unit也已经就位正在处理音频数据。

2.3 通过 Graph "pull"音频流

在音频处理图的音频流流动类似生产者消费者模式,消费者在需要更多音频数据时通知生产者。请求音频数据流的方向与音频流提供的方向正好相反。

pull_model_2x.png

对一组音频数据的每个请求称为渲染调用(render call),也称为拉流(pull)。该图灰色“控制流”箭头表示为拉流操作。拉流请求的数据本质是一组音频样本帧(audio sample frames),一组音频样本帧也称为一个切片(slice)。提供切片的代码称为渲染回调函数( render callback function)。下列是拉取音频流的步骤:

  1. 调用AUGraphStart函数后,虚拟输出设备调用Remote I/O unitoutput element的渲染回调函数请求一片处理过的音频数据帧。
  2. Remote I/O unit的回调函数在其输入缓冲区中查找要处理的音频数据去满足渲染请求。如果有数据则直接传递,否则,调用连接其输入端的回调函数。上图中Remote I/O unit的输入端连接一个effect unit的输出端,则I/O uniteffect unit中拉流,请求一片音频数据帧。
  3. effect unit的行为与Remote I/O unit一样。当它需要音频数据时,便从输入连接中获取它。上图中,effect unit从应用程序的回调函数中获取音频数据。
  4. 应用程序的回调函数最终接收了这个拉流请求,在函数中提供effect unit需要的音频帧数据。
  5. effect unit从应用程序的回调函数中获取的音频数据,然后按照步骤2中要求提供音频数据给Remote I/O unit
  6. Remote I/O unit将从effect unit提供的音频数据,按照步骤1中的要求提供给虚拟输出设备,完成了一个拉流周期。

3. 通过回调函数将音频传递给 Audio Units

为了将音频数据从内存或磁盘中传递到音频单元的input bus,需要使用实现一个渲染回调函数填充数据并通过AURenderCallback属性配置音频单元的属性。这样当音频单元需要一片音频帧数据时候,该回调函数就会被调用。渲染回调函数给了我们操作音频数据很高的自由度,我们可以在回调函数以任何方式创建或改变音频数据。与此同时,渲染回调函数处于实时优先级线程上,后续的函数调用都是异步的。因此,我们在渲染回调函数中处理时间是有限的,如果在下一次渲染调用到达时,上一个回调函数还没运行完成,那么你的音频结果将会出现缺口,导致你的结果是不连续的。因此,不能在渲染回调函数中执行线程锁、分配内存、访问文件系统或网络连接等耗时任务。

以下是渲染回调函数的详细说明:

static OSStatus MyAURenderCallback (
    void                        *inRefCon,
    AudioUnitRenderActionFlags  *ioActionFlags,
    const AudioTimeStamp        *inTimeStamp,
    UInt32                      inBusNumber,
    UInt32                      inNumberFrames,
    AudioBufferList             *ioData
) { /* callback body */ }
  • inRefCon:表示注册回调函数时传递的指针,一般可传当前对象实例。因为回调函数是C语言形式,无法直接访问本类中属性与方法,所以将实例化对象传入可以间接调用当前对象中属性与方法。
  • ioActionFlags:表示音频渲染处理的行为,本质是一个位移枚举,告诉 Audio Unit 使用不同的音频渲染方式。比如在一个乐器音乐合成应用中,用户当前没有需要播放的音符,则在回调函数中使用*ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
  • inTimeStamp:表示调用回调函数的时间,可用作音频同步的时间戳。它是一个AudioTimeStamp结构体,每次mSampleTime字段的值都会根据inNumberFrames参数中的数字递增。例如,如果你的应用是音序器或鼓机,则可以使用mSampleTime的值来调度声音。
  • inBusNumber:表示调用回调函数的audio unit bus,可通过该值在回调函数中进行分支操作。另外,当音频单元注册回调函数时,可以为每个bus指定不同的inRefCon
  • inNumberFrames:表示回调函数中的需要填充的音频帧数。
  • ioData:表示需要填入的音频数据缓冲,该音频数据缓冲的结构必须与当前所在bus的的音频流格式一致。如果需要在渲染回调中实现静音功能,则需要通过memset函数将ioDatabuffers都设为0。

下图描述的是ioData参数中的一对非交错(noninterleaved)立体声缓冲区:

ioDataBuffers_2x.png

4. 音频流格式启用数据流

在一个音频样本数据中,二进制位的布局是有含义的,这不是单纯用Float32UInt16数据类型可以表达的。音频单元(audio unit)可以使用音频组件描述(AudioComponentDescription)来表达,音频流格式则使用音频流基本描述(AudioStreamBasicDescription,即ASBD)来表达。

// ASBD 结构体
struct AudioStreamBasicDescription {
    Float64 mSampleRate;
    UInt32  mFormatID;
    UInt32  mFormatFlags;
    UInt32  mBytesPerPacket;
    UInt32  mFramesPerPacket;
    UInt32  mBytesPerFrame;
    UInt32  mChannelsPerFrame;
    UInt32  mBitsPerChannel;
    UInt32  mReserved;
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

// 定义一个立体声 ASBD
// AudioUnitSampleType => 8.24 fixed-point integer => SInt32
size_t bytesPerSample = sizeof(AudioUnitSampleType); 
AudioStreamBasicDescription stereoStreamFormat = {0};
stereoStreamFormat.mSampleRate        = graphSampleRate;
stereoStreamFormat.mFormatID          = kAudioFormatLinearPCM; // 未压缩的音频数据
/*
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsFloat |
                                kAudioFormatFlagsNativeEndian |
                                     kAudioFormatFlagIsPacked |
                             kAudioFormatFlagIsNonInterleaved
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsSignedInteger |
                                                                                kAudioFormatFlagsNativeEndian | 
                                                                                     kAudioFormatFlagIsPacked |             
                                                                         kAudioFormatFlagIsNonInterleaved | (kAudioUnitSampleFractionBits << kLinearPCMFormatFlagsSampleFractionShift)
*/
stereoStreamFormat.mFormatFlags       = kAudioFormatFlagsAudioUnitCanonical;
stereoStreamFormat.mBytesPerPacket    = bytesPerSample;
stereoStreamFormat.mFramesPerPacket   = 1;
stereoStreamFormat.mBytesPerFrame     = bytesPerSample;
stereoStreamFormat.mChannelsPerFrame  = 2; // 2 indicates stereo
stereoStreamFormat.mBitsPerChannel    = 8 * bytesPerSample;
stereoStreamFormat.mReserved          = 0;
  • mSampleRate:采样率,每秒钟音频流中的样本帧数
  • mFormatID:格式标识,大体的数据格式类型
  • mFormatFlags:格式配置,位移枚举,指定具体的数据格式
  • mBytesPerPacket:每个音频包中的字节数
  • mFramesPerPacket:每个音频包的帧数
    • 未压缩音频:一个音频包只有一帧的数据
    • 压缩音频:一个音频包是一块压缩好的数据,比如一个ACC音频包有1024个样本帧
  • mBytesPerFrame:每一帧的字节数
    • 非交错型数据(non-interleaved):每一帧只是包含一个声道数据
    • 交错型数据(interleaved):每一帧只是包含多个声道数据
  • mChannelsPerFrame:声道数,每一帧的声道数
  • mBitsPerChannel:位深,每一个声道的二进制数
  • mReserved:保留的?,表示填塞数据结构强制8字节对齐

注:音频流格式在创建的时候需要将事先初始化为0,即不包含任何数据,否则可能会出bug。

音频处理图中必须在关键点设置好音频数据格式,其他点系统将会设置自动格式。iOS 设备上的音频输入和输出硬件具有系统确定的音频流格式,该格式始终是未压缩的,采用交错的线性 PCM 格式。

IOWithoutRenderCallback_2x.png
  • 绿色方块:系统根据硬件事先设置好了音频流格式
  • 蓝色方块:开发者设置音频流格式的关键点
  • 红色方块:开发者设置采样率的位置
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345