diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index d0d01188a..4715d2ac3 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -571,6 +571,7 @@ C31D1DDD25217014005D4DA8 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DDC25217014005D4DA8 /* UserCell.swift */; }; C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; }; C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; }; + C31F8117252546F200DD9FD9 /* file_example_MP3_2MG.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C31F8116252546F200DD9FD9 /* file_example_MP3_2MG.mp3 */; }; C329FEEC24F7277900B1C64C /* LightModeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEEB24F7277900B1C64C /* LightModeSheet.swift */; }; C329FEEF24F7743F00B1C64C /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */; }; C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; }; @@ -1370,6 +1371,7 @@ C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = ""; }; C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = ""; }; C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = ""; }; + C31F8116252546F200DD9FD9 /* file_example_MP3_2MG.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = file_example_MP3_2MG.mp3; sourceTree = ""; }; C329FEEB24F7277900B1C64C /* LightModeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightModeSheet.swift; sourceTree = ""; }; C329FEED24F7742E00B1C64C /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Utilities.swift"; sourceTree = ""; }; C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = ""; }; @@ -2870,6 +2872,7 @@ C35E8AA42485C83B00ACB629 /* CSV */, 34330A581E7875FB00DF2FB9 /* Fonts */, B633C4FD1A1D190B0059AC12 /* Images */, + C31F8116252546F200DD9FD9 /* file_example_MP3_2MG.mp3 */, B66DBF4919D5BBC8006EA940 /* Images.xcassets */, B67EBF5C19194AC60084CCFD /* Settings.bundle */, B657DDC91911A40500F45B0C /* Signal.entitlements */, @@ -3263,6 +3266,7 @@ AD83FF411A73426500B5C81A /* audio_play_button_blue@2x.png in Resources */, 34C3C78D20409F320000134C /* Opening.m4r in Resources */, FC5CDF3A1A3393DD00B47253 /* warning_white@2x.png in Resources */, + C31F8117252546F200DD9FD9 /* file_example_MP3_2MG.mp3 in Resources */, B633C58D1A1D190B0059AC12 /* contact_default_feed.png in Resources */, B10C9B621A7049EC00ECA2BF /* play_icon@2x.png in Resources */, B633C5861A1D190B0059AC12 /* call@2x.png in Resources */, diff --git a/Signal/file_example_MP3_2MG.mp3 b/Signal/file_example_MP3_2MG.mp3 new file mode 100644 index 000000000..80cbe53e3 Binary files /dev/null and b/Signal/file_example_MP3_2MG.mp3 differ diff --git a/Signal/src/Loki/Components/VoiceMessageView2.swift b/Signal/src/Loki/Components/VoiceMessageView2.swift index 63daafabd..257688225 100644 --- a/Signal/src/Loki/Components/VoiceMessageView2.swift +++ b/Signal/src/Loki/Components/VoiceMessageView2.swift @@ -2,17 +2,16 @@ import Accelerate @objc(LKVoiceMessageView2) final class VoiceMessageView2 : UIView { - private let audioFileURL: URL - private let player: AVAudioPlayer - private var duration: Double = 1 + private let voiceMessage: TSAttachment private var isAnimating = false - private var volumeSamples: [Float] = [] { didSet { updateShapeLayer() } } + private var volumeSamples: [Float] = [] { didSet { updateShapeLayers() } } + private var progress: CGFloat = 0 + private var duration: CGFloat = 1 // Not initialized at 0 to avoid division by zero // MARK: Components private lazy var loader: UIView = { let result = UIView() result.backgroundColor = Colors.text.withAlphaComponent(0.2) - result.layer.cornerRadius = Values.messageBubbleCornerRadius return result }() @@ -29,34 +28,41 @@ final class VoiceMessageView2 : UIView { }() // MARK: Settings - private let margin = Values.smallSpacing + private let margin: CGFloat = 4 private let sampleSpacing: CGFloat = 1 // MARK: Initialization - init(audioFileURL: URL) { - self.audioFileURL = audioFileURL - player = try! AVAudioPlayer(contentsOf: audioFileURL) + @objc(initWithVoiceMessage:) + init(voiceMessage: TSAttachment) { + self.voiceMessage = voiceMessage super.init(frame: CGRect.zero) initialize() } override init(frame: CGRect) { - preconditionFailure("Use init(audioFileURL:) instead.") + preconditionFailure("Use init(voiceMessage:associatedWith:) instead.") } required init?(coder: NSCoder) { - preconditionFailure("Use init(audioFileURL:) instead.") + preconditionFailure("Use init(voiceMessage:associatedWith:) instead.") } private func initialize() { setUpViewHierarchy() - AudioUtilities.getVolumeSamples(for: audioFileURL).done(on: DispatchQueue.main) { [weak self] duration, volumeSamples in - guard let self = self else { return } - self.duration = duration - self.volumeSamples = volumeSamples - self.stopAnimating() - }.catch(on: DispatchQueue.main) { error in - print("[Loki] Couldn't sample audio file due to error: \(error).") + if voiceMessage.isDownloaded { + loader.alpha = 0 + guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else { + return print("[Loki] Couldn't get URL for voice message.") + } + AudioUtilities.getVolumeSamples(for: url).done(on: DispatchQueue.main) { [weak self] volumeSamples in + guard let self = self else { return } + self.volumeSamples = volumeSamples + self.stopAnimating() + }.catch(on: DispatchQueue.main) { error in + print("[Loki] Couldn't sample audio file due to error: \(error).") + } + } else { + showLoader() } } @@ -65,16 +71,11 @@ final class VoiceMessageView2 : UIView { set(.height, to: 40) addSubview(loader) loader.pin(to: self) - backgroundColor = Colors.sentMessageBackground - layer.cornerRadius = Values.messageBubbleCornerRadius layer.insertSublayer(backgroundShapeLayer, at: 0) layer.insertSublayer(foregroundShapeLayer, at: 1) - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(togglePlayback)) - addGestureRecognizer(tapGestureRecognizer) - showLoader() } - // MARK: User Interface + // MARK: UI & Updating private func showLoader() { isAnimating = true loader.alpha = 1 @@ -98,10 +99,17 @@ final class VoiceMessageView2 : UIView { override func layoutSubviews() { super.layoutSubviews() - updateShapeLayer() + updateShapeLayers() } - private func updateShapeLayer() { + @objc(updateForProgress:duration:) + func update(for progress: CGFloat, duration: CGFloat) { + self.progress = progress + self.duration = duration + updateShapeLayers() + } + + private func updateShapeLayers() { guard !volumeSamples.isEmpty else { return } let max = CGFloat(volumeSamples.max()!) let min = CGFloat(volumeSamples.min()!) @@ -117,20 +125,11 @@ final class VoiceMessageView2 : UIView { let y = margin + (h - sH) / 2 let subPath = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: sW, height: sH), cornerRadius: sW / 2) backgroundPath.append(subPath) - if player.currentTime / duration > Double(i) / Double(volumeSamples.count) { foregroundPath.append(subPath) } + if progress / duration > CGFloat(i) / CGFloat(volumeSamples.count) { foregroundPath.append(subPath) } } backgroundPath.close() foregroundPath.close() backgroundShapeLayer.path = backgroundPath.cgPath foregroundShapeLayer.path = foregroundPath.cgPath } - - @objc private func togglePlayback() { - player.play() - Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in - guard let self = self else { return timer.invalidate() } - self.updateShapeLayer() - if !self.player.isPlaying { timer.invalidate() } - } - } } diff --git a/Signal/src/Loki/Utilities/AudioUtilities.swift b/Signal/src/Loki/Utilities/AudioUtilities.swift index 5e32ce29e..b009b1a1c 100644 --- a/Signal/src/Loki/Utilities/AudioUtilities.swift +++ b/Signal/src/Loki/Utilities/AudioUtilities.swift @@ -27,7 +27,7 @@ enum AudioUtilities { } } - static func getVolumeSamples(for audioFileURL: URL, targetSampleCount: Int = 32) -> Promise<(duration: Double, volumeSamples: [Float])> { + static func getVolumeSamples(for audioFileURL: URL, targetSampleCount: Int = 32) -> Promise<[Float]> { return loadFile(audioFileURL).then { fileInfo in AudioUtilities.parseSamples(from: fileInfo, with: targetSampleCount) } @@ -59,7 +59,7 @@ enum AudioUtilities { return promise } - private static func parseSamples(from fileInfo: FileInfo, with targetSampleCount: Int) -> Promise<(duration: Double, volumeSamples: [Float])> { + private static func parseSamples(from fileInfo: FileInfo, with targetSampleCount: Int) -> Promise<[Float]> { // Prepare the reader guard let reader = try? AVAssetReader(asset: fileInfo.asset) else { return Promise(error: Error.parsingFailed) } let range = 0..