-
-
Notifications
You must be signed in to change notification settings - Fork 110
/
WaveformImageTypes.swift
253 lines (210 loc) · 10.5 KB
/
WaveformImageTypes.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import AVFoundation
#if os(macOS)
import AppKit
public typealias DSColor = NSColor
public typealias DSImage = NSImage
public enum DSScreen {
public static var scale: CGFloat { NSScreen.main?.backingScaleFactor ?? 1 }
}
#else
import UIKit
public typealias DSColor = UIColor
public typealias DSImage = UIImage
public enum DSScreen {
public static var scale: CGFloat {
#if swift(>=5.9) && os(visionOS)
return (UIApplication.shared.connectedScenes.first(where: {$0 is UIWindowScene}) as? UIWindowScene)?.traitCollection.displayScale ?? 1
#else
return UIScreen.main.scale
#endif
}
}
#endif
/**
Renders the waveformsamples on the provided `CGContext`.
Default implementations are `LinearWaveformRenderer` and `CircularWaveformRenderer`.
Check out those if you'd like to implement your own custom renderer.
*/
public protocol WaveformRenderer: Sendable {
/**
Calculates a CGPath from the waveform samples.
- Parameters:
- samples: `[Float]` of the amplitude envelope to be drawn, normalized to interval `(0...1)`. `0` is maximum (typically `0dB`).
`1` is the noise floor, typically `-50dB`, as defined in `WaveformAnalyzer.noiseFloorDecibelCutoff`.
- lastOffset: You can typtically leave this `0`. **Required for live rendering**, where it is needed to keep track of the last drawing cycle. Setting it avoids 'flickering' as samples are being added
continuously and the waveform moves across the view.
*/
func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position) -> CGPath
/**
Renders the waveform samples on the provided `CGContext`.
- Parameters:
- samples: `[Float]` of the amplitude envelope to be drawn, normalized to interval `(0...1)`. `0` is maximum (typically `0dB`).
`1` is the noise floor, typically `-50dB`, as defined in `WaveformAnalyzer.noiseFloorDecibelCutoff`.
- with configuration: The desired configuration to be used for drawing.
- lastOffset: You can typtically leave this `0`. **Required for live rendering**, where it is needed to keep track of the last drawing cycle. Setting it avoids 'flickering' as samples are being added
continuously and the waveform moves across the view.
*/
func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position)
}
public extension WaveformRenderer {
func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) -> CGPath {
path(samples: samples, with: configuration, lastOffset: lastOffset, position: position)
}
func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) {
render(samples: samples, on: context, with: configuration, lastOffset: lastOffset, position: position)
}
}
public enum Waveform {
/** Position of the drawn waveform. */
public enum Position: Equatable {
/// **top**: Draws the waveform at the top of the image, such that only the bottom 50% are visible.
case top
/// **middle**: Draws the waveform in the middle the image, such that the entire waveform is visible.
case middle
/// **bottom**: Draws the waveform at the bottom of the image, such that only the top 50% are visible.
case bottom
/// **custom**: Draws the waveform at the specified point of the image. Clamped within range `(0...1)`. Where `0`
/// is equal to `.top`, `0.5` is equal to `.middle` and `1` is equal to `.bottom`.
case custom(CGFloat)
func offset() -> CGFloat {
switch self {
case .top: return 0.0
case .middle: return 0.5
case .bottom: return 1.0
case let .custom(offset): return min(1, max(0, offset))
}
}
}
/**
Style of the waveform which is used during drawing:
- **filled**: Use solid color for the waveform.
- **outlined**: Draws the envelope as an outline with the provided thickness.
- **gradient**: Use gradient based on color for the waveform.
- **gradientOutlined**: Use gradient based on color for the waveform. Draws the envelope as an outline with the provided thickness.
- **striped**: Use striped filling based on color for the waveform.
*/
public enum Style: Equatable, Sendable {
public struct StripeConfig: Equatable, Sendable {
/// Color of the waveform stripes. Default is clear.
public let color: DSColor
/// Width of stripes drawn. Default is `1`
public let width: CGFloat
/// Space between stripes. Default is `5`
public let spacing: CGFloat
/// Line cap style. Default is `.round`.
public let lineCap: CGLineCap
public init(color: DSColor, width: CGFloat = 1, spacing: CGFloat = 5, lineCap: CGLineCap = .round) {
self.color = color
self.width = width
self.spacing = spacing
self.lineCap = lineCap
}
}
case filled(DSColor)
case outlined(DSColor, CGFloat)
case gradient([DSColor])
case gradientOutlined([DSColor], CGFloat)
case striped(StripeConfig)
}
/**
Defines the damping attributes of the waveform.
*/
public struct Damping: Equatable, Sendable {
public enum Sides: Equatable, Sendable {
case left
case right
case both
}
/// Determines the percentage of the resulting graph to be damped.
///
/// Must be within `(0..<0.5)` to leave an undapmened area.
/// Default is `0.125`
public let percentage: Float
/// Determines which sides of the graph to damp.
/// Default is `.both`
public let sides: Sides
/// Easing function to be used. Default is `pow(x, 2)`.
public let easing: @Sendable (Float) -> Float
public init(percentage: Float = 0.125, sides: Sides = .both, easing: @escaping @Sendable (Float) -> Float = { x in pow(x, 2) }) {
guard (0...0.5).contains(percentage) else {
preconditionFailure("dampingPercentage must be within (0..<0.5)")
}
self.percentage = percentage
self.sides = sides
self.easing = easing
}
/// Build a new `Waveform.Damping` with only the given parameters replaced.
public func with(percentage: Float? = nil, sides: Sides? = nil, easing: (@Sendable (Float) -> Float)? = nil) -> Damping {
.init(percentage: percentage ?? self.percentage, sides: sides ?? self.sides, easing: easing ?? self.easing)
}
public static func == (lhs: Waveform.Damping, rhs: Waveform.Damping) -> Bool {
// poor-man's way to make two closures Equatable w/o too much hassle
let randomEqualitySample = Float.random(in: (0..<Float.greatestFiniteMagnitude))
return lhs.percentage == rhs.percentage && lhs.sides == rhs.sides && lhs.easing(randomEqualitySample) == rhs.easing(randomEqualitySample)
}
}
/// Allows customization of the waveform output image.
public struct Configuration: Equatable, Sendable {
/// Desired output size of the waveform image, works together with scale. Default is `.zero`.
public let size: CGSize
/// Background color of the waveform, defaults to `clear`.
public let backgroundColor: DSColor
/// Waveform drawing style, defaults to `.gradient`.
public let style: Style
/// *Optional* Waveform damping, defaults to `nil`.
public let damping: Damping?
/// Scale (@2x, @3x, etc.) to be applied to the image, defaults to `UIScreen.main.scale`.
public let scale: CGFloat
/**
Vertical scaling factor. Default is `0.95`, leaving a small vertical padding.
The `verticalScalingFactor` describes the maximum vertical amplitude
of the envelope being drawn in relation to its view's (image's) size.
* `0`: the waveform has no vertical amplitude and is just a line.
* `1`: the waveform uses the full available vertical space.
* `> 1`: louder waveform samples will extend out of the view boundaries and clip.
*/
public let verticalScalingFactor: CGFloat
/// Waveform antialiasing. If enabled, may reduce overall opacity. Default is `false`.
public let shouldAntialias: Bool
public var shouldDamp: Bool {
damping != nil
}
public init(size: CGSize = .zero,
backgroundColor: DSColor = DSColor.clear,
style: Style = .gradient([DSColor.black, DSColor.gray]),
damping: Damping? = nil,
scale: CGFloat = DSScreen.scale,
verticalScalingFactor: CGFloat = 0.95,
shouldAntialias: Bool = false) {
guard verticalScalingFactor > 0 else {
preconditionFailure("verticalScalingFactor must be greater 0")
}
self.backgroundColor = backgroundColor
self.style = style
self.damping = damping
self.size = size
self.scale = scale
self.verticalScalingFactor = verticalScalingFactor
self.shouldAntialias = shouldAntialias
}
/// Build a new `Waveform.Configuration` with only the given parameters replaced.
public func with(size: CGSize? = nil,
backgroundColor: DSColor? = nil,
style: Style? = nil,
damping: Damping? = nil,
scale: CGFloat? = nil,
verticalScalingFactor: CGFloat? = nil,
shouldAntialias: Bool? = nil
) -> Configuration {
Configuration(
size: size ?? self.size,
backgroundColor: backgroundColor ?? self.backgroundColor,
style: style ?? self.style,
damping: damping ?? self.damping,
scale: scale ?? self.scale,
verticalScalingFactor: verticalScalingFactor ?? self.verticalScalingFactor,
shouldAntialias: shouldAntialias ?? self.shouldAntialias
)
}
}
}