DSWaveformImage
Generate waveform images from audio files on iOS, macOS & visionOS in Swift. Native SwiftUI & UIKit views.
Native audio waveform rendering for **iOS**, **iPadOS**, **macOS**, **visionOS**, and Mac Catalyst. The project is written primarily in Swift, distributed under the MIT License license, first published in 2013. It has gained significant community traction with 1,247 stars and 128 forks on GitHub. Key topics include: audio-analysis, audio-files, audio-visualizer, catalyst, fft.
DSWaveformImage
Native audio waveform rendering for iOS, iPadOS, macOS, visionOS, and Mac Catalyst.
<p align="center"><img src="./Promotion/readme/hero.png" alt="Waveform hero" width="800"></p>Three layers, pick whichever fits:
- SwiftUI views —
WaveformView,WaveformLiveCanvas,WaveformShape - UIKit views —
WaveformImageView,WaveformLiveView - Raw API —
WaveformImageDrawerrenders toUIImage/NSImage;WaveformAnalyzergives you the normalized[Float]samples to do your own thing with.
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.
Installation
Add the package via SPM:
https://github.com/dmrschmidt/DSWaveformImage (Up to Next Major from 14.0.0)
swiftimport DSWaveformImage // core: drawer, analyzer, renderers, types import DSWaveformImageViews // UIKit + SwiftUI views (optional)
Quick start
SwiftUI
swiftWaveformView(audioURL: url)
UIKit
swiftlet view = WaveformImageView(frame: .init(x: 0, y: 0, width: 500, height: 300)) view.waveformAudioURL = url
Raw UIImage / NSImage
swiftlet image = try await WaveformImageDrawer().waveformImage( fromAudioAt: url, with: .init(size: size, style: .filled(.black)) )
Gallery
Every feature, once. Most options compose with most others — the example app's WaveformGalleryView lets you explore the permutations interactively.
Linear renderer
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.
swiftWaveformView(audioURL: url, renderer: LinearWaveformRenderer()) // default WaveformView(audioURL: url, renderer: LinearWaveformRenderer(sides: .up)) // top-only WaveformView(audioURL: url, renderer: LinearWaveformRenderer.stereo) // stereo
Circular renderer
CircularWaveformRenderer wraps the envelope around a circle. .circle fills the disk; .ring(innerFraction) cuts a hole, producing an annulus driven by the same envelope.
swiftWaveformView(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.
Styles
Waveform.Style controls how the envelope is drawn — same renderer throughout. Top to bottom: .filled, .outlined, .gradient, .gradientOutlined, .striped.
swift.filled(.indigo) .outlined(.indigo, 1.5) .gradient([.blue, .purple]) .gradientOutlined([.blue, .purple], 1.5) .striped(.init(color: .indigo, width: 3, spacing: 3))
Spectral tint
.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.
swiftWaveformView(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 selection
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.
swiftLinearWaveformRenderer(channelSelection: .merged) // default LinearWaveformRenderer(channelSelection: .specific(0)) // left only LinearWaveformRenderer(channelSelection: .specific(1)) // right only
When you're calling WaveformAnalyzer directly for raw samples, pass channelSelection there instead.
Stereo
LinearWaveformRenderer.stereo interprets a [allLeft..., allRight...] sample array as left on top, right on bottom, in one image.
swiftWaveformView(audioURL: url, configuration: .init( style: .gradient([.blue, .red]) ), renderer: LinearWaveformRenderer.stereo)
Amplitude scaling
Waveform.AmplitudeScaling chooses how sample loudness maps to the canvas:
.absolute(default) — fixed0 dBFSreference. Quiet recordings render visibly smaller than loud ones; loudness across files is preserved..normalized— shift the file's peak to the canvas edge so every clip fills the canvas regardless of recording level. The envelope shape is preserved.
swift.init(style: .filled(.indigo), amplitudeScaling: .normalized)
Damping
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.
swift.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) }).
Custom shape (SwiftUI)
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.
swiftWaveformView(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:
swiftWaveformShape(samples: samples).fill(.indigo)
Live recording
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.
swift// 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.
Progress / playback
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.
<p align="center"><img src="./Promotion/readme/progress.png" alt="Progress / scrubber screen" width="300"></p>swiftGeometryReader { 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.
Loading remote audio
WaveformAnalyzer and WaveformImageDrawer work with local file URLs. For a remote-audio recipe see #22.
Migration
In 15.0.0 (upcoming)
Waveform.Style.spectralTint(low:high:)is a new case. Exhaustiveswitchstatements overWaveform.Stylewill need to add it (or an@unknown default).Position.middlewaveforms render smaller atverticalScalingFactor=1. The previous math overshot, letting peak-loud samples extend a full canvas height in each direction from the centerline. They now fill exactly the budget the centerline leaves available (half-canvas per direction for.middle, full canvas for.top/.bottom). BumpverticalScalingFactorif you want the old visual size.- Stereo + damping now damps each channel half independently. Previously the damping ran across the concatenated
[allLeft..., allRight...]array, so only the start of L and the end of R faded; the middle (end of L + start of R) got no damping at all. - Live stereo drawing window doubled internally to cover both channels — fixes the left channel being silently dropped from the visible scroll window.
LinearWaveformRenderernow also conforms to the newSpectralAwareWaveformRendererprotocol (additive).- New
Waveform.AmplitudeScaling(defaults to.absolute, preserves prior behavior). Adds anamplitudeScaling:parameter toWaveform.Configuration.init/with(...), both with defaults. - New
WaveformAnalyzer.analyze(...)returns amplitudes + per-slot spectral centroids in one pass.
In 14.0.0
- Minimum deployment target is iOS 15.0, macOS 12.0 to remove internal usage of deprecated APIs.
WaveformAnalyzerandWaveformImageDrawernow returnResult<[Float] | DSImage, Error>when used with completion handlers.WaveformAnalyzeris stateless and takes the URL insamples(fromAudioAt:count:qos:)instead of its constructor.WaveformViewhas a new constructor that exposes the underlyingWaveformShape, see #78.
In 13.0.0
dampening→dampingeverywhere (most notably inWaveform.Configuration). See #64..outlinedand.gradientOutlinedstyles were added toWaveform.Style.Waveform.Positionwas removed. Move positioning responsibility to the parent view.
In 12.0.0
- The rendering pipeline was split out from analysis — implement
WaveformRendererfor custom renderers. - New
CircularWaveformRenderer. positionremoved fromWaveform.Configuration, see 0447737.- New
Waveform.Styleoptions need accounting for inswitchstatements.
In 11.0.0
- The library was split into
DSWaveformImageandDSWaveformImageViews. Add the additionalimport DSWaveformImageViewsif you use the native views. - SwiftUI views moved from
Bindingto plain values.
In 9.0.0
- Public API names tightened; all types grouped under the
Waveformenum namespace (WaveformConfiguration→Waveform.Configuration, etc.).
In 7.0.0
- Colors moved into associated values on the respective
styleenum case.
Waveform and the UIImage category were removed in 6.0.0 to simplify the API.
More related iOS Controls
Other iOS controls in Swift I maintain:
- SwiftColorWheel — a delightful color picker
- QRCode — a customizable QR code generator
If you really like this library (aka Sponsoring)
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.
<p float="left"> <a href="https://www.buymeacoffee.com/dmrschmidt" target="_blank"> <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" height="60"></a> <a href="https://www.snekris.com" target="_blank"> <img src="http://snekris.com/images/snekris-banner.png" alt="Play Snekris" width="217" height="60"></a> </p>See it live in action
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.
<div align="center"> <a href="http://bit.ly/soundcardio"> <img src="./Promotion/appstore.svg" alt="Download SoundCard">
Download SoundCard on the App Store.
</a>
<a href="http://bit.ly/soundcardio"> <img src="https://www.soundcard.io/images/opengraph-preview.jpg" alt="Screenshot"> </a>
Regenerating screenshots
The README images live in Promotion/readme/ and are produced by the WaveformScreenshots SPM executable target:
bashswift run WaveformScreenshots
The 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:
bashxcrun 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
Contributors
Showing top 12 contributors by commit count.
