Allow to record a voice message (#1926)

This commit is contained in:
Nicolas Mauri
2023-10-23 17:47:36 +02:00
committed by GitHub
parent 65b7c1df20
commit 2f57fbc77d
62 changed files with 1805 additions and 342 deletions
@@ -0,0 +1,132 @@
//
// Copyright 2023 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 SwiftUI
struct EstimatedWaveform: Equatable, Hashable {
static let dataRange: ClosedRange<UInt16> = 0...1024
let data: [UInt16]
}
extension EstimatedWaveform {
/// Maps the `data` array to Float values in the range 0...1 respecting the `Self.dataRange` limits.
/// Up to `maxSamplesCount` will be returned in the output array
func normalisedData(maxSamplesCount: Int) -> [Float] {
guard maxSamplesCount > 0 else {
return []
}
// Filter the data to keep only the expected number of samples
let result: [UInt16]
if data.count > maxSamplesCount {
result = (0..<maxSamplesCount)
.map { index in
let targetIndex = Int((Double(index) * (Double(data.count) / Double(maxSamplesCount))).rounded())
return UInt16(data[targetIndex])
}
} else {
result = data
}
// Normalize the sample in the allowed range
return result.map { Float($0) / Float(Self.dataRange.upperBound) }
}
}
extension EstimatedWaveform {
static let mockWaveform = EstimatedWaveform(data: [0, 0, 0, 3, 3, 127, 400, 266, 126, 122, 373, 251, 45, 112,
334, 205, 99, 138, 397, 354, 125, 361, 199, 51,
294, 131, 19, 2, 3, 3, 1, 2, 0, 0,
0, 0])
}
struct EstimatedWaveformView: View {
var lineWidth: CGFloat = 2
var linePadding: CGFloat = 2
var waveform: EstimatedWaveform
private let minimumGraphAmplitude: CGFloat = 1
var progress: CGFloat = 0.0
@State private var normalizedWaveformData: [Float] = []
var body: some View {
GeometryReader { geometry in
WaveformShape(lineWidth: lineWidth,
linePadding: linePadding,
waveformData: normalizedWaveformData)
.stroke(Color.compound.iconSecondary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
.progressMask(progress: progress)
.preference(key: ViewSizeKey.self, value: geometry.size)
}
.onPreferenceChange(ViewSizeKey.self) { size in
buildNormalizedWaveformData(size: size)
}
}
private func buildNormalizedWaveformData(size: CGSize) {
let count = Int(size.width / (lineWidth + linePadding))
// Rebuild the normalized waveform data only if the count has changed
if normalizedWaveformData.count == count {
return
}
normalizedWaveformData = waveform.normalisedData(maxSamplesCount: count)
}
}
private struct ViewSizeKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
private struct WaveformShape: Shape {
let lineWidth: CGFloat
let linePadding: CGFloat
let waveformData: [Float]
var minimumGraphAmplitude: CGFloat = 1.0
func path(in rect: CGRect) -> Path {
let width = rect.size.width
let height = rect.size.height
let centerY = rect.size.height / 2
var xOffset: CGFloat = lineWidth / 2
var index = 0
var path = Path()
while xOffset <= width {
let sample = CGFloat(index >= waveformData.count ? 0 : waveformData[index])
let drawingAmplitude = max(minimumGraphAmplitude, sample * (height - 2))
path.move(to: CGPoint(x: xOffset, y: centerY - drawingAmplitude / 2))
path.addLine(to: CGPoint(x: xOffset, y: centerY + drawingAmplitude / 2))
xOffset += lineWidth + linePadding
index += 1
}
return path
}
}
struct EstimatedWaveformView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
// Wrap the WaveformView in a VStack otherwise the preview test will fail (because of Prefire / GeometryReader)
VStack {
EstimatedWaveformView(waveform: EstimatedWaveform.mockWaveform, progress: 0.5)
.frame(width: 140, height: 50)
}
}
}
@@ -0,0 +1,56 @@
//
// Copyright 2023 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 SwiftUI
extension View {
func progressMask(progress: CGFloat,
trackColor: Color = .compound.iconSecondary,
backgroundTrackColor: Color = .compound.iconQuaternary) -> some View {
modifier(ProgressMaskModifier(progress: progress,
trackColor: trackColor,
backgroundTrackColor: backgroundTrackColor))
}
}
private struct ProgressMaskModifier: ViewModifier {
private let progress: CGFloat
private let trackColor: Color
private let backgroundTrackColor: Color
init(progress: CGFloat, trackColor: Color, backgroundTrackColor: Color) {
self.progress = progress
self.trackColor = trackColor
self.backgroundTrackColor = backgroundTrackColor
}
func body(content: Content) -> some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.fill(backgroundTrackColor)
.frame(width: geometry.size.width, height: geometry.size.height)
Rectangle()
.fill(trackColor)
.frame(width: max(0.0, geometry.size.width * progress), height: geometry.size.height)
}
.mask {
content
}
}
}
}
@@ -0,0 +1,24 @@
//
// Copyright 2023 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
enum WaveformSource: Equatable {
/// File URL of the source audio file
case url(URL)
/// Array of small number of pre-computed samples
case data([Float])
}
@@ -0,0 +1,98 @@
//
// Copyright 2023 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 DSWaveformImageViews
import Foundation
import SwiftUI
struct WaveformViewDragGestureModifier: ViewModifier {
@GestureState private var dragGestureState = WaveformViewDragState.inactive
@Binding var dragState: WaveformViewDragState
let minimumDragDistance: Double
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.gesture(SpatialTapGesture()
.simultaneously(with: LongPressGesture())
.sequenced(before: DragGesture(minimumDistance: minimumDragDistance, coordinateSpace: .local))
.updating($dragGestureState) { value, state, _ in
switch value {
// (SpatialTap, LongPress) begins.
case .first(let spatialLongPress):
// Compute the progress with the spatialTap location
let progress = (spatialLongPress.first?.location ?? .zero).x / geometry.size.width
state = .pressing(progress: progress)
// Long press confirmed, dragging may begin.
case .second(let spatialLongPress, let drag) where spatialLongPress.second ?? false:
var progress: Double = dragState.progress
// Compute the progress with drag location
if let location = drag?.location {
progress = location.x / geometry.size.width
}
state = .dragging(progress: progress)
// Dragging ended or the long press cancelled.
default:
state = .inactive
}
})
}
.onChange(of: dragGestureState) { value in
dragState = value
}
}
}
extension View {
func waveformDragGesture(_ dragState: Binding<WaveformViewDragState>, minimumDragDistance: Double = 0) -> some View {
modifier(WaveformViewDragGestureModifier(dragState: dragState,
minimumDragDistance: minimumDragDistance))
}
}
enum WaveformViewDragState: Equatable {
case inactive
case pressing(progress: Double)
case dragging(progress: Double)
var progress: Double {
switch self {
case .inactive:
return .zero
case .pressing(let progress), .dragging(let progress):
return progress
}
}
var isActive: Bool {
switch self {
case .inactive:
return false
case .pressing, .dragging:
return true
}
}
var isDragging: Bool {
switch self {
case .inactive, .pressing:
return false
case .dragging:
return true
}
}
}