ios - 音频检测器可以在设备上工作,但不能在模拟器上工作......和准确性
问题描述
嗨,萨雷姆
背景
我有一个应用程序可以检测到有人说“Hi Sarem”是一种电子锁。我想做一些像“Hi Siri”这样的东西,但既然是这样,我就去做了一些不同的事情,比如“Hi Sarem”。
执行
该代码从麦克风采样音频,拟合 FFT,然后检查三个连续频率,因此如果您例如吹口哨或在钢琴上弹奏正确的三个音符,则可以触发它。这些频率需要在一定时间内相互触发,并且可以使用滑块进行配置。该代码包含设置时间和公差等所需的参数。三个滑块代表“Hi-Sa-rem”中的三个“音符”。
用户界面
此处的图像给出了 UI 的概念。当检测到相关频率时,子弹会变成红色,一旦检测到整个序列,大的就会变成红色。顶部的滑块充当监视器,持续监视“听到”的频率,因此您可以使用它来校准音符。
问题
我对此有一些问题。准确性是一个重要因素,但不是主要因素。(我想如果我有一个更可怕的妈妈,这可能会更准确,也可以在午餐时间完成,但那是另一个故事......)
所以这里是 - 主要问题。
这在设备上运行良好,但在模拟器上我在日志中得到以下内容
2020-07-26 18:47:13.543219+0200 HiSarem[68826:1238118] [plugin] AddInstanceForFactory: No factory registered for id <CFUUID 0x600000788320> F8BB1C28-BAE8-11D6-9C31-00039315CD46
2020-07-26 18:47:13.575866+0200 HiSarem[68826:1238118] No exclusivity (null)
我怀疑这与访问权限有关,但我不确定。我到处找我知道的,但对我来说,错误会抱怨工厂没有注册是没有意义的。另外,为什么它在设备上而不是在模拟器上工作?现在我确实打印出我无法获得对设备的独占访问权限,但即使没有请求或锁定麦克风,我仍然会遇到问题。
代码
这来自单个视图应用程序ViewController
将提供的默认值,我确实描述了 UI 是如何连接到它的。因此,您应该能够将其简单地粘贴到项目中并在需要时运行它。这是一个测试项目,并不完善,但本着 MRE 的精神,您拥有所有代码。
#import <AVKit/AVKit.h>
#import <Accelerate/Accelerate.h>
#import "ViewController.h"
// Amplitute threshold
#define THRESHOLD 500
// Maximum frequency
#define MAXFREQ 7000
// Tolerance (% so 0.1 is 10%)
#define TOL 0.1
// Reset if no match within so many millis
#define RESETMIL 1500
#define BIGRESETMIL 5000
@interface ViewController () < AVCaptureAudioDataOutputSampleBufferDelegate >
@property (weak, nonatomic) IBOutlet UISlider * monitorSlider;
@property (weak, nonatomic) IBOutlet UISlider * phrase1Slider;
@property (weak, nonatomic) IBOutlet UISlider * phrase2Slider;
@property (weak, nonatomic) IBOutlet UISlider * phrase3Slider;
@property (weak, nonatomic) IBOutlet UILabel * phrase1Label;
@property (weak, nonatomic) IBOutlet UILabel * phrase2Label;
@property (weak, nonatomic) IBOutlet UILabel * phrase3Label;
@property (weak, nonatomic) IBOutlet UILabel * successLabel;
@property (nonatomic) BOOL busy;
@property (nonatomic, strong) AVCaptureSession * avSession;
@property (nonatomic, strong) AVCaptureInput * avInput;
@property (nonatomic, strong) AVCaptureDevice * avDevice;
@property (nonatomic, strong) AVCaptureOutput * avOutput;
@property (nonatomic) double prevF;
@property (nonatomic) NSDate * prevTime;
@end
@implementation ViewController
+ ( NSString * ) offText
{
return @"⚫️";
}
+ ( NSString * ) onText
{
return @"";
}
// See if we can turn on for a given frequency
- ( BOOL ) turnOn:( double ) f
want:( double ) w
{
double wLo = w * ( 1 - TOL );
double wHi = w * ( 1 + TOL );
return self.prevF < wLo && f >= wLo && f <= wHi;
}
// Update the value
- ( void ) measure:( int ) s
n:( int ) n
{
// Convert
double f = 44100.0 * s / n;
if ( f <= MAXFREQ )
{
self.monitorSlider.value = f;
// See where we are with the sliders
if ( [self.phrase1Label.text isEqualToString:ViewController.offText] )
{
// See if we can turn on 1
if ( [self turnOn:f want:self.phrase1Slider.value] )
{
self.phrase1Label.text = ViewController.onText;
// Match
self.prevTime = NSDate.date;
}
}
else if ( [self.phrase2Label.text isEqualToString:ViewController.offText] )
{
// See if we can turn on 2
if ( [self turnOn:f want:self.phrase2Slider.value] )
{
self.phrase2Label.text = ViewController.onText;
// Match
self.prevTime = NSDate.date;
}
}
else if ( [self.phrase3Label.text isEqualToString:ViewController.offText] )
{
// See if we can turn on 3
if ( [self turnOn:f want:self.phrase3Slider.value] )
{
self.phrase3Label.text = ViewController.onText;
self.successLabel.text = ViewController.onText;
// Big match
self.prevTime = NSDate.date;
}
}
}
// Reset if we do not get a match fast enough
if ( self.prevTime )
{
NSTimeInterval d = [NSDate.date timeIntervalSinceDate:self.prevTime] * 1000;
if ( d > RESETMIL )
{
self.phrase1Label.text = ViewController.offText;
self.phrase2Label.text = ViewController.offText;
self.phrase3Label.text = ViewController.offText;
}
if ( d > BIGRESETMIL )
{
self.successLabel.text = ViewController.offText;
}
}
}
- ( void ) viewDidLoad
{
super.viewDidLoad;
}
- ( void ) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if ( self.requestPermission )
{
self.startCapture;
}
}
- ( void ) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if ( self.avSession )
{
self.avSession.stopRunning;
self.avSession = nil;
}
}
- ( BOOL ) requestPermission
{
if ( AVAudioSession.sharedInstance.recordPermission == AVAudioSessionRecordPermissionGranted )
{
return YES;
}
else if ( AVAudioSession.sharedInstance.recordPermission == AVAudioSessionRecordPermissionDenied )
{
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"No ears"
message:@"I can not hear you - please change it quickly"
preferredStyle:UIAlertActionStyleDefault];
[alert addAction:[UIAlertAction actionWithTitle:@"Apologies"
style:UIAlertActionStyleDefault
handler:nil]];
[self presentViewController:alert
animated:YES
completion:nil];
return NO;
}
else
{
[AVAudioSession.sharedInstance requestRecordPermission:^ ( BOOL granted ) {
if ( granted )
{
self.startCapture;
}
}];
return NO;
}
}
- ( void ) startCapture
{
if ( ! self.busy )
{
self.busy = YES;
// Create the capture session.
NSError * avErr;
AVCaptureSession * captureSession = [[AVCaptureSession alloc] init];
// Default anyhow
captureSession.sessionPreset = AVCaptureSessionPresetHigh;
// Lookup the default audio device.
AVCaptureDevice * audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
if ( [audioDevice lockForConfiguration: & avErr] )
{
// Wrap the audio device in a capture device input.
AVCaptureDeviceInput * audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice
error: & avErr];
audioDevice.unlockForConfiguration;
if ( audioInput )
{
// If the input can be added, add it to the session.
if ( [captureSession canAddInput:audioInput] )
{
[captureSession addInput:audioInput];
AVCaptureAudioDataOutput * audioOutput = [[AVCaptureAudioDataOutput alloc] init];
if ( [captureSession canAddOutput:audioOutput] )
{
[audioOutput setSampleBufferDelegate:self
queue:dispatch_queue_create ( "ears", NULL )];
[captureSession addOutput:audioOutput];
// Do on background
dispatch_async ( dispatch_queue_create ( "spotty", NULL ), ^ {
NSLog ( @"Come to papa" );
captureSession.startRunning;
// Done
dispatch_async ( dispatch_get_main_queue (), ^ {
self.busy = NO;
self.avSession = captureSession;
self.avDevice = audioDevice;
self.avInput = audioInput;
self.avOutput = audioOutput;
} );
} );
}
else
{
NSLog ( @"Not today : add output" );
self.busy = NO;
}
}
else
{
NSLog( @"Sorry : add input" );
self.busy = NO;
}
}
else
{
NSLog( @"Ooops %@", avErr );
self.busy = NO;
}
}
else
{
NSLog( @"No exclusivity %@", avErr );
self.busy = NO;
}
}
}
#pragma mark -
#pragma mark Audio capture delegate
- ( void ) captureOutput:( AVCaptureOutput * ) output
didOutputSampleBuffer:( CMSampleBufferRef ) sampleBuffer
fromConnection:( AVCaptureConnection * ) connection
{
CMItemCount n = CMSampleBufferGetNumSamples ( sampleBuffer );
// We have our standards
if ( n == 1024 )
{
AudioBufferList audioBufferList;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer (
sampleBuffer,
NULL,
& audioBufferList,
sizeof ( audioBufferList ),
NULL,
NULL,
kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
& sampleBuffer
);
// Loop buffers
for ( int b = 0; b < audioBufferList.mNumberBuffers; b ++ )
{
// Evaluate samples
[self fft:audioBufferList.mBuffers [ b ].mData];
}
// Release the baby ... I mean buffer
CFRelease ( sampleBuffer );
}
}
- ( void ) fft:( SInt16 * ) samples
{
// In place so r and i are both input and output
COMPLEX_SPLIT c;
float r [ 512 ];
float i [ 512 ];
c.realp = r;
c.imagp = i;
// Load it and calculate maximum amplitute along the way
int amp = 0;
for ( int s = 0; s < 512; s ++ )
{
SInt16 ev = samples [ s * 2 ];
SInt16 od = samples [ s * 2 + 1 ];
// Convert to float
r [ s ] = ( float ) ev;
i [ s ] = ( float ) od;
if ( amp < ev )
{
amp = ev;
}
if ( amp < od )
{
amp = od;
}
}
// Only proceed if we have a big enough amplitute
if ( amp > THRESHOLD )
{
FFTSetup fft = vDSP_create_fftsetup ( 10, kFFTRadix2 );
if ( fft )
{
// FFT!
vDSP_fft_zrip ( fft, & c, 1, 10, FFT_FORWARD );
// Get frequency
int maxS = 0;
float maxF = 0;
for ( int s = 1; s < 512; s ++ )
{
float f = r [ s ] * r [ s ] + i [ s ] * i [ s ];
if ( f > maxF )
{
maxF = f;
maxS = s;
}
}
// Dealloc
vDSP_destroy_fftsetup ( fft );
// Done
dispatch_async ( dispatch_get_main_queue (), ^ {
[self measure:maxS
n:1024];
} );
}
}
}
@end
为什么这在设备上运行良好但在模拟器上拒绝?
然后,第二个问题,因为我确实在这里提供了所有细节,关于如何提高准确性的任何想法,或者只能通过使用更多频率触发器来实现?
TIA
解决方案
欢迎来到仅使用真实设备进行调试的世界,因为涉及到音频并且模拟器可能对此很挑剔。
请记住,nil/NULL
在为它们分配任何东西之前,您希望将 AVCaptureXYZ 指针设置为。音频是 C 业务,Objective-C 不是调用快速快速工作的方法的理想语言。即使它有效.. 还没有什么新东西。
此外,您可能在打开任何会话之前需要一个设备,因此 AVCaptureSession 可以在 AVCaptureDevice 启动之后进行。我知道文档告诉对方。但是当没有设备时你不需要会话,对吧?:)
写入时dispatch_async(...
,self->_busy
代替self.busy
. 并且dispatch_async(dispatch_get_main_queue(),^{})
是线程业务,把它放在它所属的地方,围绕访问 UIKit 的东西。在例子里面-(void)measure:(int)samples n:(int)n
。
帮自己一个忙,把objective-C-(void)fft:(SInt16 *)samples;
改成
void fft(SInt16* samples, int *result) {
//do fast fourier transformation
}
如果您需要在此函数中访问self,那么您实际上正在做一些接近错误的事情。避免在音频线程中使用 ObjC 方法调用。给这个函数一个void*
指针变量以使其可以从函数内部访问呢?或者将引用指针传递给函数以更改给定变量的内容。或者让它返回结果。
并忽略这个特定的模拟器警告。这是一个警告,它为工厂添加了一个实例,因为那里还没有那个 CFUUID。这不是一个错误,这是因为你在 OSX 偏离路线的模拟器上运行 AV_XYZ-iOS 的东西。
一些微小的变化..您的浮点转换可能看起来像。
SInt16 amp = 0;
int s=0;
SInt16 evens;
SInt16 odds;
while ( s < 512 ) {
evens = samples[s * 2 ];
odds = samples[s * 2 + 1];
r[s] = (float)evens;
i[s] = (float)odds;
amp = MAX(amp,MAX(odds,evens));
s++;
}
并在委托方法中-captureOutput:didOutputSampleBuffer:fromConnection:
CMItemCount numSamplesInBuffer = CMSampleBufferGetNumSamples(sampleBuffer);
// works only with 1024 samples
if ( numSamplesInBuffer == 1024 ) {
AudioBufferList audioBufferList;
CMBlockBufferRef buffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer,
NULL,
&audioBufferList,
sizeof(audioBufferList),
NULL,
NULL,
kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
&buffer //now its correct pointer
);
//provide variable for feedback
int result = 0;
// Loop buffers
int b = 0;
for (; b < audioBufferList.mNumberBuffers; b ++) {
// Evaluate samples
// use C if possible, don't call ObjC in functions if possible
fft(audioBufferList.mBuffers[b].mData, &result);
}
// later Release the baby ... I mean buffer <- yes buffer :)
CFRelease(buffer);
[self measure:result n:1024];
}
推荐阅读
- c - 在这个 mutex/pthread_cond_wait 结构中,我的数据会在哪里丢失?
- swiftui - SwiftUI - @State property is not updated
- odoo - 为什么我的简单 odoo 扩展在升级时会使我的服务器崩溃?
- java - 2020 年 11 月 1 日/“帐户保留”:是否必须向“帐户保留”用户显示说明性消息?
- android - 行中图像的动态列表,带有换行符(之间没有空格),允许长按图像打开编辑选项 - Flutter
- laravel - 如何在 sql 数组 laravel 中找到项目
- reactjs - 如何在组件卸载上使用 useEffect 挂钩有条件地运行代码
- javascript - 如何通过函数式编程在一个公共键上合并 N 个元组数组?
- python-3.x - cmd没有从spyder脚本中显示
- javascript - 如果使用箭头功能,如何访问 Vue 上的数据