diff --git a/submodules/TelegramUI/TelegramUI/AnimatedStickerNode.swift b/submodules/TelegramUI/TelegramUI/AnimatedStickerNode.swift index db46c3acbc..9c47e568ee 100644 --- a/submodules/TelegramUI/TelegramUI/AnimatedStickerNode.swift +++ b/submodules/TelegramUI/TelegramUI/AnimatedStickerNode.swift @@ -83,20 +83,23 @@ private final class AnimatedStickerFrame { let width: Int let height: Int let bytesPerRow: Int + let index: Int let isLastFrame: Bool - init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, isLastFrame: Bool) { + init(data: Data, type: AnimationRendererFrameType, width: Int, height: Int, bytesPerRow: Int, index: Int, isLastFrame: Bool) { self.data = data self.type = type self.width = width self.height = height self.bytesPerRow = bytesPerRow + self.index = index self.isLastFrame = isLastFrame } } private protocol AnimatedStickerFrameSource: class { var frameRate: Int { get } + var frameCount: Int { get } func takeFrame() -> AnimatedStickerFrame } @@ -110,6 +113,8 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource let bytesPerRow: Int let height: Int let frameRate: Int + let frameCount: Int + private var frameIndex: Int private let initialOffset: Int private var offset: Int var decodeBuffer: Data @@ -125,21 +130,26 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource var height = 0 var bytesPerRow = 0 var frameRate = 0 + var frameCount = 0 if !self.data.withUnsafeBytes({ (bytes: UnsafePointer) -> Bool in var frameRateValue: Int32 = 0 - memcpy(&frameRateValue, bytes.advanced(by: offset), 4) - frameRate = Int(frameRateValue) - offset += 4 + var frameCountValue: Int32 = 0 var widthValue: Int32 = 0 var heightValue: Int32 = 0 var bytesPerRowValue: Int32 = 0 + memcpy(&frameRateValue, bytes.advanced(by: offset), 4) + offset += 4 + memcpy(&frameCountValue, bytes.advanced(by: offset), 4) + offset += 4 memcpy(&widthValue, bytes.advanced(by: offset), 4) offset += 4 memcpy(&heightValue, bytes.advanced(by: offset), 4) offset += 4 memcpy(&bytesPerRowValue, bytes.advanced(by: offset), 4) offset += 4 + frameRate = Int(frameRateValue) + frameCount = Int(frameCountValue) width = Int(widthValue) height = Int(heightValue) bytesPerRow = Int(bytesPerRowValue) @@ -154,7 +164,9 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource self.width = width self.height = height self.frameRate = frameRate + self.frameCount = frameCount + self.frameIndex = 0 self.initialOffset = offset self.offset = offset @@ -178,6 +190,8 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource let decodeBufferLength = self.decodeBuffer.count let frameBufferLength = self.frameBuffer.count + let frameIndex = self.frameIndex + self.data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in var frameLength: Int32 = 0 memcpy(&frameLength, bytes.advanced(by: self.offset), 4) @@ -208,9 +222,11 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource } } + self.frameIndex += 1 self.offset += Int(frameLength) if self.offset == dataLength { isLastFrame = true + self.frameIndex = 0 self.offset = self.initialOffset self.frameBuffer.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in memset(bytes, 0, frameBufferLength) @@ -218,7 +234,7 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource } } - return AnimatedStickerFrame(data: frameData!, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, isLastFrame: isLastFrame) + return AnimatedStickerFrame(data: frameData!, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame) } } @@ -228,7 +244,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource private let width: Int private let height: Int private let bytesPerRow: Int - private let frameCount: Int + let frameCount: Int let frameRate: Int private var currentFrame: Int private let animation: LottieInstance @@ -263,7 +279,7 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource memset(bytes, 0, self.bytesPerRow * self.height) self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow)) } - return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, isLastFrame: frameIndex == self.frameCount) + return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount) } } @@ -298,6 +314,18 @@ private final class AnimatedStickerFrameQueue { } } +public struct AnimatedStickerStatus: Equatable { + public let playing: Bool + public let duration: Double + public let timestamp: Double + + public init(playing: Bool, duration: Double, timestamp: Double) { + self.playing = playing + self.duration = duration + self.timestamp = timestamp + } +} + final class AnimatedStickerNode: ASDisplayNode { private let queue: Queue private var account: Account? @@ -319,6 +347,11 @@ final class AnimatedStickerNode: ASDisplayNode { private var isPlaying: Bool = false private var playbackMode: AnimatedStickerPlaybackMode = .loop + private let playbackStatus = Promise() + public var status: Signal { + return self.playbackStatus.get() + } + var visibility = false { didSet { if self.visibility != oldValue { @@ -440,7 +473,10 @@ final class AnimatedStickerNode: ASDisplayNode { }) timerHolder.swap(nil)?.invalidate() - let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameSource.frameRate), repeat: true, completion: { + let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0 + let frameRate = frameSource.frameRate + + let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: true, completion: { let maybeFrame = frameQueue.syncWith { frameQueue in return frameQueue.take() } @@ -449,6 +485,7 @@ final class AnimatedStickerNode: ASDisplayNode { guard let strongSelf = self else { return } + strongSelf.renderer?.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, completion: { guard let strongSelf = self else { return @@ -458,10 +495,14 @@ final class AnimatedStickerNode: ASDisplayNode { strongSelf.started() } }) + if case .once = strongSelf.playbackMode, frame.isLastFrame { strongSelf.stop() strongSelf.isPlaying = false } + + let timestamp: Double = frameRate > 0 ? Double(frame.index) / Double(frameRate) : 0 + strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: strongSelf.isPlaying, duration: duration, timestamp: timestamp))) } } frameQueue.with { frameQueue in @@ -502,6 +543,8 @@ final class AnimatedStickerNode: ASDisplayNode { }) timerHolder.swap(nil)?.invalidate() + let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0 + let maybeFrame = frameQueue.syncWith { frameQueue in return frameQueue.take() } @@ -510,6 +553,7 @@ final class AnimatedStickerNode: ASDisplayNode { guard let strongSelf = self else { return } + strongSelf.renderer?.render(queue: strongSelf.queue, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, completion: { guard let strongSelf = self else { return @@ -519,10 +563,8 @@ final class AnimatedStickerNode: ASDisplayNode { strongSelf.started() } }) - if case .once = strongSelf.playbackMode, frame.isLastFrame { - strongSelf.stop() - strongSelf.isPlaying = false - } + + strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0))) } } frameQueue.with { frameQueue in diff --git a/submodules/TelegramUI/TelegramUI/AnimatedStickerUtils.swift b/submodules/TelegramUI/TelegramUI/AnimatedStickerUtils.swift index bb317b7bf1..1d15924e11 100644 --- a/submodules/TelegramUI/TelegramUI/AnimatedStickerUtils.swift +++ b/submodules/TelegramUI/TelegramUI/AnimatedStickerUtils.swift @@ -196,7 +196,9 @@ func experimentalConvertCompressedLottieToCombinedMp4(data: Data, size: CGSize, var currentFrame: Int32 = 0 var fps: Int32 = player.frameRate + var frameCount: Int32 = player.frameCount let _ = fileContext.write(&fps, count: 4) + let _ = fileContext.write(&frameCount, count: 4) var widthValue: Int32 = Int32(size.width) var heightValue: Int32 = Int32(size.height) var bytesPerRowValue: Int32 = Int32(bytesPerRow) diff --git a/submodules/TelegramUI/TelegramUI/Bridge Audio/TGBridgeAudioEncoder.m b/submodules/TelegramUI/TelegramUI/Bridge Audio/TGBridgeAudioEncoder.m index cfec210545..a2518f2d52 100644 --- a/submodules/TelegramUI/TelegramUI/Bridge Audio/TGBridgeAudioEncoder.m +++ b/submodules/TelegramUI/TelegramUI/Bridge Audio/TGBridgeAudioEncoder.m @@ -178,7 +178,7 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100 if (_assetReader.status == AVAssetReaderStatusCompleted) { NSLog(@"finished"); - if (_oggWriter != nil && [_oggWriter writeFrame:NULL frameByteCount:0]) + if (_oggWriter != nil) { dataItemResult = _tempFileItem; durationResult = [_oggWriter encodedDuration]; diff --git a/submodules/TelegramUI/TelegramUI/CachedResourceRepresentations.swift b/submodules/TelegramUI/TelegramUI/CachedResourceRepresentations.swift index e962023208..fb7229d802 100644 --- a/submodules/TelegramUI/TelegramUI/CachedResourceRepresentations.swift +++ b/submodules/TelegramUI/TelegramUI/CachedResourceRepresentations.swift @@ -271,7 +271,7 @@ final class CachedAnimatedStickerRepresentation: CachedMediaResourceRepresentati let height: Int32 var uniqueId: String { - return "animated-sticker-\(self.width)x\(self.height)-v7" + return "animated-sticker-\(self.width)x\(self.height)-v8" } init(width: Int32, height: Int32) { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index 58383f8a50..e3c591ed09 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -14,6 +14,92 @@ private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont +private class ChatMessageHeartbeatHaptic { + private var hapticFeedback = HapticFeedback() + var timer: SwiftSignalKit.Timer? + var time: Double = 0 + var enabled = false { + didSet { + if !self.enabled { + self.reset() + } + } + } + + var active: Bool { + return self.timer != nil + } + + private func reset() { + if let timer = self.timer { + self.time = 0.0 + timer.invalidate() + self.timer = nil + } + } + + private func beat(time: Double) { + let epsilon = 0.1 + if fabs(0.0 - time) < epsilon || fabs(1.0 - time) < epsilon || fabs(2.0 - time) < epsilon { + self.hapticFeedback.impact(.medium) + } else if fabs(0.2 - time) < epsilon || fabs(1.2 - time) < epsilon || fabs(2.2 - time) < epsilon { + self.hapticFeedback.impact(.light) + } + } + + func start(time: Double) { + self.hapticFeedback.prepareImpact() + + if time > 2.0 { + return + } + + var startTime: Double = 0.0 + var delay: Double = 0.0 + + if time > 0.0 { + if time <= 1.0 { + startTime = 1.0 + } else if time <= 2.0 { + startTime = 2.0 + } + } + + delay = max(0.0, startTime - time) + + let block = { [weak self] in + guard let strongSelf = self, strongSelf.enabled else { + return + } + + strongSelf.time = startTime + strongSelf.beat(time: startTime) + strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in + guard let strongSelf = self, strongSelf.enabled else { + return + } + strongSelf.time += 0.2 + strongSelf.beat(time: strongSelf.time) + + if strongSelf.time > 2.2 { + strongSelf.reset() + strongSelf.time = 0.0 + strongSelf.timer?.invalidate() + strongSelf.timer = nil + } + + }, queue: Queue.mainQueue()) + strongSelf.timer?.start() + } + + if delay > 0.0 { + Queue.mainQueue().after(delay, block) + } else { + block() + } + } +} + class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let imageNode: TransformImageNode private let animationNode: AnimatedStickerNode @@ -40,7 +126,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private var highlightedState: Bool = false - private var hapticFeedback: HapticFeedback? + private var heartbeatHaptic: ChatMessageHeartbeatHaptic? private var currentSwipeToReplyTranslation: CGFloat = 0.0 @@ -125,6 +211,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { didSet { if self.visibilityStatus != oldValue { self.updateVisibility() + self.heartbeatHaptic?.enabled = self.visibilityStatus } } } @@ -684,33 +771,34 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if self.telegramFile != nil { let _ = item.controllerInteraction.openMessage(item.message, .default) } else if let _ = self.emojiFile { + var startTime: Signal if self.animationNode.playIfNeeded() { - if self.item?.message.text == "❤️" { - let hapticFeedback: HapticFeedback - if let currentHapticFeedback = self.hapticFeedback { - hapticFeedback = currentHapticFeedback + startTime = .single(0.0) + } else { + startTime = self.animationNode.status + |> map { $0.timestamp } + |> take(1) + |> deliverOnMainQueue + } + + if self.item?.message.text == "❤️" { + let _ = startTime.start(next: { [weak self] time in + guard let strongSelf = self else { + return + } + + let heartbeatHaptic: ChatMessageHeartbeatHaptic + if let current = strongSelf.heartbeatHaptic { + heartbeatHaptic = current } else { - hapticFeedback = HapticFeedback() - self.hapticFeedback = hapticFeedback + heartbeatHaptic = ChatMessageHeartbeatHaptic() + heartbeatHaptic.enabled = true + strongSelf.heartbeatHaptic = heartbeatHaptic } - hapticFeedback.prepareImpact() - hapticFeedback.impact(.medium) - Queue.mainQueue().after(0.2) { - hapticFeedback.impact(.light) - Queue.mainQueue().after(0.78) { - hapticFeedback.impact(.medium) - Queue.mainQueue().after(0.2) { - hapticFeedback.impact(.light) - Queue.mainQueue().after(0.78) { - hapticFeedback.impact(.medium) - Queue.mainQueue().after(0.2) { - hapticFeedback.impact(.light) - } - } - } - } + if !heartbeatHaptic.active { + heartbeatHaptic.start(time: time) } - } + }) } } return diff --git a/submodules/TelegramUI/TelegramUI/ManagedAudioRecorder.swift b/submodules/TelegramUI/TelegramUI/ManagedAudioRecorder.swift index 45d02f89f6..120417fc4d 100644 --- a/submodules/TelegramUI/TelegramUI/ManagedAudioRecorder.swift +++ b/submodules/TelegramUI/TelegramUI/ManagedAudioRecorder.swift @@ -557,7 +557,7 @@ final class ManagedAudioRecorderContext { self.currentPeak = max(Int64(sample), self.currentPeak) self.currentPeakCount += 1 if self.currentPeakCount == self.peakCompressionFactor { - var compressedPeak = self.currentPeak//Int16(Float(self.currentPeak) / Float(self.peakCompressionFactor)) + var compressedPeak = self.currentPeak withUnsafeBytes(of: &compressedPeak, { buffer in self.compressedWaveformSamples.append(buffer.bindMemory(to: UInt8.self)) }) @@ -592,57 +592,53 @@ final class ManagedAudioRecorderContext { } func takeData() -> RecordedAudioData? { - if self.oggWriter.writeFrame(nil, frameByteCount: 0) { - var scaledSamplesMemory = malloc(100 * 2)! - var scaledSamples: UnsafeMutablePointer = scaledSamplesMemory.assumingMemoryBound(to: Int16.self) - defer { - free(scaledSamplesMemory) - } - memset(scaledSamples, 0, 100 * 2); - var waveform: Data? - - let count = self.compressedWaveformSamples.count / 2 - self.compressedWaveformSamples.withUnsafeMutableBytes { (samples: UnsafeMutablePointer) -> Void in - for i in 0 ..< count { - let sample = samples[i] - let index = i * 100 / count - if (scaledSamples[index] < sample) { - scaledSamples[index] = sample; - } - } - - var peak: Int16 = 0 - var sumSamples: Int64 = 0 - for i in 0 ..< 100 { - let sample = scaledSamples[i] - if peak < sample { - peak = sample - } - sumSamples += Int64(sample) - } - var calculatedPeak: UInt16 = 0 - calculatedPeak = UInt16((Double(sumSamples) * 1.8 / 100.0)) - - if calculatedPeak < 2500 { - calculatedPeak = 2500 - } - - for i in 0 ..< 100 { - let sample: UInt16 = UInt16(Int64(scaledSamples[i])) - let minPeak = min(Int64(sample), Int64(calculatedPeak)) - let resultPeak = minPeak * 31 / Int64(calculatedPeak) - scaledSamples[i] = Int16(clamping: min(31, resultPeak)) - } - - let resultWaveform = AudioWaveform(samples: Data(bytes: scaledSamplesMemory, count: 100 * 2), peak: 31) - let bitstream = resultWaveform.makeBitstream() - waveform = AudioWaveform(bitstream: bitstream, bitsPerSample: 5).makeBitstream() - } - - return RecordedAudioData(compressedData: self.dataItem.data(), duration: self.oggWriter.encodedDuration(), waveform: waveform) - } else { - return nil + var scaledSamplesMemory = malloc(100 * 2)! + var scaledSamples: UnsafeMutablePointer = scaledSamplesMemory.assumingMemoryBound(to: Int16.self) + defer { + free(scaledSamplesMemory) } + memset(scaledSamples, 0, 100 * 2); + var waveform: Data? + + let count = self.compressedWaveformSamples.count / 2 + self.compressedWaveformSamples.withUnsafeMutableBytes { (samples: UnsafeMutablePointer) -> Void in + for i in 0 ..< count { + let sample = samples[i] + let index = i * 100 / count + if (scaledSamples[index] < sample) { + scaledSamples[index] = sample; + } + } + + var peak: Int16 = 0 + var sumSamples: Int64 = 0 + for i in 0 ..< 100 { + let sample = scaledSamples[i] + if peak < sample { + peak = sample + } + sumSamples += Int64(sample) + } + var calculatedPeak: UInt16 = 0 + calculatedPeak = UInt16((Double(sumSamples) * 1.8 / 100.0)) + + if calculatedPeak < 2500 { + calculatedPeak = 2500 + } + + for i in 0 ..< 100 { + let sample: UInt16 = UInt16(Int64(scaledSamples[i])) + let minPeak = min(Int64(sample), Int64(calculatedPeak)) + let resultPeak = minPeak * 31 / Int64(calculatedPeak) + scaledSamples[i] = Int16(clamping: min(31, resultPeak)) + } + + let resultWaveform = AudioWaveform(samples: Data(bytes: scaledSamplesMemory, count: 100 * 2), peak: 31) + let bitstream = resultWaveform.makeBitstream() + waveform = AudioWaveform(bitstream: bitstream, bitsPerSample: 5).makeBitstream() + } + + return RecordedAudioData(compressedData: self.dataItem.data(), duration: self.oggWriter.encodedDuration(), waveform: waveform) } }