| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Native audio waveform rendering for iOS, iPadOS, macOS, visionOS, and Mac Catalyst.
Three layers, pick whichever fits:
The Example/ directory contains a multi-platform showcase (WaveformGalleryView) that exercises every public surface interactively — recommended for poking around with renderers, styles, and configurations together.
Add the package via SPM:
https://github.com/dmrschmidt/DSWaveformImage (Up to Next Major from 14.0.0)
import DSWaveformImage // core: drawer, analyzer, renderers, types
import DSWaveformImageViews // UIKit + SwiftUI views (optional)SwiftUI
WaveformView(audioURL: url)UIKit
let view = WaveformImageView(frame: .init(x: 0, y: 0, width: 500, height: 300))
view.waveformAudioURL = urlRaw UIImage / NSImage
let image = try await WaveformImageDrawer().waveformImage(
fromAudioAt: url,
with: .init(size: size, style: .filled(.black))
)Every feature, once. Most options compose with most others — the example app's WaveformGalleryView lets you explore the permutations interactively.
LinearWaveformRenderer is the default — a horizontal 2D amplitude envelope. sides controls which side of the centerline the envelope occupies (.both, .up, .down). .stereo is a factory that interprets a two-channel sample array as left-on-top / right-on-bottom in a single image.
WaveformView(audioURL: url, renderer: LinearWaveformRenderer()) // default
WaveformView(audioURL: url, renderer: LinearWaveformRenderer(sides: .up)) // top-only
WaveformView(audioURL: url, renderer: LinearWaveformRenderer.stereo) // stereoCircularWaveformRenderer wraps the envelope around a circle. .circle fills the disk; .ring(innerFraction) cuts a hole, producing an annulus driven by the same envelope.
WaveformView(audioURL: url, renderer: CircularWaveformRenderer(kind: .circle))
WaveformView(audioURL: url, renderer: CircularWaveformRenderer(kind: .ring(0.5)))You can also implement your own renderer by conforming to WaveformRenderer.
Waveform.Style controls how the envelope is drawn — same renderer throughout. Top to bottom: .filled, .outlined, .gradient, .gradientOutlined, .striped.
.filled(.indigo)
.outlined(.indigo, 1.5)
.gradient([.blue, .purple])
.gradientOutlined([.blue, .purple], 1.5)
.striped(.init(color: .indigo, width: 3, spacing: 3)).spectralTint(low:high:) colors each amplitude column by its spectral centroid — bass-heavy columns get the low color, treble-heavy columns get the high color, with smooth interpolation in between. The envelope shape stays identical to the non-spectral path; only the fill follows the audio's frequency content over time.
WaveformView(audioURL: url, configuration: .init(
style: .spectralTint(low: .systemBlue, high: .systemRed)
))Renderers that opt in to spectral data conform to SpectralAwareWaveformRenderer; ones that don't fall back to filling with the low color. LinearWaveformRenderer conforms by default.
Channel handling lives on the renderer, not on Configuration. .merged (default) sums all channels; .specific(index) picks one; .stereo is its own thing — see below.
LinearWaveformRenderer(channelSelection: .merged) // default
LinearWaveformRenderer(channelSelection: .specific(0)) // left only
LinearWaveformRenderer(channelSelection: .specific(1)) // right onlyWhen you're calling WaveformAnalyzer directly for raw samples, pass channelSelection there instead.
LinearWaveformRenderer.stereo interprets a [allLeft..., allRight...] sample array as left on top, right on bottom, in one image.
WaveformView(audioURL: url, configuration: .init(
style: .gradient([.blue, .red])
), renderer: LinearWaveformRenderer.stereo)Waveform.AmplitudeScaling chooses how sample loudness maps to the canvas:
.init(style: .filled(.indigo), amplitudeScaling: .normalized)Waveform.Damping fades the envelope toward zero at one or both ends — useful for live capture where the leading/trailing edge would otherwise look like a hard cut.
.init(style: .filled(.indigo), damping: .init(percentage: 0.18, sides: .both))Pass a custom easing: closure to shape the falloff (e.g. { x in pow(x, 4) }).
WaveformView's trailing closure hands you the underlying WaveformShape so you can apply any SwiftUI ShapeStyle — LinearGradient, masks, animations, anything Shape supports. Thanks to @alfogrillo for the API.
WaveformView(audioURL: url) { shape in
shape.stroke(
LinearGradient(colors: [.purple, .blue, .cyan], startPoint: .leading, endPoint: .trailing),
style: StrokeStyle(lineWidth: 3, lineCap: .round)
)
} placeholder: {
ProgressView()
}If you already have samples, instantiate WaveformShape directly:
WaveformShape(samples: samples).fill(.indigo)WaveformLiveCanvas (SwiftUI) and WaveformLiveView (UIKit) render a [Float] sample stream in real time. Pair with AVAudioRecorder or any other source that reports per-frame amplitudes.
// SwiftUI
WaveformLiveCanvas(samples: recorder.samples, shouldDrawSilencePadding: true)
// UIKit
let view = WaveformLiveView()
recorder.updateMeters()
let amplitude = 1 - pow(10, recorder.averagePower(forChannel: 0) / 20)
view.add(sample: amplitude)For a complete recording demo see LiveRecordingShowcase in the example app.
Render the waveform once and overlay a progress-clipped tint on top. The base shape stays static; only the foreground mask reacts to playback time.
GeometryReader { geometry in
WaveformView(audioURL: url) { shape in
shape.fill(.secondary)
shape.fill(.accentColor).mask(alignment: .leading) {
Rectangle().frame(width: geometry.size.width * progress)
}
}
}The same idea works with two image views and a CAShapeLayer mask in UIKit — see UIKitShowcaseViewController.swift. There's no built-in ProgressWaveformView; every app's playback model is different and the masking trick is small enough that wrapping it would just be in your way.
WaveformAnalyzer and WaveformImageDrawer work with local file URLs. For a remote-audio recipe see #22.
Waveform and the UIImage category were removed in 6.0.0 to simplify the API.
Other iOS controls in Swift I maintain:
I'm doing all this for fun and joy and because I strongly believe in the power of open source. On the off-chance though, that using my library has brought joy to you and you just feel like saying "thank you", I would smile like a 4-year old getting a huge ice cream cone, if you'd support me via one of the sponsoring buttons ☺️💕
Alternatively, consider supporting me by downloading one of my side project iOS apps. If you're feeling in the mood of sending someone else a lovely gesture of appreciation, maybe check out my iOS app 💌 SoundCard to send them a real postcard with a personal audio message. Or download my ad-supported free to play game 🕹️ Snekris for iOS.
SoundCard — postcards with sound lets you send real, physical postcards with audio messages. Right from your iOS device.
DSWaveformImage is used to draw the waveforms of the audio messages that get printed on the postcards sent by SoundCard — postcards with audio.
The README images live in Promotion/readme/ and are produced by the WaveformScreenshots SPM executable target:
swift run WaveformScreenshotsThe iOS-simulator shots (live-recording.png, progress.png) come from the example app — build, install, launch with -tab 2 or -tab 3, and crop with ImageMagick:
xcrun simctl launch <udid> de.dmrschmidt.DSWaveformImageExample-iOS -tab 2
xcrun simctl io <udid> screenshot raw.png
magick raw.png -crop 1206x2343+0+177 +repage live-recording.png| Back | FazBrowse Home | New Git URL |