Files
letro-ios/ElementX/Sources/Other/VoiceMessage/EstimatedWaveformView.swift
manuroe c29f4cc9b4 Dual licensing: AGPL + Element Commercial (#3657)
* New LICENSE-COMMERCIAL file

* Apply dual licenses: AGPL + Element Commercial to file headers

* Update README with dual licensing
2025-01-06 11:27:37 +01:00

123 lines
4.3 KiB
Swift

//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
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
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(spacing: 0) {
EstimatedWaveformView(waveform: EstimatedWaveform.mockWaveform, progress: 0.5)
.frame(width: 140, height: 50)
}
}
}