AVFoundation开发秘籍笔记:第10章 混合音频

10.1 混合音频

上一章创建的组合媒体看起来还不错,各场景间的切换也很清晰,不过在处理相关音频输出方面确实还有一些问题。首先就是音乐轨道刚开始播放时音量就很大,并且在组合资源结束时又突然停止,用户会觉得声音很震耳。如果开始的时候声音是渐渐增大,结束的时候声音渐渐减小,会带来更好的体验。另一个问题,也可以说更严重的问题是画外音轨道的处理。音乐轨道的音量已经完全覆盖了画外音的声音,几乎听不到画外音。与其说让这两个音频轨道发生冲突,倒不如使用一种 名为闪避处理(ducking)的技术在画外音持续期间将音乐声调低,并在画外音持续时间保持这种状态,在画外音完成后再恢复到之前的音量。框架提供了一个类AVAudioMix来解决上面两个问题。

AVAudioMix用来在组合的音频轨道中进行自定义音频的处理。图10-1展示 了AVAudioMix类和其相关类的示意图,还给出了框架中用到它的类。

AVAudioMix所具有的音频处理方法是由它的输入参数集定义的,它的参数是AVAudioMixInputParameters类型的对象。AVAudioMixInputParameters的实 例关联组合中的单独音频轨道,并在添加到音频混合时定义基于轨道的处理方法。AVAudioMix 和其相关联的AVAudioMixInputParameters集合都是不可变对象,意味着它们适用于为AVPlayertem和AVAssetExportSession之类的客户端提供相关数据,不过它们不能操作其状态。当我们需要创建一个自定义音频混合时,需要改用它们在AVMutableAudioMix和AVMutableAudioMixInput-Parameters中的可变子类。


自动调节音量

在应用音频混合的问题上,最核心的处理就是调整组合音频轨道的音量。当一个组合资源播放或导出时,默认行为是以最大音量或正常音量播放音频轨道。只有一个单音频轨道时这样的方法才可能比较容易接收,不过当一个组合资源包含多个音频源时就会出现问题。对于多音频轨道的情况,每个声音都在争夺空间,这就不可避免会导致一些声 音可能无法被听到。一些简单的音频混合可以解决这个问题并帮助我们得到一个清晰悦耳的混合音效。

如果你曾用过音频混合的工具,比如Pro Tools或Logic Pro X,应该比较习惯使用分贝来描述音量。AV Foundation使用一种更简 单的模型,把音量定义为一个标准化的浮点型数值,数值范围从0.0~ 1.0,即0.0等于静音和1.0等于最大音量。音频轨道的默认音量为1.0,不过可以使用AVMutableAudioMixInputParameters实例修改这个值。这个对象允许在一个指定时间点或给定的时间范围自动调节音量,如图10-2所示。


AVMutableAudioMixInputParameters提供了两个方法来实现图10-2中所示的音量调节。

●setVolume:atTime; 在指定时间点立即调节音量。音量在音频轨道持续时间内会保持不变,直到有另一个音量调节出现。

●setVolumeRampFromStartV olume:toEndVolume:timeRange:允许在一个给定时间范围内平滑地将音量从一个值调节到另一个值。 当需要在一个时间范围内调整音量 时,音量会立即变为指定值的初始音量并在持续时间之,内逐渐调整为指定的结束值。音量会保持一个 toEndVolume:参数所指定的常量,直到接收另一个音量调整为止,这个参数值用于表示音频轨道的持续时间。

下面通过一个示例了解如何使用这些方法创建自动调整音量的功能,如图10-3所示。


image.png
AVCompositionTrack *track = // audio track in composition

// Define automation times
CMTime twoSeconds = CMTimeMake(2, 1);
CMTime fourSeconds = CMTimeMake(4, 1);
CMTime sevenSeconds = CMTimeMake(7,1);

// Create a new parameters object for the given audio track
AVMutableAudioMixInputParameters *parameters =
[AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:track];

