RecordingMeterGraph.m 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2014-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "RecordingMeterGraph.h"
  21. #import "RectUtil.h"
  22. #import "AudioTrackAnalyzer.h"
  23. #import "Utils.h"
  24. #import "BundleUtil.h"
  25. // 0db max output
  26. // -160db min output
  27. // -50db noise level
  28. #define DECIBEL_RANGE 50.0f
  29. #define NOISE_OFFSET -50.0f
  30. #define GRAPH_LINE_MIN_HEIGHT 2.0f
  31. #define GRAPH_LINE_WIDTH 1.5f
  32. #define TIMER_INTERVAL_RECORDER 0.5f
  33. #define Y_OFFSET
  34. #define COLOR_RECORDING [UIColor redColor]
  35. #define COLOR_PLAY_FUTURE [Colors fontLight]
  36. #define COLOR_PLAY_PAST [Colors main]
  37. #define COLOR_PLAY_CURRENT COLOR_PLAY_PAST
  38. @interface RecordingMeterGraph () <AudioTrackAnalyzerDelegate>
  39. @property AVAudioRecorder *recorder;
  40. @property AVAudioPlayer *player;
  41. @property NSTimer *recordTimer;
  42. @property NSTimer *playTimer;
  43. @property NSInteger numberOfChanels;
  44. @property float scale;
  45. @property CGFloat numberOfSamples;
  46. @property CGFloat runningXOffset;
  47. @property CGFloat noiseOffset;
  48. @property CGFloat widthPerSample;
  49. @property UIColor *graphColor;
  50. @property UIView *slider;
  51. @end
  52. @implementation RecordingMeterGraph
  53. - (void)reset {
  54. [self setup];
  55. }
  56. - (void)setup {
  57. _widthPerSample = GRAPH_LINE_WIDTH * 2.0;
  58. _runningXOffset = 0.0;
  59. _noiseOffset = NOISE_OFFSET;
  60. _scale = self.frame.size.height/(DECIBEL_RANGE);
  61. [_playTimer invalidate];
  62. [_recordTimer invalidate];
  63. for (UIView *view in [self subviews]) {
  64. [view removeFromSuperview];
  65. }
  66. self.accessibilityTraits |= UIAccessibilityTraitAdjustable;
  67. self.accessibilityLabel = [BundleUtil localizedStringForKey:@"voice message"];
  68. self.backgroundColor = [Colors background];
  69. }
  70. - (void)drawLiveRecorder:(AVAudioRecorder *)recorder {
  71. [self setup];
  72. _graphColor = COLOR_RECORDING;
  73. _recorder = recorder;
  74. _recorder.meteringEnabled = YES;
  75. NSDictionary *settings = [_recorder settings];
  76. NSNumber *number = [settings objectForKey: @"AVNumberOfChannelsKey"];
  77. _numberOfChanels = [number integerValue];
  78. [self setRecording:YES];
  79. }
  80. - (void)drawAudioTrack:(AVAudioPlayer *)player {
  81. [self setup];
  82. _player = player;
  83. _graphColor = COLOR_PLAY_FUTURE;
  84. _numberOfSamples = self.frame.size.width / _widthPerSample;
  85. AudioTrackAnalyzer *analyzer = [AudioTrackAnalyzer audioTrackAnalyzerFor:player.url];
  86. analyzer.delegate = self;
  87. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  88. [analyzer reduceAudioToDecibelLevels: _numberOfSamples];
  89. });
  90. }
  91. - (void)setPlaying:(BOOL)playing {
  92. if (playing && _player && _playTimer.valid == NO) {
  93. NSTimeInterval interval = _player.duration / (4.0 * _numberOfSamples);
  94. _playTimer = [NSTimer timerWithTimeInterval:interval target:self selector:@selector(timerFiredPlayer) userInfo:nil repeats:YES];
  95. [[NSRunLoop mainRunLoop] addTimer:_playTimer forMode:NSDefaultRunLoopMode];
  96. } else {
  97. [_playTimer invalidate];
  98. }
  99. }
  100. - (void)setRecording:(BOOL)recording {
  101. if (recording && _recordTimer.valid == NO) {
  102. _recordTimer = [NSTimer timerWithTimeInterval:TIMER_INTERVAL_RECORDER target:self selector:@selector(timerFiredRecorder) userInfo:nil repeats:YES];
  103. [[NSRunLoop mainRunLoop] addTimer:_recordTimer forMode:NSDefaultRunLoopMode];
  104. } else {
  105. [_recordTimer invalidate];
  106. }
  107. }
  108. #pragma mark - AudioTrackAnalyzerDelegate
  109. - (void)trackAnalyzerNextValue:(Float32)value {
  110. dispatch_async(dispatch_get_main_queue(), ^{
  111. [self drawNextSample: value];
  112. });
  113. }
  114. -(void)trackAnalyzerFinished {
  115. ;//nop
  116. }
  117. - (void)drawNextSample:(CGFloat)value {
  118. CGRect rect = [self rectForValue:value];
  119. UIView *view = [[UIView alloc] initWithFrame: rect];
  120. view.backgroundColor = _graphColor;
  121. [self addSubview: view];
  122. }
  123. - (void)timerFiredRecorder {
  124. if (_recorder.isRecording == NO) {
  125. [_recordTimer invalidate];
  126. return;
  127. }
  128. CGFloat decibel = [self avgForAllChanels];
  129. CGRect rect = [self rectForValue:decibel];
  130. UIView *view = [[UIView alloc] initWithFrame: rect];
  131. view.backgroundColor = _graphColor;
  132. [self addSubview: view];
  133. }
  134. - (void)timerFiredPlayer {
  135. if (_player.playing == NO) {
  136. [_playTimer invalidate];
  137. return;
  138. }
  139. [self updateGraph];
  140. }
  141. - (void)updateGraph {
  142. CGFloat currentPos = [self xAtCurrentPlayerTime];
  143. for (UIView *view in [self subviews]) {
  144. if (view == _slider) {
  145. continue;
  146. }
  147. if (CGRectGetMaxX(view.frame) < currentPos) {
  148. view.backgroundColor = COLOR_PLAY_PAST;
  149. } else if (CGRectGetMinX(view.frame) < currentPos) {
  150. view.backgroundColor = COLOR_PLAY_CURRENT;
  151. } else {
  152. view.backgroundColor = COLOR_PLAY_FUTURE;
  153. }
  154. }
  155. if (_delegate) {
  156. [_delegate didUpdatePlayerPosition];
  157. }
  158. }
  159. - (CGFloat)xAtCurrentPlayerTime {
  160. return _player.currentTime/_player.duration * self.frame.size.width;
  161. }
  162. - (CGFloat)currentPlayerTimeForX:(CGFloat)x {
  163. return x/self.frame.size.width * _player.duration;
  164. }
  165. - (CGRect)rectForValue:(CGFloat)value {
  166. CGFloat avgOffset = value - _noiseOffset;
  167. CGFloat scaledAvg = avgOffset * _scale;
  168. scaledAvg = fmaxf(GRAPH_LINE_MIN_HEIGHT, scaledAvg); // draw at least 1px
  169. scaledAvg = fminf(self.frame.size.height, scaledAvg); // clip on top edge
  170. CGFloat y = self.frame.size.height - scaledAvg;
  171. CGRect rect = CGRectMake(_runningXOffset, y, GRAPH_LINE_WIDTH, scaledAvg);
  172. if (_runningXOffset >= self.frame.size.width - GRAPH_LINE_WIDTH) {
  173. [self shiftGraphBy: _widthPerSample];
  174. } else {
  175. _runningXOffset += _widthPerSample;
  176. }
  177. return rect;
  178. }
  179. - (void)shiftGraphBy:(CGFloat)offset {
  180. for (UIView *view in [self subviews]) {
  181. view.frame = [RectUtil offsetRect:view.frame byX: -offset byY:0.0];
  182. if (view.frame.origin.x < 0.0) {
  183. [view removeFromSuperview];
  184. }
  185. }
  186. }
  187. - (CGFloat)avgForAllChanels {
  188. [_recorder updateMeters];
  189. CGFloat avgSum = 0.0;
  190. for (int i=0; i<_numberOfChanels; i++) {
  191. avgSum += [_recorder averagePowerForChannel: i];
  192. }
  193. return avgSum / (CGFloat) _numberOfChanels;
  194. }
  195. #pragma mark - Slider
  196. - (void)addSliderView:(CGPoint) point {
  197. CGRect rect = [self sliderRect:point];
  198. _slider = [[UIView alloc] initWithFrame: rect];
  199. _slider.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.3];
  200. [self addSubview: _slider];
  201. }
  202. - (CGRect)sliderRect:(CGPoint) point {
  203. CGFloat x = point.x;
  204. x = fmin(self.frame.size.width, x);
  205. x = fmax(1.0, x);
  206. CGFloat y = 0.0;
  207. CGRect rect = CGRectMake(0.0, y, x, self.frame.size.height);
  208. return rect;
  209. }
  210. - (void)updateSliderTo:(CGPoint)point {
  211. _player.currentTime = [self currentPlayerTimeForX: point.x];
  212. _slider.frame = [self sliderRect: point];
  213. }
  214. #pragma mark - touch handling for slider
  215. - (void)clearSlider {
  216. [_slider removeFromSuperview];
  217. _slider = nil;
  218. }
  219. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  220. if (_recorder.isRecording) {
  221. return;
  222. }
  223. if ([touches count] == 1 && _slider == nil) {
  224. UITouch *touch = [touches anyObject];
  225. CGPoint position = [touch locationInView: self];
  226. [self addSliderView: position];
  227. [self updateSliderTo:position];
  228. }
  229. }
  230. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  231. {
  232. if (_recorder.isRecording) {
  233. return;
  234. }
  235. UITouch *touch = [touches anyObject];
  236. CGPoint position = [touch locationInView: self];
  237. if (UIAccessibilityIsVoiceOverRunning() == NO) {
  238. // for some reason this is extremly slow when accessability is enabled
  239. [self updateSliderTo:position];
  240. [self updateGraph];
  241. }
  242. }
  243. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  244. {
  245. if (UIAccessibilityIsVoiceOverRunning()) {
  246. dispatch_async(dispatch_get_main_queue(), ^{
  247. NSString *timeString = [Utils accessibilityStringAtTime:_player.currentTime withPrefix:@"go_to_position"];
  248. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, timeString);
  249. });
  250. }
  251. [self clearSlider];
  252. }
  253. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
  254. {
  255. [self clearSlider];
  256. }
  257. #pragma mark - Accessability
  258. - (BOOL)isAccessibilityElement {
  259. return YES;
  260. }
  261. - (void)accessibilityIncrement {
  262. NSTimeInterval time = _player.currentTime;
  263. _player.currentTime = time + ((_player.duration/100)*10);
  264. [self updateGraph];
  265. NSString *timeString = [Utils accessibilityStringAtTime:_player.currentTime withPrefix:@"go_to_position"];
  266. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, timeString);
  267. }
  268. - (void)accessibilityDecrement {
  269. NSTimeInterval time = _player.currentTime;
  270. _player.currentTime = time - ((_player.duration/100)*10);
  271. [self updateGraph];
  272. NSString *timeString = [Utils accessibilityStringAtTime:_player.currentTime withPrefix:@"go_to_position"];
  273. UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, timeString);
  274. }
  275. @end