Skip to content

Commit a5416a7

Browse files
committed
add back ability to position waveform vertically when using image API
This is kind of rowing back on 0447737. Intentionally not documented though to avoid the previous confusion by not promoting its use too much and still leaving it out from the typical use cases inside views. While views can be moved, the plain image can not, so to avoid the need of further processing, the image API supports positioning again.
1 parent 332f74a commit a5416a7

File tree

6 files changed

+75
-31
lines changed

6 files changed

+75
-31
lines changed

Example/DSWaveformImageExample-iOS/ViewController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ class ViewController: UIViewController {
7272
middleWaveformView.waveformAudioURL = audioURL
7373

7474
Task {
75-
let image = try! await waveformImageDrawer.waveformImage(fromAudioAt: audioURL, with: bottomWaveformConfiguration)
75+
let image = try! await waveformImageDrawer.waveformImage(fromAudioAt: audioURL, with: bottomWaveformConfiguration, position: .top)
7676

7777
await MainActor.run {
7878
// as an added bonus, use CALayer's compositingFilter for more elaborate image display
79-
self.bottomWaveformView.layer.compositingFilter = "multiplyBlendMode"
79+
self.bottomWaveformView.layer.compositingFilter = "overlayBlendMode"
8080
self.bottomWaveformView.image = image
8181
}
8282
}
@@ -89,7 +89,7 @@ class ViewController: UIViewController {
8989
private var bottomWaveformConfiguration: Waveform.Configuration {
9090
Waveform.Configuration(
9191
size: bottomWaveformView.bounds.size,
92-
style: .filled(UIColor(red: 129/255.0, green: 178/255.0, blue: 154/255.0, alpha: 1))
92+
style: .filled(.black)
9393
)
9494
}
9595
}

Sources/DSWaveformImage/Renderers/CircularWaveformRenderer.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ public struct CircularWaveformRenderer: WaveformRenderer {
2525
self.kind = kind
2626
}
2727

28-
public func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int) -> CGPath {
28+
public func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) -> CGPath {
2929
switch kind {
30-
case .circle: return circlePath(samples: samples, with: configuration, lastOffset: lastOffset)
31-
case .ring: return ringPath(samples: samples, with: configuration, lastOffset: lastOffset)
30+
case .circle: return circlePath(samples: samples, with: configuration, lastOffset: lastOffset, position: position)
31+
case .ring: return ringPath(samples: samples, with: configuration, lastOffset: lastOffset, position: position)
3232
}
3333
}
3434

35-
public func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int) {
35+
public func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) {
3636
let path = path(samples: samples, with: configuration, lastOffset: lastOffset)
3737
context.addPath(path)
3838

@@ -54,10 +54,13 @@ public struct CircularWaveformRenderer: WaveformRenderer {
5454
}
5555
}
5656

