-
-
Notifications
You must be signed in to change notification settings - Fork 114
/
WaveformView.swift
156 lines (141 loc) · 7.19 KB
/
WaveformView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import DSWaveformImage
import SwiftUI
@available(iOS 15.0, macOS 12.0, *)
/// Renders and displays a waveform for the audio at `audioURL`.
public struct WaveformView<Content: View>: View {
private let audioURL: URL
private let configuration: Waveform.Configuration
private let renderer: WaveformRenderer
private let priority: TaskPriority
private let content: (WaveformShape) -> Content
@State private var samples: [Float] = []
@State private var rescaleTimer: Timer?
@State private var currentSize: CGSize = .zero
/**
Creates a new WaveformView which displays a waveform for the audio at `audioURL`.
- Parameters:
- audioURL: The `URL` of the audio asset to be rendered.
- configuration: The `Waveform.Configuration` to be used for rendering.
- renderer: The `WaveformRenderer` implementation to be used. Defaults to `LinearWaveformRenderer`. Also comes with `CircularWaveformRenderer`.
- priority: The `TaskPriority` used during analyzing. Defaults to `.userInitiated`.
- content: ViewBuilder with the WaveformShape to be customized.
*/
public init(
audioURL: URL,
configuration: Waveform.Configuration = Waveform.Configuration(damping: .init(percentage: 0.125, sides: .both)),
renderer: WaveformRenderer = LinearWaveformRenderer(),
priority: TaskPriority = .userInitiated,
@ViewBuilder content: @escaping (WaveformShape) -> Content
) {
self.audioURL = audioURL
self.configuration = configuration
self.renderer = renderer
self.priority = priority
self.content = content
}
public var body: some View {
GeometryReader { geometry in
content(WaveformShape(samples: samples, configuration: configuration, renderer: renderer))
.onAppear {
guard samples.isEmpty else { return }
update(size: geometry.size, url: audioURL, configuration: configuration)
}
.modifier(OnChange(of: geometry.size, action: { newValue in update(size: newValue, url: audioURL, configuration: configuration, delayed: true) }))
.modifier(OnChange(of: audioURL, action: { newValue in update(size: geometry.size, url: audioURL, configuration: configuration) }))
.modifier(OnChange(of: configuration, action: { newValue in update(size: geometry.size, url: audioURL, configuration: newValue) }))
}
}
private func update(size: CGSize, url: URL, configuration: Waveform.Configuration, delayed: Bool = false) {
rescaleTimer?.invalidate()
let updateTask: @Sendable (Timer?) -> Void = { _ in
Task(priority: .userInitiated) {
do {
let samplesNeeded = Int(size.width * configuration.scale)
let samples = try await WaveformAnalyzer().samples(fromAudioAt: url, count: samplesNeeded)
await MainActor.run {
self.currentSize = size
self.samples = samples
}
} catch {
assertionFailure(error.localizedDescription)
}
}
}
if delayed {
rescaleTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false, block: updateTask)
RunLoop.main.add(rescaleTimer!, forMode: .common)
} else {
updateTask(nil)
}
}
}
public extension WaveformView {
/**
Creates a new WaveformView which displays a waveform for the audio at `audioURL`.
- Parameters:
- audioURL: The `URL` of the audio asset to be rendered.
- configuration: The `Waveform.Configuration` to be used for rendering.
- renderer: The `WaveformRenderer` implementation to be used. Defaults to `LinearWaveformRenderer`. Also comes with `CircularWaveformRenderer`.
- priority: The `TaskPriority` used during analyzing. Defaults to `.userInitiated`.
*/
init(
audioURL: URL,
configuration: Waveform.Configuration = Waveform.Configuration(damping: .init(percentage: 0.125, sides: .both)),
renderer: WaveformRenderer = LinearWaveformRenderer(),
priority: TaskPriority = .userInitiated
) where Content == AnyView {
self.init(audioURL: audioURL, configuration: configuration, renderer: renderer, priority: priority) { shape in
AnyView(DefaultShapeStyler().style(shape: shape, with: configuration))
}
}
/**
Creates a new WaveformView which displays a waveform for the audio at `audioURL`.
- Parameters:
- audioURL: The `URL` of the audio asset to be rendered.
- configuration: The `Waveform.Configuration` to be used for rendering.
- renderer: The `WaveformRenderer` implementation to be used. Defaults to `LinearWaveformRenderer`. Also comes with `CircularWaveformRenderer`.
- priority: The `TaskPriority` used during analyzing. Defaults to `.userInitiated`.
- placeholder: ViewBuilder for a placeholder view during the loading phase.
*/
init<Placeholder: View>(
audioURL: URL,
configuration: Waveform.Configuration = Waveform.Configuration(damping: .init(percentage: 0.125, sides: .both)),
renderer: WaveformRenderer = LinearWaveformRenderer(),
priority: TaskPriority = .userInitiated,
@ViewBuilder placeholder: @escaping () -> Placeholder
) where Content == _ConditionalContent<Placeholder, AnyView> {
self.init(audioURL: audioURL, configuration: configuration, renderer: renderer, priority: priority) { shape in
if shape.isEmpty {
placeholder()
} else {
AnyView(DefaultShapeStyler().style(shape: shape, with: configuration))
}
}
}
/**
Creates a new WaveformView which displays a waveform for the audio at `audioURL`.
- Parameters:
- audioURL: The `URL` of the audio asset to be rendered.
- configuration: The `Waveform.Configuration` to be used for rendering.
- renderer: The `WaveformRenderer` implementation to be used. Defaults to `LinearWaveformRenderer`. Also comes with `CircularWaveformRenderer`.
- priority: The `TaskPriority` used during analyzing. Defaults to `.userInitiated`.
- content: ViewBuilder with the WaveformShape to be customized.
- placeholder: ViewBuilder for a placeholder view during the loading phase.
*/
init<Placeholder: View, ModifiedContent: View>(
audioURL: URL,
configuration: Waveform.Configuration = Waveform.Configuration(damping: .init(percentage: 0.125, sides: .both)),
renderer: WaveformRenderer = LinearWaveformRenderer(),
priority: TaskPriority = .userInitiated,
@ViewBuilder content: @escaping (WaveformShape) -> ModifiedContent,
@ViewBuilder placeholder: @escaping () -> Placeholder
) where Content == _ConditionalContent<Placeholder, ModifiedContent> {
self.init(audioURL: audioURL, configuration: configuration, renderer: renderer, priority: priority) { shape in
if shape.isEmpty {
placeholder()
} else {
content(shape)
}
}
}
}