// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2014-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "RecordingMeterGraph.h" #import "RectUtil.h" #import "AudioTrackAnalyzer.h" #import "Utils.h" #import "BundleUtil.h" // 0db max output // -160db min output // -50db noise level #define DECIBEL_RANGE 50.0f #define NOISE_OFFSET -50.0f #define GRAPH_LINE_MIN_HEIGHT 2.0f #define GRAPH_LINE_WIDTH 1.5f #define TIMER_INTERVAL_RECORDER 0.5f #define Y_OFFSET #define COLOR_RECORDING [UIColor redColor] #define COLOR_PLAY_FUTURE [Colors fontLight] #define COLOR_PLAY_PAST [Colors main] #define COLOR_PLAY_CURRENT COLOR_PLAY_PAST @interface RecordingMeterGraph () @property AVAudioRecorder *recorder; @property AVAudioPlayer *player; @property NSTimer *recordTimer; @property NSTimer *playTimer; @property NSInteger numberOfChanels; @property float scale; @property CGFloat numberOfSamples; @property CGFloat runningXOffset; @property CGFloat noiseOffset; @property CGFloat widthPerSample; @property UIColor *graphColor; @property UIView *slider; @end @implementation RecordingMeterGraph - (void)reset { [self setup]; } - (void)setup { _widthPerSample = GRAPH_LINE_WIDTH * 2.0; _runningXOffset = 0.0; _noiseOffset = NOISE_OFFSET; _scale = self.frame.size.height/(DECIBEL_RANGE); [_playTimer invalidate]; [_recordTimer invalidate]; for (UIView *view in [self subviews]) { [view removeFromSuperview]; } self.accessibilityTraits |= UIAccessibilityTraitAdjustable; self.accessibilityLabel = [BundleUtil localizedStringForKey:@"voice message"]; self.backgroundColor = [Colors background]; } - (void)drawLiveRecorder:(AVAudioRecorder *)recorder { [self setup]; _graphColor = COLOR_RECORDING; _recorder = recorder; _recorder.meteringEnabled = YES; NSDictionary *settings = [_recorder settings]; NSNumber *number = [settings objectForKey: @"AVNumberOfChannelsKey"]; _numberOfChanels = [number integerValue]; [self setRecording:YES]; } - (void)drawAudioTrack:(AVAudioPlayer *)player { [self setup]; _player = player; _graphColor = COLOR_PLAY_FUTURE; _numberOfSamples = self.frame.size.width / _widthPerSample; AudioTrackAnalyzer *analyzer = [AudioTrackAnalyzer audioTrackAnalyzerFor:player.url]; analyzer.delegate = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [analyzer reduceAudioToDecibelLevels: _numberOfSamples]; }); } - (void)setPlaying:(BOOL)playing { if (playing && _player && _playTimer.valid == NO) { NSTimeInterval interval = _player.duration / (4.0 * _numberOfSamples); _playTimer = [NSTimer timerWithTimeInterval:interval target:self selector:@selector(timerFiredPlayer) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:_playTimer forMode:NSDefaultRunLoopMode]; } else { [_playTimer invalidate]; } } - (void)setRecording:(BOOL)recording { if (recording && _recordTimer.valid == NO) { _recordTimer = [NSTimer timerWithTimeInterval:TIMER_INTERVAL_RECORDER target:self selector:@selector(timerFiredRecorder) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:_recordTimer forMode:NSDefaultRunLoopMode]; } else { [_recordTimer invalidate]; } } #pragma mark - AudioTrackAnalyzerDelegate - (void)trackAnalyzerNextValue:(Float32)value { dispatch_async(dispatch_get_main_queue(), ^{ [self drawNextSample: value]; }); } -(void)trackAnalyzerFinished { ;//nop } - (void)drawNextSample:(CGFloat)value { CGRect rect = [self rectForValue:value]; UIView *view = [[UIView alloc] initWithFrame: rect]; view.backgroundColor = _graphColor; [self addSubview: view]; } - (void)timerFiredRecorder { if (_recorder.isRecording == NO) { [_recordTimer invalidate]; return; } CGFloat decibel = [self avgForAllChanels]; CGRect rect = [self rectForValue:decibel]; UIView *view = [[UIView alloc] initWithFrame: rect]; view.backgroundColor = _graphColor; [self addSubview: view]; } - (void)timerFiredPlayer { if (_player.playing == NO) { [_playTimer invalidate]; return; } [self updateGraph]; } - (void)updateGraph { CGFloat currentPos = [self xAtCurrentPlayerTime]; for (UIView *view in [self subviews]) { if (view == _slider) { continue; } if (CGRectGetMaxX(view.frame) < currentPos) { view.backgroundColor = COLOR_PLAY_PAST; } else if (CGRectGetMinX(view.frame) < currentPos) { view.backgroundColor = COLOR_PLAY_CURRENT; } else { view.backgroundColor = COLOR_PLAY_FUTURE; } } if (_delegate) { [_delegate didUpdatePlayerPosition]; } } - (CGFloat)xAtCurrentPlayerTime { return _player.currentTime/_player.duration * self.frame.size.width; } - (CGFloat)currentPlayerTimeForX:(CGFloat)x { return x/self.frame.size.width * _player.duration; } - (CGRect)rectForValue:(CGFloat)value { CGFloat avgOffset = value - _noiseOffset; CGFloat scaledAvg = avgOffset * _scale; scaledAvg = fmaxf(GRAPH_LINE_MIN_HEIGHT, scaledAvg); // draw at least 1px scaledAvg = fminf(self.frame.size.height, scaledAvg); // clip on top edge CGFloat y = self.frame.size.height - scaledAvg; CGRect rect = CGRectMake(_runningXOffset, y, GRAPH_LINE_WIDTH, scaledAvg); if (_runningXOffset >= self.frame.size.width - GRAPH_LINE_WIDTH) { [self shiftGraphBy: _widthPerSample]; } else { _runningXOffset += _widthPerSample; } return rect; } - (void)shiftGraphBy:(CGFloat)offset { for (UIView *view in [self subviews]) { view.frame = [RectUtil offsetRect:view.frame byX: -offset byY:0.0]; if (view.frame.origin.x < 0.0) { [view removeFromSuperview]; } } } - (CGFloat)avgForAllChanels { [_recorder updateMeters]; CGFloat avgSum = 0.0; for (int i=0; i<_numberOfChanels; i++) { avgSum += [_recorder averagePowerForChannel: i]; } return avgSum / (CGFloat) _numberOfChanels; } #pragma mark - Slider - (void)addSliderView:(CGPoint) point { CGRect rect = [self sliderRect:point]; _slider = [[UIView alloc] initWithFrame: rect]; _slider.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.3]; [self addSubview: _slider]; } - (CGRect)sliderRect:(CGPoint) point { CGFloat x = point.x; x = fmin(self.frame.size.width, x); x = fmax(1.0, x); CGFloat y = 0.0; CGRect rect = CGRectMake(0.0, y, x, self.frame.size.height); return rect; } - (void)updateSliderTo:(CGPoint)point { _player.currentTime = [self currentPlayerTimeForX: point.x]; _slider.frame = [self sliderRect: point]; } #pragma mark - touch handling for slider - (void)clearSlider { [_slider removeFromSuperview]; _slider = nil; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (_recorder.isRecording) { return; } if ([touches count] == 1 && _slider == nil) { UITouch *touch = [touches anyObject]; CGPoint position = [touch locationInView: self]; [self addSliderView: position]; [self updateSliderTo:position]; } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (_recorder.isRecording) { return; } UITouch *touch = [touches anyObject]; CGPoint position = [touch locationInView: self]; if (UIAccessibilityIsVoiceOverRunning() == NO) { // for some reason this is extremly slow when accessability is enabled [self updateSliderTo:position]; [self updateGraph]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (UIAccessibilityIsVoiceOverRunning()) { dispatch_async(dispatch_get_main_queue(), ^{ NSString *timeString = [Utils accessibilityStringAtTime:_player.currentTime withPrefix:@"go_to_position"]; UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, timeString); }); } [self clearSlider]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self clearSlider]; } #pragma mark - Accessability - (BOOL)isAccessibilityElement { return YES; } - (void)accessibilityIncrement { NSTimeInterval time = _player.currentTime; _player.currentTime = time + ((_player.duration/100)*10); [self updateGraph]; NSString *timeString = [Utils accessibilityStringAtTime:_player.currentTime withPrefix:@"go_to_position"]; UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, timeString); } - (void)accessibilityDecrement { NSTimeInterval time = _player.currentTime; _player.currentTime = time - ((_player.duration/100)*10); [self updateGraph]; NSString *timeString = [Utils accessibilityStringAtTime:_player.currentTime withPrefix:@"go_to_position"]; UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, timeString); } @end