Skip to content

Commit

Permalink
#4090 - Add voice message controller, audio recorder and toolbar view…
Browse files Browse the repository at this point in the history
… links.
  • Loading branch information
stefanceriu committed Jun 7, 2021
1 parent ff537e8 commit 05a8144
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 9 deletions.
17 changes: 17 additions & 0 deletions Riot/Modules/Room/VoiceMessages/AudioPlayer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
79 changes: 79 additions & 0 deletions Riot/Modules/Room/VoiceMessages/AudioRecorder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import AVFoundation

protocol AudioRecorderDelegate: AnyObject {
func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder)
func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder)
func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error)
}

enum AudioRecorderError: Error {
case genericError
}

class AudioRecorder: NSObject, AVAudioRecorderDelegate {

private(set) var isRecording: Bool = false
private(set) var currentTime: TimeInterval = 0
private(set) var url: URL?

private var audioRecorder: AVAudioRecorder!

weak var delegate: AudioRecorderDelegate?

func recordWithOuputURL(_ url: URL) {

let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]

do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
audioRecorder.delegate = self
audioRecorder.record()
} catch {
delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError)
}

}

func stopRecording() {
audioRecorder.stop()
}

// MARK: - AVAudioRecorderDelegate

func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully success: Bool) {
if success {
delegate?.audioRecorderDidFinishRecording(self)
} else {
delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError)
}
}

func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
delegate?.audioRecorder(self, didFailWithError: AudioRecorderError.genericError)
}
}

extension String: LocalizedError {
public var errorDescription: String? { return self }
}
79 changes: 79 additions & 0 deletions Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

class VoiceMessageController: VoiceMessageToolbarViewDelegate, AudioRecorderDelegate {

let voiceMessageToolbarView: VoiceMessageToolbarView
var audioRecorder: AudioRecorder?

init() {
voiceMessageToolbarView = VoiceMessageToolbarView.instanceFromNib()
voiceMessageToolbarView.delegate = self
}

// MARK: - VoiceMessageToolbarViewDelegate

func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) {

let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString)

audioRecorder = AudioRecorder()
audioRecorder?.delegate = self
audioRecorder?.recordWithOuputURL(temporaryFileURL)
}

func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) {
audioRecorder?.stopRecording()
}

func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) {
audioRecorder?.stopRecording()
deleteRecordingAtURL(audioRecorder?.url)
}

// MARK: - AudioRecorderDelegate

func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder) {
voiceMessageToolbarView.state = .recording
}

func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder) {
voiceMessageToolbarView.state = .idle
}

func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error) {
MXLog.error("Failed recording voice message.")
voiceMessageToolbarView.state = .idle
}

// MARK: - Private

private func deleteRecordingAtURL(_ url: URL?) {
guard let url = url else {
MXLog.error("Invalid audio recording URL")
return
}

do {
try FileManager.default.removeItem(at: url)
} catch {
MXLog.error(error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@

import UIKit

private enum VoiceMessageToolbarViewState {
protocol VoiceMessageToolbarViewDelegate: AnyObject {
func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView)
}

enum VoiceMessageToolbarViewState {
case idle
case recording
}

class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate {

weak var delegate: VoiceMessageToolbarViewDelegate?

@IBOutlet private var backgroundView: UIView!

Expand All @@ -36,14 +44,25 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel

private var cancelLabelToRecordButtonDistance: CGFloat = 0.0

private var state: VoiceMessageToolbarViewState = .idle {
private var currentTheme: Theme? {
didSet {
updateUIAnimated(true)
}
}

private var currentTheme: Theme? {
var state: VoiceMessageToolbarViewState = .idle {
didSet {
switch state {
case .recording:
let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView)
cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX
case .idle:
gestureRecognizers?.forEach { gestureRecognizer in
gestureRecognizer.isEnabled = false
gestureRecognizer.isEnabled = true
}
}

updateUIAnimated(true)
}
}
Expand Down Expand Up @@ -90,13 +109,11 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
@objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case UIGestureRecognizer.State.began:
state = .recording

let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView)
cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX

delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self)
case UIGestureRecognizer.State.ended:
state = .idle
delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self)
case UIGestureRecognizer.State.cancelled:
delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self)
default:
break
}
Expand Down

0 comments on commit 05a8144

Please sign in to comment.