// Set initial volume
[parameters setVolume:0.5f atTime: kCMTimeZero] ;

// Define time range of the volume ramp
CMTimeRange range = CMTimeRangeFromTimeToTime(twoSeconds, fourSeconds);

// Perform 2 second ramp from 0.5 -> 0.8
[parameters setVolumeRampFromStartVolume:0.5f toEndVolume:0.8f timeRange:range];

// Drop volume to 0.3 at the 7-second mark
[parameters setVolume:0.3f atTime:sevenSeconds] ;

// Create a new audio mix instance
AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];

// Assign the input parameters to the audio mix
audioMix.inputParameters = @[parameters];

示例首先创建一个新的与要操作的轨道关联的AVMutableAudioMixInputParameters实例。轨道的默认音量是1.0,按照图10-3所示我们设置开始时的音量为0.5,使用setVolume:atTime:方法并传递kCMTimeZero值来设置初始音量。在轨道中选定2秒,在这2秒期间使用setVolumeRampFromStartVolume:toEndVolume:timeRange:方法应用一个从0.5 ~0.8的音量渐变。最后,在7秒处使用setVolume:atTime:方法将音量下调到0.3。定义好所有参数后就可以创建AVMutableAudioMix了,将参数添加到数组中,并将数组赋给音频混合对象的inputParameters属性。示例中创建了一个全格式的音频混合,可以被设置为AVPlayerltem或AVAssetExportSession的audioMix属性进行播放或导出。

注意:
我们讨论的是与在AVComposition中混合音频相关的AVAudioMix和AVAudioMixInputParameters.虽然这看起来是一个常见用例,不过对于组合并不单独使用。AVAudioMixInputParameters的实例和AVAssetTrack(AVCompositionTrack的超类)相关,这就意味着我们在一个常规的AVAsset.上也可以定义音量调整,这就可以用于一些特定的播放和导出场景。

10.2 15 Seconds应用程序中的音频混合

现在我们已经学会了如何使用AVAudioMix,下面在15 Second应用程序来使用它。首先创建一个新的类THAudioMixComposition,它需要遵循THComposition协议,如代码清单10-1所示。

代码清单10-1 THAudioMixComposition 接口

#import "THComposition.h"

@interface THAudioMixComposition : NSObject <THComposition>

@property (strong, nonatomic, readonly) AVAudioMix *audioMix;
@property (strong, nonatomic, readonly) AVComposition *composition;

+ (instancetype)compositionWithComposition:(AVComposition *)composition
                                  audioMix:(AVAudioMix *)audioMix;

- (instancetype)initWithComposition:(AVComposition *)composition
                           audioMix:(AVAudioMix *)audioMix;

@end

这个接口看起来很熟悉,因为它和之前章节中创建的THBasicComposition对象类似。最主要的区别在于添加了AVAudioMix属性和一个带有AVComposition和AVAudioMix参数的初始化方法。下面继续分析代码清单10 2给出的具体实现。

代码清单10-2 THAudioMixComposition 实现

#import "THAudioMixComposition.h"

@interface THAudioMixComposition ()
@property (strong, nonatomic) AVAudioMix *audioMix;
@property (strong, nonatomic) AVComposition *composition;
@end

@implementation THAudioMixComposition

+ (instancetype)compositionWithComposition:(AVComposition *)composition
                                  audioMix:(AVAudioMix *)audioMix {
    return [[self alloc] initWithComposition:composition audioMix:audioMix];
}

- (instancetype)initWithComposition:(AVComposition *)composition
                           audioMix:(AVAudioMix *)audioMix {
    self = [super init];
    if (self) {
        _composition = composition;
        _audioMix = audioMix;
    }
    return self;
}

- (AVPlayerItem *)makePlayable {                                            // 1
    AVPlayerItem *playerItem =
        [AVPlayerItem playerItemWithAsset:[self.composition copy]];
    playerItem.audioMix = self.audioMix;
    return playerItem;
}

