-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSpirometerEffortAnalyzer.m
More file actions
636 lines (519 loc) · 25.9 KB
/
SpirometerEffortAnalyzer.m
File metadata and controls
636 lines (519 loc) · 25.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
//
// SpirometerAnalyzer.m
// OpenSpirometry
//
// Created by Eric Larson
// Copyright (c) 2015 Eric Larson. All rights reserved.
//
// TODO: save flow rate
// TODO: calculate volume from analyzer (separate model)
// TODO: provide end of effort analytics (separate model): cough, bad start, not long enough, insufficient effort
// TODO: enable custom whistles through some mechanism for setting the whistle (use UI Delegate for presenting settings)
// TODO: whistle frequency converter (should be stored in this model, most likely)
// TODO: extrapolate tail (don't want this--can the whistle be made better to get the lower range? How Low?)
// TODO: fundamental peak following across frames
// TODO: provide reproducibility analytics (in separate model, not built yet)
#import "SpirometerEffortAnalyzer.h"
#import <QuartzCore/QuartzCore.h>
#import "Novocaine.h"
#import "FFTHelper.h"
#import "BufferedOverlapQueue.h"
#import "PeakFinder.h"
#import "FlowVolumeDataAnalyzer.h"
@interface SpirometerEffortAnalyzer()
@property (strong, nonatomic) Novocaine* audioManager;
@property (strong, nonatomic) FFTHelper* fftHelper;
@property (strong, nonatomic) BufferedOverlapQueue* dataBuffer;
@property (strong, nonatomic) PeakFinder *peakFinder;
@property (strong, nonatomic) FlowVolumeDataAnalyzer *fvAnalyzer;
@property (atomic) BOOL isShuttingDown;
@property (nonatomic) NSUInteger samplesRead;
@property (atomic) NSUInteger numBlocksProcessed;
@property (atomic) NSUInteger numProcessedSamples;
@property (nonatomic) float frequencyResolution;
@property (nonatomic) float silenceThreshold;
@property (nonatomic) BOOL silenceThresholdIsSet;
@property (nonatomic) BOOL audioDebugIsActive;
@property (nonatomic) BOOL shouldSaveEffortsToDocumentDirectory;
@property (nonatomic, strong) NSString *audioDebugFileName;
@end
@implementation SpirometerEffortAnalyzer{
struct {
unsigned int didFinishCalibratingSilence:1;
unsigned int didTimeoutWaitingForTestToStart:1;
unsigned int didStartExhaling:1;
unsigned int willEndTestSoon:1;
unsigned int didCancelEffort:1;
unsigned int didEndEffortWithResults:1;
unsigned int didUpdateFlowAndVolume:1;
unsigned int didUpdateAudioBufferWithMaximum:1;
} delegateRespondsTo;
}
- (void)setDelegate:(id <SpirometerEffortDelegate>)aDelegate {
if (_delegate != aDelegate) {
_delegate = aDelegate;
delegateRespondsTo.didFinishCalibratingSilence = [_delegate respondsToSelector:@selector(didFinishCalibratingSilence)];
delegateRespondsTo.didTimeoutWaitingForTestToStart = [_delegate respondsToSelector:@selector(didTimeoutWaitingForTestToStart)];
delegateRespondsTo.didStartExhaling = [_delegate respondsToSelector:@selector(didStartExhaling)];
delegateRespondsTo.willEndTestSoon = [_delegate respondsToSelector:@selector(willEndTestSoon)];
delegateRespondsTo.didCancelEffort = [_delegate respondsToSelector:@selector(didCancelEffort)];
delegateRespondsTo.didEndEffortWithResults = [_delegate respondsToSelector:@selector(didEndEffortWithResults:)];
delegateRespondsTo.didUpdateFlowAndVolume = [_delegate respondsToSelector:@selector(didUpdateFlow:andVolume:)];
delegateRespondsTo.didUpdateAudioBufferWithMaximum = [_delegate respondsToSelector:@selector(didUpdateAudioBufferWithMaximum:)];
}
}
//=============================================================================================================
#pragma mark Lazy Instantiation
-(Novocaine*)audioManager{
if(!_audioManager){
_audioManager = [Novocaine audioManager];
if(_audioDebugIsActive){
[_audioManager overrideMicrophoneWithAudioFile:_audioDebugFileName];
}
_audioManager.shouldSaveContinuouslySampledMicrophoneAudioDataToNewFile = _shouldSaveEffortsToDocumentDirectory;
// and the other properties dependent here
_frequencyResolution = ((float)BUFFER_SIZE)/_audioManager.samplingRate;
_peakFinder = [[PeakFinder alloc]initWithFrequencyResolution:_frequencyResolution];
}
return _audioManager;
}
-(FFTHelper*)fftHelper{
if(!_fftHelper){
_fftHelper = [[FFTHelper alloc] initWithFFTSize:BUFFER_SIZE
andWindow:WindowTypeHann];
}
return _fftHelper;
}
//=============================================================================================================
#pragma mark Init/Dealloc
// set up as singleton class
+ (SpirometerEffortAnalyzer *)sharedInstance{
static SpirometerEffortAnalyzer * _sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate,^{
_sharedInstance = [[SpirometerEffortAnalyzer alloc] init];
});
return _sharedInstance;
}
-(id)init{
if(self = [super init]){
[self setup];
return self;
}
return nil;
}
-(void)dealloc{
if(_audioManager){
[_audioManager setInputBlock:nil];
[_audioManager teardownAudio];
}
}
-(void) safeFree:(float **) var{
// don't want to use c++ here for passing by reference so instead we will use double indirection
if(*var){
free(*var);
}
*var = nil;
}
-(void) setup{
// instantiate in init
_dataBuffer = [[BufferedOverlapQueue alloc] initWithBufferLength:BUFFER_SIZE andOverlapLength:BUFFER_OVERLAP];
_dataBuffer.delegate = self;
_isShuttingDown = NO;
_samplesRead = 0;
_numBlocksProcessed = 0;
_numProcessedSamples = 0;
_silenceThreshold = 0;
_silenceThresholdIsSet = NO;
_currentStage = SpirometryStageIsIdle;
_prefferredAudioMaxUpdateIntervalInSeconds = 1.0/30.0; // 30FPS default
_audioDebugIsActive = NO;
_audioDebugFileName = nil;
_shouldSaveEffortsToDocumentDirectory = NO;
_whistle = [[SpirometryWhistle alloc]init]; // whistle is set to default params (Sato Whistle)
_fvAnalyzer = [[FlowVolumeDataAnalyzer alloc] init];
}
//=============================================================================================================
#pragma mark Permission
// return if successful
-(void)askPermissionToUseAudioIfNotDone{
if(![self delegateCanPresentUI]){return;}
if(self.currentStage == SpirometryStageIsIdle){
//display alert if permissions not set
// maybe get text for alert from use or provide default
enum AVAudioSessionRecordPermission auth = [Novocaine checkAudioAuthorization];
//BOOL shouldInformUserDeviceIsRestricted = NO; // No support for this yet in AVAudioSession
switch (auth) {
case AVAudioSessionRecordPermissionGranted:
{
// nothing to do, audio can be setup now without prompting
[self setupAudio];
}
break;
case AVAudioSessionRecordPermissionDenied:
{
//we have been denied in the past
[self informUserToChangeRecordingSettings];
}
break;
case AVAudioSessionRecordPermissionUndetermined:
// ask for access twice, once where we explain the process, then with iOS
[self explainRecordingPermissions];
break;
//case Restricted: // No support for this yet in AVAudioSession yet
//shouldInformUserDeviceIsRestricted = YES;
}
}
}
-(BOOL)delegateCanPresentUI{
if(!self.delegate){return NO;} // if no delegate, no UI. sorry
if(![self.delegate isKindOfClass:[UIViewController class]]){return NO;} // not a UIcontroller, can't do this, sorry
return YES;
}
-(void)informUserToChangeRecordingSettings{
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Microphone Access Denied Previously"
message:@"To perform a spirometry test, you will need to allow this app access to record audio.\n\n To allow access: \n1. Close this app. \n2.Open settings from the home screen.\n3. Find and Click on this app. \n4. Change the privacy to \"Allow\". "
preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
}];
//TODO: add a "settings" button for easy launch
[alert addAction:defaultAction];
[(UIViewController*)self.delegate presentViewController:alert animated:YES completion:nil];
}
-(void)explainRecordingPermissions{
if([self delegateCanPresentUI]){
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Need Microphone Access"
message:@"To perform a spirometry test, you will need to allow this app recording access in order to listen to the sound of the test. \n\nRecording will only occur during the test and will never be saved."
preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"Allow" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
NSLog(@"User wants to attain access.");
[self setupAudio];
}];
UIAlertAction* refuseAction = [UIAlertAction actionWithTitle:@"Ask Later" style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
NSLog(@"User will wait to allow access");
}];
[alert addAction:defaultAction];
[alert addAction:refuseAction];
[(UIViewController*)self.delegate presentViewController:alert animated:YES completion:nil];
}
}
-(void)askForRecordingPermission{
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
if (granted) {
NSLog(@"Recording Permission Granted through Prompt");
[self setupAudio];
} else {
NSLog(@"Recording Permission Denied through Prompt");
}
}];
}
//=============================================================================================================
#pragma mark Analyze Audio Methods
-(void)setupAudio{
[self.audioManager setInputBlock:nil];
[self.audioManager pause];
self.currentStage = SpirometryStageIsIdle;
}
-(void)resetEffort{
self.samplesRead = 0;
self.numBlocksProcessed = 0;
self.numProcessedSamples = 0;
self.silenceThreshold = 0;
self.silenceThresholdIsSet = NO;
self.isShuttingDown = NO;
[self.fvAnalyzer clearDataInEffort]; // object recycled for each effort
}
-(void)beginListeningForEffort{
[self resetEffort];
self.currentStage = SpirometryStageIsCalibratingSilence;
// audio instantiated here if neccessary (will generate microphone ask if not set)
// grab non-reference count adding handle to ourselves
__block SpirometerEffortAnalyzer * __weak weakSelf = self;
[self.audioManager setInputBlock:^(float *data, UInt32 numFrames, UInt32 numChannels)
{
// add data to the ring buffer (interleaved in case iOS upgrades microphone or test is over airplay)
if(weakSelf != nil && !weakSelf.isShuttingDown){
// copy data over to overlap buffer
[weakSelf.dataBuffer addFreshInterleavedFloatData:data withLength:numFrames fromChannel:0 withNumChannels:numChannels];
weakSelf.samplesRead += numFrames; // increment the total samples collected thus far
// get max of this buffer stream
float maxValue;
vDSP_maxv(data, 1, &maxValue, numFrames);
// now get out of this block! It needs to run way too often
dispatch_async(dispatch_get_main_queue(),^{
// analyze stage based on most recent data (super fast for small frame size here)
weakSelf.currentStage = [weakSelf analyzeStagesFromAudioMax:maxValue];
// shut down audio from main queue if needed
// this has sync code in it, so it might be a bit slow for the main queue
[weakSelf endEffortIfDone];
});
}
}];
[self.audioManager play];
}
-(SpirometryStage)analyzeStagesFromAudioMax:(float)maxValue{
static BOOL testStarted = NO;
static CFTimeInterval lastGoodTime = 0;
static CFTimeInterval testStartTime = 0;
static CFTimeInterval silencedEndedStartTime = 0;
if( self.samplesRead < self.audioManager.samplingRate*MAX_CALIBRATION_TIME){
// still collecting samples for silence
//reset state
testStarted = NO;
lastGoodTime = 0;
testStartTime = 0;
silencedEndedStartTime = 0;
// better be quite here
self.silenceThreshold = maxValue>self.silenceThreshold ? maxValue : self.silenceThreshold;
return SpirometryStageIsCalibratingSilence;
}
else if(!self.silenceThresholdIsSet && self.samplesRead >= self.audioManager.samplingRate*2){
// just finished getting all the samples here, lock in silence threshold and notify delegate
self.silenceThresholdIsSet = YES; // now we are set
silencedEndedStartTime = CACurrentMediaTime();
if(delegateRespondsTo.didFinishCalibratingSilence){
dispatch_async(dispatch_get_main_queue(),^{
//delegation on main queue for did finish calibrating
[self.delegate didFinishCalibratingSilence];
});
}
}
// update the delegate about the audio (this will happen after calibrating silence)
if(delegateRespondsTo.didUpdateAudioBufferWithMaximum){
static CFTimeInterval lastAudioUpdateTime = 0;
CFTimeInterval tempCurrTime = CACurrentMediaTime();
CFTimeInterval elapsedTimeForAudioUpdate = tempCurrTime-lastAudioUpdateTime;
if(lastAudioUpdateTime==0 || elapsedTimeForAudioUpdate >= self.prefferredAudioMaxUpdateIntervalInSeconds){
lastAudioUpdateTime = tempCurrTime;
dispatch_async(dispatch_get_main_queue(),^{
//delegation on main queue for did finish calibrating
[self.delegate didUpdateAudioBufferWithMaximum:maxValue];
});
}
}
if(testStarted){
CFTimeInterval elapsedTime = CACurrentMediaTime()-lastGoodTime;
CFTimeInterval totalEffortTime =CACurrentMediaTime()-silencedEndedStartTime;
if(totalEffortTime>TEST_MAX_DURATION_SECONDS)
return SpirometryStageIsFinished;
if(maxValue>TEST_END_THRESH*self.silenceThreshold){
// audio still way above threshold
lastGoodTime = CACurrentMediaTime();
return SpirometryStageIsExhaling;
}
else {
if(elapsedTime>WAIT_DURATION_AFTER_TEST){ // has low audio for a while now, end effort
NSLog(@"Test has finished (no more audible sound)");
return SpirometryStageIsFinished;
}else if(elapsedTime<WAIT_DURATION_AFTER_PEAK){
// below, threshold, but too close to last update to end the audio test
return SpirometryStageIsExhaling;
}
if(delegateRespondsTo.willEndTestSoon){
dispatch_async(dispatch_get_main_queue(),^{
[self.delegate willEndTestSoon];
});
}
return SpirometryStageIsWaitingForEndOfTest;
}
return SpirometryStageIsExhaling;
}
else if(maxValue>TEST_START_THRESH*self.silenceThreshold){
testStarted = YES;
lastGoodTime = CACurrentMediaTime();
testStartTime = lastGoodTime-1.0;
NSLog(@"Spirometry Effort has begun");
if(delegateRespondsTo.didStartExhaling){
dispatch_async(dispatch_get_main_queue(),^{
[self.delegate didStartExhaling];
});
}
return SpirometryStageIsExhaling;
}
CFTimeInterval effortWaitTime =CACurrentMediaTime()-silencedEndedStartTime;
if(silencedEndedStartTime!=0 && effortWaitTime > TIME_OUT_WAIT_FOR_TEST_START){
return SpirometryStageDidTimeOutWaitingForEffort;
}
return SpirometryStageIsWaitingForTestToBegin;
}
// this delegate method is performed asynchronously
// if blocks are not consumed faster than they are added, then memory will build up
// the block passed in is freed immediately after this executes
-(void)didFillBuffer:(DataBufferBlock *)block{
//CFAbsoluteTime timeInQueue = CACurrentMediaTime()-block.timeCreated;
static float lastFrequency = -1.0;
const unsigned long lenMagBuffer = self.fftHelper.fftSizeOver2;
float *fftMagnitudeBuffer = (float *)calloc(lenMagBuffer,sizeof(float));
// take FFT
[self.fftHelper performForwardFFTWithData:block.data
andCopydBMagnitudeToBuffer:fftMagnitudeBuffer];
// find local maxima and identify most likely harmonics of whistle (returns nil if none exist)
float minimumMagnitude = PEAK_DBMAG_START;
if(lastFrequency>0)
minimumMagnitude = PEAK_DBMAG_SUSTAINED;
NSArray *fundamentalFrequencies = [self.peakFinder getFundamentalPeaksFromBuffer:fftMagnitudeBuffer
withLength:lenMagBuffer
usingWindowSize:PEAK_WINDOW_SIZE
andPeakMagnitudeMinimum:minimumMagnitude
aboveFrequency:MIN_FREQUENCY_OF_WHISTLE_IN_HZ];
// if there was at least one fundamental peak frequency
if(fundamentalFrequencies){
if([fundamentalFrequencies count] > NUM_PEAKS_IS_COUGH){ // identify spectra with many peaks as cough
NSLog(@"Detected cough from %ld peaks", (unsigned long)[fundamentalFrequencies count]);
[self.fvAnalyzer addCustomErrorToEffort:@"Cough Detected During Test"
forKey:@"Cough"];
}
// first pass flow rate detection (no fundamental following yet)
float frequency = ((Peak*)[fundamentalFrequencies objectAtIndex:0]).frequency;
if(lastFrequency>0){
// grab the closest frequency to last detection
float minDistance = 100000.0;
float bestFrequency = -1;
NSRange range = NSMakeRange(0, MIN(fundamentalFrequencies.count,3));
float bestMag = ((Peak*)[fundamentalFrequencies firstObject]).magnitude;
for(Peak* f in [fundamentalFrequencies subarrayWithRange:range]){
float tmp = fabs(f.frequency-lastFrequency);
if(tmp<minDistance && f.magnitude/bestMag > 0.1){ // close and magnitude relative to max is large
minDistance = tmp;
bestFrequency = f.frequency;
}
}
if(bestFrequency>0)
frequency = bestFrequency;
}
lastFrequency = frequency;
// convert to flow rate from frequency using whistle model
float flow = [self.whistle calcFlowInLiterPerSecondFromFrequencyInHz:frequency];
float volume;
[self.fvAnalyzer addFlowEstimateInLitersPerSecond:flow // calced flow rate
withTimeStamp:block.timeCreated]; // original time stamp for audio
// this is not a great method, maybe do not even offer volume until we know start of effort for sure
// query running volume from analyzer (using the new flow we just passed in)
volume = [self.fvAnalyzer getEstimateOfTotalVolumeInLiters];
// call delegate did update flow and volume on main queue
if(delegateRespondsTo.didUpdateFlowAndVolume){
dispatch_async(dispatch_get_main_queue(),^{
[self.delegate didUpdateFlow:flow andVolume:volume];
});
}
}
else{
//TODO: handle frequency dropout (probably not done here)
}
// if(DEBUG)
// {
// // find maximum of spectrum, just some debug info here
// float maxValue;
// unsigned long maxIndex;
// vDSP_maxvi(fftMagnitudeBuffer, 1, &maxValue, &maxIndex, lenMagBuffer);
//
// float interpolatedFrequency = [self.peakFinder getFrequencyFromIndex:maxIndex usingData:fftMagnitudeBuffer];
// NSLog(@"Freq = %.2f, Mag=%.2f, QTime = %.2f, Blocks = %ld",
// interpolatedFrequency,
// maxValue,
// timeInQueue,
// (unsigned long)self.dataBuffer.numFullBuffers);
// }
free(fftMagnitudeBuffer);
}
-(void)didFinishProcessingAllBuffers{
// if number of buffers is all done, and we are shutting down
if(self.isShuttingDown && self.currentStage==SpirometryStageIsFinished){
self.currentStage = SpirometryStageIsAnalyzingResults;
// finalize and get the results
NSMutableDictionary *results = [[self.fvAnalyzer finalizeCurvesAndGetResults]mutableCopy];
[results setObject:@"None" forKey:@"RecordedAudioFilenameForEffort"];
// add in the filname if one exists for this file
if(self.audioManager.audioFileWrittenOut != nil){
[results setObject:[self.audioManager.audioFileWrittenOut copy] forKey:@"RecordedAudioFilenameForEffort"];
}
// perform delegation for effort did finish
if(delegateRespondsTo.didEndEffortWithResults){
dispatch_async(dispatch_get_main_queue(),^{
[self.delegate didEndEffortWithResults:[[NSDictionary alloc]initWithDictionary:results]];
});
}
// add this for synchronization of the serial main queue (not UI related, but simple calculation so, meh)
dispatch_async(dispatch_get_main_queue(),^{
self.currentStage = SpirometryStageIsIdle;
});
}
}
-(void)endEffortIfDone{ // only called from main queue
if(!self.isShuttingDown){
if(self.currentStage == SpirometryStageIsFinished)
{
self.isShuttingDown = YES; // this function should only be called from main queue so no semaphore needed
NSLog(@"Effort did end, Shutting Down Audio");
[self.audioManager pause]; // stop
[self.dataBuffer processRemainingBlocks]; // kill remainder of queue
[self didFinishProcessingAllBuffers];
}
else if(self.currentStage == SpirometryStageDidTimeOutWaitingForEffort){
self.isShuttingDown = YES; // this function should only be called from main queue so no semaphore needed
NSLog(@"Effort did time out waiting");
[self.audioManager pause]; // stop
[self.dataBuffer clear];
self.currentStage = SpirometryStageIsIdle;
// perform delegation for effort did timeout
if(delegateRespondsTo.didTimeoutWaitingForTestToStart){
[self.delegate didTimeoutWaitingForTestToStart];
}
}
}
}
//====================================================================================================
#pragma mark User Request Controls
-(void)requestThatCurrentEffortShouldCancel{
if(self.currentStage != SpirometryStageIsIdle){
dispatch_async(dispatch_get_main_queue(),^{
self.isShuttingDown = YES; // this function should only be called from main queue so no semaphore needed
NSLog(@"Effort cancelled");
[self.audioManager pause]; // stop
[self.dataBuffer processRemainingBlocks]; // kill remainder of queue
[self.dataBuffer clear]; // and throw it away
self.currentStage = SpirometryStageIsIdle;
// perform delegation for effort was cancelled successfully
if(delegateRespondsTo.didCancelEffort){
[self.delegate didCancelEffort];
}
});
}
}
-(void)requestThatEffortShouldEnd{
// add this for synchronization of the serial main queue (not UI related, but simple calculation so, meh)
dispatch_async(dispatch_get_main_queue(),^{
if(self.currentStage > SpirometryStageIsWaitingForTestToBegin){
NSLog(@"Requesting that effort should end without canceling, user inititated");
self.currentStage = SpirometryStageIsFinished;
[self endEffortIfDone];
}
else{
NSLog(@"Cannot end test because no valid start was found, canceling");
[self requestThatCurrentEffortShouldCancel];
}
});
}
//=============================================================================================================
#pragma mark Debug Options
-(void)activateDebugAudioModeWithWAVFile:(NSString*)filename{
self.audioDebugIsActive = YES;
if(filename != nil)
self.audioDebugFileName = filename;
else
self.audioDebugFileName = @"VortexWhistleRed";
// set it, if we can, otherwise set at time of creation
if(_audioManager){
[self.audioManager overrideMicrophoneWithAudioFile:self.audioDebugFileName];
}
}
-(void)shouldSaveSeparateEffortsToDocumentDirectory:(BOOL)should{
self.shouldSaveEffortsToDocumentDirectory = should;
if(_audioManager){
self.audioManager.shouldSaveContinuouslySampledMicrophoneAudioDataToNewFile = should;
}
}
@end