57-
private func circlePath(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int) -> CGPath {
57+
private func circlePath(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position) -> CGPath {
5858
let graphRect = CGRect(origin: .zero, size: configuration.size)
5959
let maxRadius = CGFloat(min(graphRect.maxX, graphRect.maxY) / 2.0) * configuration.verticalScalingFactor
60-
let center = CGPoint(x: graphRect.maxX * 0.5, y: graphRect.maxY * 0.5)
60+
let center = CGPoint(
61+
x: graphRect.maxX * position.offset(),
62+
y: graphRect.maxY * position.offset()
63+
)
6164
let path = CGMutablePath()
6265

6366
path.move(to: center)
@@ -86,15 +89,18 @@ public struct CircularWaveformRenderer: WaveformRenderer {
8689
return path
8790
}
8891

89-
private func ringPath(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int) -> CGPath {
92+
private func ringPath(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position) -> CGPath {
9093
guard case let .ring(config) = kind else {
9194
fatalError("called with wrong kind")
9295
}
9396

9497
let graphRect = CGRect(origin: .zero, size: configuration.size)
9598
let maxRadius = CGFloat(min(graphRect.maxX, graphRect.maxY) / 2.0) * configuration.verticalScalingFactor
9699
let innerRadius: CGFloat = maxRadius * config
97-
let center = CGPoint(x: graphRect.maxX * 0.5, y: graphRect.maxY * 0.5)
100+
let center = CGPoint(
101+
x: graphRect.maxX * position.offset(),
102+
y: graphRect.maxY * position.offset()
103+
)
98104
let path = CGMutablePath()
99105

100106
path.move(to: CGPoint(

Sources/DSWaveformImage/Renderers/LinearWaveformRenderer.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@ import CoreGraphics
99
public struct LinearWaveformRenderer: WaveformRenderer {
1010
public init() {}
1111

12-
public func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int) -> CGPath {
12+
public func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) -> CGPath {
1313
let graphRect = CGRect(origin: CGPoint.zero, size: configuration.size)
14-
let positionAdjustedGraphCenter = 0.5 * graphRect.size.height
14+
let positionAdjustedGraphCenter = position.offset() * graphRect.size.height
1515
var path = CGMutablePath()
1616

1717
path.move(to: CGPoint(x: 0, y: positionAdjustedGraphCenter))
1818

1919
if case .striped = configuration.style {
20-
path = draw(samples: samples, path: path, with: configuration, lastOffset: lastOffset, sides: .both)
20+
path = draw(samples: samples, path: path, with: configuration, lastOffset: lastOffset, sides: .both, position: position)
2121
} else {
22-
path = draw(samples: samples, path: path, with: configuration, lastOffset: lastOffset, sides: .up)
23-
path = draw(samples: samples.reversed(), path: path, with: configuration, lastOffset: lastOffset, sides: .down)
22+
path = draw(samples: samples, path: path, with: configuration, lastOffset: lastOffset, sides: .up, position: position)
23+
path = draw(samples: samples.reversed(), path: path, with: configuration, lastOffset: lastOffset, sides: .down, position: position)
2424
}
2525

2626
path.closeSubpath()
2727
return path
2828
}
2929

30-
public func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int) {
31-
context.addPath(path(samples: samples, with: configuration, lastOffset: lastOffset))
30+
public func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) {
31+
context.addPath(path(samples: samples, with: configuration, lastOffset: lastOffset, position: position))
3232
defaultStyle(context: context, with: configuration)
3333
}
3434

@@ -44,10 +44,10 @@ public struct LinearWaveformRenderer: WaveformRenderer {
4444
case up, down, both
4545
}
4646

47-
private func draw(samples: [Float], path: CGMutablePath, with configuration: Waveform.Configuration, lastOffset: Int, sides: Sides) -> CGMutablePath {
47+
private func draw(samples: [Float], path: CGMutablePath, with configuration: Waveform.Configuration, lastOffset: Int, sides: Sides, position: Waveform.Position = .middle) -> CGMutablePath {
4848
let graphRect = CGRect(origin: CGPoint.zero, size: configuration.size)
49-
let positionAdjustedGraphCenter = 0.5 * graphRect.size.height
50-
let drawMappingFactor = 0.5 * graphRect.size.height * configuration.verticalScalingFactor // we always draw in the center now
49+
let positionAdjustedGraphCenter = position.offset() * graphRect.size.height
50+
let drawMappingFactor = graphRect.size.height * configuration.verticalScalingFactor
5151
let minimumGraphAmplitude: CGFloat = 1 / configuration.scale // we want to see at least a 1px line for silence
5252
var maxAmplitude: CGFloat = 0.0 // we know 1 is our max in normalized data, but we keep it 'generic'
5353

Sources/DSWaveformImage/WaveformImageDrawer+iOS.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public extension WaveformImageDrawer {
88
/// Renders a DSImage of the provided waveform samples.
99
///
1010
/// Samples need to be normalized within interval `(0...1)`.
11-
func waveformImage(from samples: [Float], with configuration: Waveform.Configuration, renderer: WaveformRenderer) -> DSImage? {
11+
func waveformImage(from samples: [Float], with configuration: Waveform.Configuration, renderer: WaveformRenderer, position: Waveform.Position = .middle) -> DSImage? {
1212
guard samples.count > 0, samples.count == Int(configuration.size.width * configuration.scale) else {
1313
print("ERROR: samples: \(samples.count) != \(configuration.size.width) * \(configuration.scale)")
1414
return nil
@@ -20,7 +20,7 @@ public extension WaveformImageDrawer {
2020
let dampedSamples = configuration.shouldDamp ? damp(samples, with: configuration) : samples
2121

2222
return imageRenderer.image { renderContext in
23-
draw(on: renderContext.cgContext, from: dampedSamples, with: configuration, renderer: renderer)
23+
draw(on: renderContext.cgContext, from: dampedSamples, with: configuration, renderer: renderer, position: position)
2424
}
2525
}
2626
}

Sources/DSWaveformImage/WaveformImageDrawer.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ public class WaveformImageDrawer: ObservableObject {
2727
public func waveformImage(fromAudioAt audioAssetURL: URL,
2828
with configuration: Waveform.Configuration,
2929
renderer: WaveformRenderer = LinearWaveformRenderer(),
30+
position: Waveform.Position = .middle,
3031
qos: DispatchQoS.QoSClass = .userInitiated) async throws -> DSImage {
31-
try await render(fromAudioAt: audioAssetURL, with: configuration, renderer: renderer, qos: qos)
32+
try await render(fromAudioAt: audioAssetURL, with: configuration, renderer: renderer, qos: qos, position: position)
3233
}
3334

3435
/// Async analyzes the provided audio and renders a DSImage of the waveform data calculated by the analyzer.
@@ -44,10 +45,11 @@ public class WaveformImageDrawer: ObservableObject {
4445
with configuration: Waveform.Configuration,
4546
renderer: WaveformRenderer = LinearWaveformRenderer(),
4647
qos: DispatchQoS.QoSClass = .userInitiated,
48+
position: Waveform.Position = .middle,
4749
completionHandler: @escaping (Result<DSImage, Error>) -> ()) {
4850
Task {
4951
do {
50-
let image = try await render(fromAudioAt: audioAssetURL, with: configuration, renderer: renderer, qos: qos)
52+
let image = try await render(fromAudioAt: audioAssetURL, with: configuration, renderer: renderer, qos: qos, position: position)
5153
completionHandler(.success(image))
5254
} catch {
5355
completionHandler(.failure(error))
@@ -96,13 +98,13 @@ extension WaveformImageDrawer {
9698
draw(on: context, from: paddedSamples, with: configuration, renderer: renderer)
9799
}
98100

99-
func draw(on context: CGContext, from samples: [Float], with configuration: Waveform.Configuration, renderer: WaveformRenderer) {
101+
func draw(on context: CGContext, from samples: [Float], with configuration: Waveform.Configuration, renderer: WaveformRenderer, position: Waveform.Position = .middle) {
100102
context.setAllowsAntialiasing(configuration.shouldAntialias)
101103
context.setShouldAntialias(configuration.shouldAntialias)
102104
context.setAlpha(1.0)
103105

104106
drawBackground(on: context, with: configuration)
105-
renderer.render(samples: samples, on: context, with: configuration, lastOffset: lastOffset)
107+
renderer.render(samples: samples, on: context, with: configuration, lastOffset: lastOffset, position: position)
106108
}
107109

108110
/// Damp the samples for a smoother animation.
@@ -125,14 +127,15 @@ private extension WaveformImageDrawer {
125127
fromAudioAt audioAssetURL: URL,
126128
with configuration: Waveform.Configuration,
127129
renderer: WaveformRenderer,
128-
qos: DispatchQoS.QoSClass
130+
qos: DispatchQoS.QoSClass,
131+
position: Waveform.Position
129132
) async throws -> DSImage {
130133
let sampleCount = Int(configuration.size.width * configuration.scale)
131134
let waveformAnalyzer = WaveformAnalyzer()
132135
let samples = try await waveformAnalyzer.samples(fromAudioAt: audioAssetURL, count: sampleCount, qos: qos)
133136
let dampedSamples = configuration.shouldDamp ? self.damp(samples, with: configuration) : samples
134137

135-
if let image = waveformImage(from: dampedSamples, with: configuration, renderer: renderer) {
138+
if let image = waveformImage(from: dampedSamples, with: configuration, renderer: renderer, position: position) {
136139
return image
137140
} else {
138141
throw GenerationError.generic

Sources/DSWaveformImage/WaveformImageTypes.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public protocol WaveformRenderer: Sendable {
4141
- 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
4242
continuously and the waveform moves across the view.
4343
*/
44-
func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int) -> CGPath
44+
func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position) -> CGPath
4545

4646
/**
4747
Renders the waveform samples on the provided `CGContext`.
@@ -53,10 +53,45 @@ public protocol WaveformRenderer: Sendable {
5353
- 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
5454
continuously and the waveform moves across the view.
5555
*/
56-
func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int)
56+
func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position)
57+
}
58+
59+
public extension WaveformRenderer {
60+
func path(samples: [Float], with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) -> CGPath {
61+
path(samples: samples, with: configuration, lastOffset: lastOffset, position: position)
62+
}
63+
64+
func render(samples: [Float], on context: CGContext, with configuration: Waveform.Configuration, lastOffset: Int, position: Waveform.Position = .middle) {
65+
render(samples: samples, on: context, with: configuration, lastOffset: lastOffset, position: position)
66+
}
5767
}
5868

5969
public enum Waveform {
70+
/** Position of the drawn waveform. */
71+
public enum Position: Equatable {
72+
/// **top**: Draws the waveform at the top of the image, such that only the bottom 50% are visible.
73+
case top
74+
75+
/// **middle**: Draws the waveform in the middle the image, such that the entire waveform is visible.
76+
case middle
77+
78+
/// **bottom**: Draws the waveform at the bottom of the image, such that only the top 50% are visible.
79+
case bottom
80+
81+
/// **custom**: Draws the waveform at the specified point of the image. Clamped within range `(0...1)`. Where `0`
82+
/// is equal to `.top`, `0.5` is equal to `.middle` and `1` is equal to `.bottom`.
83+
case custom(CGFloat)
84+
85+
func offset() -> CGFloat {
86+
switch self {
87+
case .top: return 0.0
88+
case .middle: return 0.5
89+
case .bottom: return 1.0
90+
case let .custom(offset): return min(1, max(0, offset))
91+
}
92+
}
93+
}
94+
6095
/**
6196
Style of the waveform which is used during drawing:
6297
- **filled**: Use solid color for the waveform.

0 commit comments

Comments
 (0)