- (AVAssetExportSession *)makeExportable {                                  // 2
    NSString *preset = AVAssetExportPresetHighestQuality;
    AVAssetExportSession *session =
        [AVAssetExportSession exportSessionWithAsset:[self.composition copy]
                                          presetName:preset];
    session.audioMix = self.audioMix;
    return session;
}

@end

(1)在makePlayable方法中创建一个 带有AVComposition实例的AVPlayerItem。设置audioMix对象作为播放器条目的audioMix属性。这样就可以在应用程序视频播放器中播放音频时应用音频处理。
(2)在makeExportable方法中创建一个AVAssetExportSession与上一章节中的做法一样。设置audioMix对象作为输出会话的audioMix属性,这样就可以使得在导出这个组合时应用音频处理。

THAudioMixComposition就创建完成了,下面继续讨论它的关联对象THCompositionBuilder的实现。首先创建一个新的对 象THAudioMixCompositionBuilder,遵循THCompositionBuilder协议,如代码清单10-3所示。

代码清单10-3 THAudioMixCompositionBuilder 接口

#import "THCompositionBuilder.h"
#import "THTimeline.h"

@interface THAudioMixCompositionBuilder : NSObject <THCompositionBuilder>

- (id)initWithTimeline:(THTimeline *)timeline;

@end

THAudioMixCompositionBuilder具有一个简单的接口可以接受THTimeline实例。回顾y一下,THTimeline对象包含了应用程序时间区域的状态并提供了创建组合对象所需的数据。下面开始实现这个类,如代码清单10-4所示。

代码清单 10-4 THAudioMixCompositionBuilder 实现

#import "THAudioMixCompositionBuilder.h"
#import "THAudioItem.h"
#import "THVolumeAutomation.h"
#import "THAudioMixComposition.h"
#import "THFunctions.h"

@interface THAudioMixCompositionBuilder ()
@property (strong, nonatomic) THTimeline *timeline;
@property (strong, nonatomic) AVMutableComposition *composition;
@end

@implementation THAudioMixCompositionBuilder

- (id)initWithTimeline:(THTimeline *)timeline {
    self = [super init];
    if (self) {
        _timeline = timeline;
    }
    return self;
}

- (id <THComposition>)buildComposition {

    self.composition = [AVMutableComposition composition];                  // 1

    [self addCompositionTrackOfType:AVMediaTypeVideo
                     withMediaItems:self.timeline.videos];

    [self addCompositionTrackOfType:AVMediaTypeAudio
                     withMediaItems:self.timeline.voiceOvers];

    AVMutableCompositionTrack *musicTrack =
        [self addCompositionTrackOfType:AVMediaTypeAudio
                         withMediaItems:self.timeline.musicItems];

    AVAudioMix *audioMix = [self buildAudioMixWithTrack:musicTrack];        // 2

    return [THAudioMixComposition compositionWithComposition:self.composition
                                                    audioMix:audioMix];
}

- (AVMutableCompositionTrack *)addCompositionTrackOfType:(NSString *)type
                                          withMediaItems:(NSArray *)mediaItems {

    if (!THIsEmpty(mediaItems)) {

        CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;

        AVMutableCompositionTrack *compositionTrack =
            [self.composition addMutableTrackWithMediaType:type
                                          preferredTrackID:trackID];
        // Set insert cursor to 0
        CMTime cursorTime = kCMTimeZero;

        for (THMediaItem *item in mediaItems) {

            if (CMTIME_COMPARE_INLINE(item.startTimeInTimeline,
                                      !=,
                                      kCMTimeInvalid)) {
                cursorTime = item.startTimeInTimeline;
            }

            AVAssetTrack *assetTrack =
                [[item.asset tracksWithMediaType:type] firstObject];

            [compositionTrack insertTimeRange:item.timeRange
                                      ofTrack:assetTrack
                                       atTime:cursorTime
                                        error:nil];

            // Move cursor to next item time
            cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
        }

        return compositionTrack;
    }

    return nil;
}

