使用 AVAudioEngine 进行电平测量
Posted
技术标签:
【中文标题】使用 AVAudioEngine 进行电平测量【英文标题】:Level Metering with AVAudioEngine 【发布时间】:2015-08-18 22:09:07 【问题描述】:我刚刚在AVAudioEngine
上观看了 WWDC 视频(第 502 场 AVAudioEngine
实践),我很高兴能够制作基于这项技术的应用程序。
我无法弄清楚如何对麦克风输入或混音器的输出进行电平监控。
有人可以帮忙吗?明确地说,我说的是监控当前输入信号(并在 UI 中显示),而不是通道/轨道的输入/输出音量设置。
我知道您可以使用 AVAudioRecorder
执行此操作,但这不是 AVAudioEngine
所需的 AVAudioNode
。
【问题讨论】:
【参考方案1】:尝试在主混音器上安装一个水龙头,然后通过设置帧长度使其更快,然后读取样本并获得平均值,如下所示:
在顶部导入框架
#import <Accelerate/Accelerate.h>
添加属性
@property float averagePowerForChannel0;
@property float averagePowerForChannel1;
那么下面同理>>
self.mainMixer = [self.engine mainMixerNode];
[self.mainMixer installTapOnBus:0 bufferSize:1024 format:[self.mainMixer outputFormatForBus:0] block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when)
[buffer setFrameLength:1024];
UInt32 inNumberFrames = buffer.frameLength;
if(buffer.format.channelCount>0)
Float32* samples = (Float32*)buffer.floatChannelData[0];
Float32 avgValue = 0;
vDSP_meamgv((Float32*)samples, 1, &avgValue, inNumberFrames);
self.averagePowerForChannel0 = (LEVEL_LOWPASS_TRIG*((avgValue==0)?-100:20.0*log10f(avgValue))) + ((1-LEVEL_LOWPASS_TRIG)*self.averagePowerForChannel0) ;
self.averagePowerForChannel1 = self.averagePowerForChannel0;
if(buffer.format.channelCount>1)
Float32* samples = (Float32*)buffer.floatChannelData[1];
Float32 avgValue = 0;
vDSP_meamgv((Float32*)samples, 1, &avgValue, inNumberFrames);
self.averagePowerForChannel1 = (LEVEL_LOWPASS_TRIG*((avgValue==0)?-100:20.0*log10f(avgValue))) + ((1-LEVEL_LOWPASS_TRIG)*self.averagePowerForChannel1) ;
];
然后,得到你想要的目标值
NSLog(@"===test===%.2f", self.averagePowerForChannel1);
要获得峰值,请使用 vDSP_maxmgv 而不是 vDSP_meamgv。
LEVEL_LOWPASS_TRIG 是一个简单的过滤器,取值在 0.0 到 1.0 之间,如果设置为 0.0,您将过滤所有值并且不会获取任何数据。如果将其设置为 1.0,您将获得太多噪音。基本上,值越高,您将获得更多的数据变化。似乎 0.10 到 0.30 之间的值对于大多数应用程序来说是好的。
【讨论】:
LEVEL_LOWPASS_TRIG 使用的值(或范围)是多少? 要使用 vDSP_meamgv ,请执行“import Accelerate”以使用 Apple 的高性能数学框架。 你能在 Github 上发布一个完整的工作示例吗? @apocolipse 我也不知道该放什么... LEVEL_LOWPASS_TRIG=0.01 对我有用。 这是最好的选择。我为 Swift 做了同样的事情,所以这个 ObjC 语法是我在另一个应用程序上的救命稻草。它可以针对音量的不同视觉表示进行调整:波形图表、简单的音量条或依赖于音量的透明度(褪色的麦克风图标等...)。【参考方案2】:我发现了另一种有点奇怪的解决方案,但效果很好,比 Tap 好得多。混音器没有 AudioUnit,但如果将其转换为 AVAudioIONode,您可以获得 AudioUnit 并使用 ios 的计量工具。方法如下:
启用或禁用计量:
- (void)setMeteringEnabled:(BOOL)enabled;
UInt32 on = (enabled)?1:0;
AVAudioIONode *node = (AVAudioIONode*)self.engine.mainMixerNode;
OSStatus err = AudioUnitSetProperty(node.audioUnit, kAudioUnitProperty_MeteringMode, kAudioUnitScope_Output, 0, &on, sizeof(on));
要更新仪表:
- (void)updateMeters;
AVAudioIONode *node = (AVAudioIONode*)self.engine.mainMixerNode;
AudioUnitParameterValue level;
AudioUnitGetParameter(node.audioUnit, kMultiChannelMixerParam_PostAveragePower, kAudioUnitScope_Output, 0, &level);
self.averagePowerForChannel1 = self.averagePowerForChannel0 = level;
if(self.numberOfChannels>1)
err = AudioUnitGetParameter(node.audioUnit, kMultiChannelMixerParam_PostAveragePower+1, kAudioUnitScope_Output, 0, &level);
【讨论】:
【参考方案3】:'Farhad Malekpour'答案的等效 Swift 3 代码
在顶部导入框架
import Accelerate
全局声明
private var audioEngine: AVAudioEngine?
private var averagePowerForChannel0: Float = 0
private var averagePowerForChannel1: Float = 0
let LEVEL_LOWPASS_TRIG:Float32 = 0.30
在需要的地方使用下面的代码
let inputNode = audioEngine!.inputNode//since i need microphone audio level i have used `inputNode` otherwise you have to use `mainMixerNode`
let recordingFormat: AVAudioFormat = inputNode!.outputFormat(forBus: 0)
inputNode!.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) [weak self] (buffer:AVAudioPCMBuffer, when:AVAudioTime) in
guard let strongSelf = self else
return
strongSelf.audioMetering(buffer: buffer)
计算
private func audioMetering(buffer:AVAudioPCMBuffer)
buffer.frameLength = 1024
let inNumberFrames:UInt = UInt(buffer.frameLength)
if buffer.format.channelCount > 0
let samples = (buffer.floatChannelData![0])
var avgValue:Float32 = 0
vDSP_meamgv(samples,1 , &avgValue, inNumberFrames)
var v:Float = -100
if avgValue != 0
v = 20.0 * log10f(avgValue)
self.averagePowerForChannel0 = (self.LEVEL_LOWPASS_TRIG*v) + ((1-self.LEVEL_LOWPASS_TRIG)*self.averagePowerForChannel0)
self.averagePowerForChannel1 = self.averagePowerForChannel0
if buffer.format.channelCount > 1
let samples = buffer.floatChannelData![1]
var avgValue:Float32 = 0
vDSP_meamgv(samples, 1, &avgValue, inNumberFrames)
var v:Float = -100
if avgValue != 0
v = 20.0 * log10f(avgValue)
self.averagePowerForChannel1 = (self.LEVEL_LOWPASS_TRIG*v) + ((1-self.LEVEL_LOWPASS_TRIG)*self.averagePowerForChannel1)
【讨论】:
您有此代码的工作示例吗?这显示了整个周期..您如何实例化 AudioEngine 等.. 新手问题 - 如果节点设置在通道 0 上,为什么会有 2 个通道?【参考方案4】:#define LEVEL_LOWPASS_TRIG .3
#import "AudioRecorder.h"
@implementation AudioRecord
-(id)init
self = [super init];
self.recordEngine = [[AVAudioEngine alloc] init];
return self;
/** ---------------------- Snippet ***.com not including Audio Level Meter --------------------- **/
-(BOOL)recordToFile:(NSString*)filePath
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
const Float64 sampleRate = 44100;
AudioStreamBasicDescription aacDesc = 0 ;
aacDesc.mSampleRate = sampleRate;
aacDesc.mFormatID = kAudioFormatMPEG4AAC;
aacDesc.mFramesPerPacket = 1024;
aacDesc.mChannelsPerFrame = 2;
ExtAudioFileRef eaf;
OSStatus err = ExtAudioFileCreateWithURL((__bridge CFURLRef)fileURL, kAudioFileAAC_ADTSType, &aacDesc, NULL, kAudioFileFlags_EraseFile, &eaf);
assert(noErr == err);
AVAudioInputNode *input = self.recordEngine.inputNode;
const AVAudioNodeBus bus = 0;
AVAudioFormat *micFormat = [input inputFormatForBus:bus];
err = ExtAudioFileSetProperty(eaf, kExtAudioFileProperty_ClientDataFormat, sizeof(AudioStreamBasicDescription), micFormat.streamDescription);
assert(noErr == err);
[input installTapOnBus:bus bufferSize:1024 format:micFormat block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when)
const AudioBufferList *abl = buffer.audioBufferList;
OSStatus err = ExtAudioFileWrite(eaf, buffer.frameLength, abl);
assert(noErr == err);
/** ---------------------- Snippet from ***.com in different context --------------------- **/
UInt32 inNumberFrames = buffer.frameLength;
if(buffer.format.channelCount>0)
Float32* samples = (Float32*)buffer.floatChannelData[0];
Float32 avgValue = 0;
vDSP_maxv((Float32*)samples, 1.0, &avgValue, inNumberFrames);
self.averagePowerForChannel0 = (LEVEL_LOWPASS_TRIG*((avgValue==0)?
-100:20.0*log10f(avgValue))) + ((1- LEVEL_LOWPASS_TRIG)*self.averagePowerForChannel0) ;
self.averagePowerForChannel1 = self.averagePowerForChannel0;
dispatch_async(dispatch_get_main_queue(), ^
self.levelIndicator.floatValue=self.averagePowerForChannel0;
);
/** ---------------------- End of Snippet from ***.com in different context --------------------- **/
];
BOOL startSuccess;
NSError *error;
startSuccess = [self.recordEngine startAndReturnError:&error];
return startSuccess;
@end
【讨论】:
对于@omarojo。这是使用其他两个答案的组合的工作代码。 .h 文件来了【参考方案5】:#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/ExtendedAudioFile.h>
#import <CoreAudio/CoreAudio.h>
#import <Accelerate/Accelerate.h>
#import <AppKit/AppKit.h>
@interface AudioRecord : NSObject
@property (nonatomic) AVAudioEngine *recordEngine;
@property float averagePowerForChannel0;
@property float averagePowerForChannel1;
@property float numberOfChannels;
@property NSLevelIndicator * levelIndicator;
-(BOOL)recordToFile:(NSString*)filePath;
@end
【讨论】:
要使用,只需调用 newAudioRecord = [AudioRecord new]; newAudioRecord.levelIndicator=self.audioLevelIndicator; --- 实验性(不是很好)[newAudioRecord recordToFile:fullFilePath_Name]; [newAudioRecord.recordEngine 停止]; [newAudioRecord.recordEngine 重置]; newAudioRecord.recordEngine pause];恢复:[newAudioRecord.recordEngine startAndReturnError:NULL];【参考方案6】:Swift 5+
I got help from this project.
下载以上项目并在您的项目中复制“Microphone.swift”类。
复制粘贴这些fowling代码到你的项目中:
import AVFoundation
private var mic = MicrophoneMonitor(numberOfSamples: 1)
private var timer:Timer!
override func viewDidLoad()
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(startMonitoring), userInfo: nil, repeats: true)
timer.fire()
@objc func startMonitoring()
print("sound level:", normalizeSoundLevel(level: mic.soundSamples.first!))
private func normalizeSoundLevel(level: Float) -> CGFloat
let level = max(0.2, CGFloat(level) + 50) / 2 // between 0.1 and 25
return CGFloat(level * (300 / 25)) // scaled to max at 300 (our height of our bar)
3.开啤酒庆祝一下!
【讨论】:
这是不断地将音频重新编码到文件中吗?似乎效率不高。 这是我找到的唯一方法!以上是关于使用 AVAudioEngine 进行电平测量的主要内容,如果未能解决你的问题,请参考以下文章
测量在 iOS 中内联播放的嵌入式 YouTube 视频的音频电平
使用 AVAudioEngine 进行语音识别会在录制后阻止声音