- (AVAudioMix *)buildAudioMixWithTrack:(AVCompositionTrack *)track {
    // To be implemented
}

@end

(1)创建一个新的AVMutableComposition实例并开始添加AVCompositionTrack对象,像上一章中使用addCompositionTrackOfType:withMedialtems:方法一样。这是上一章编写的同一个方法,不过有一点小的改动是返回了一个它创建的AVMutableCompositionTrack的引用。
(2)调用私有方法buildAudioMixWithTrack:创建一个AVAudioMix实例,我们在之后会实现它。这个方法接收应用音量调整的轨道的引用。最后,创建并返回-一个新的THAudioMix-Composition实例,给它传递组合和音频混合。

在学习buildAudioMixWithTrack:方法的实现之前,我们希望简单讨论一下应用程序如何在用户界面表示音量自动调整。包含在THTimeline对象中的音频项都是THAudioItem类型,这一类型的对象封装了基础AVAsset实例并额外定义了一个属性volumeAutomation,它是THVolumeAutomation实例的数组。THVolumeAutomation用于保存音量自动调节数据并在用户界面动态创建相应的值。它提供在AVMutableAudioMixInputParameters实例上设置的创建音量参数需要的所有相关数据。下面看一下buildAudioMixWithTrack方法的实现,了解一下这个对象是如何应用的,如代码清单10-5所示。

代码清单10-5创建 音频混合

- (AVAudioMix *)buildAudioMixWithTrack:(AVCompositionTrack *)track {
    THAudioItem *item = [self.timeline.musicItems firstObject];             // 1
    if (item) {
        AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];         // 2

        AVMutableAudioMixInputParameters *parameters =
            [AVMutableAudioMixInputParameters
                audioMixInputParametersWithTrack:track];



        for (THVolumeAutomation *automation in item.volumeAutomation) {     // 3
            [parameters setVolumeRampFromStartVolume:automation.startVolume
                                         toEndVolume:automation.endVolume
                                           timeRange:automation.timeRange];
        }

        audioMix.inputParameters = @[parameters];                           // 4
        return audioMix;
    }

    return nil;
}

(1)首先从时间轴得到音乐轨道的THAudioItem实例。应用程序只允许添加一个单独的音乐轨道,所以从musicltems 数组中获取firstObject。
(2)创建一个新的AVMutableAudioMix实例,用来保存输入参数。还需要创建一个新的AVMutableAudioMixInputParameters实例,将它与传递给方法的AVCompositionTrack进行关联。
(3)遍历音频条目的volumeAutomation数组中的对象,对每个实例在parameters对象上定义一个音量渐变,传递参数startVolume、endVolume 和timeRange.
(4)将parameters对象封装在NSArray中,并设置它作为音频混合的inputParameters,并返回这个音频混合实例。

运行应用程序,可以手动创建一个自定义的组合或点击Settings菜单选择LoadDefaultComposition。一个新的Audio小节被添加到Sttings菜单,现在它包含了两个开关来切换音频处理行为,如图10-4所示。


首先启用Fade In & Out切换开关并播放组合。可以看到出现一个音量曲线,它将我们听到的声音以可视化的效果进行呈现。现在音乐会平缓进入或消失,对用户来说这是一个更好的体验。现在激活Audio Ducking开关,会发现更新了音量自动调节,给出了闪避行为的可视化描述。再次播放组合可以发现画外音变得更响亮和清楚了。音量自动调整的数据是动态创建的,所以我们可以滑动控制画外音的大小来感觉音量是否合适。

10.3 小结

本章我们学习了如何使用AVAudioMix和AVAudioMixInputParameters来提供对于组合音频轨道更好的控制。我们对15Seconds应用程序的改动很小且非常容易实现,不过它对应用程序的提升却很显著。音频混合在所有的音频或视频编辑应用程序中都非常重要,不过可以看出使用AVComposition时几乎都可以利用AVAudioMix类来提升音频输出的效果。

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