Retour au classement

dmrschmidt/DSWaveformImage

Swift

Generate waveform images from audio files on iOS, macOS & visionOS in Swift. Native SwiftUI & UIKit views.

audio-analysisaudio-visualizerwaveform-imageswaveformaudio-filesfourier-transformfftswiftiosswiftuimac-catalystcatalyst
Croissance des étoiles
Étoiles
1.3k
Forks
130
Croissance hebdomadaire
Issues
0
5001k
nov. 2013janv. 2018avr. 2022juil. 2026
README

DSWaveformImage

Swift Package Manager compatible

Native audio waveform rendering for iOS, iPadOS, macOS, visionOS, and Mac Catalyst.

Waveform hero

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.

Installation

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)

Quick start

SwiftUI

WaveformView(audioURL: url)

UIKit

let view = WaveformImageView(frame: .init(x: 0, y: 0, width: 500, height: 300))
view.waveformAudioURL = url

Raw UIImage / NSImage

let 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.

Linear and stereo renderers
WaveformView(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.

Circular and ring renderers
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.

Styles

Waveform.Style controls how the envelope is drawn — same renderer throughout. Top to bottom: .filled, .outlined, .gradient, .gradientOutlined, .striped.

Five rendering styles
.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.

Spectral tint with two color presets
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 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.

Merged, left, and right channel selection
LinearWaveformRenderer(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.

Stereo waveform
WaveformView(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) — fixed 0 dBFS reference. 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.
.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.

Damping off and on
.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 ShapeStyleLinearGradient, 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)

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.

Live recording screen

// 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.

Progress / scrubber screen

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.

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. Exhaustive switch statements over Waveform.Style will need to add it (or an @unknown default).
  • Position.middle waveforms render smaller at verticalScalingFactor=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). Bump verticalScalingFactor if 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.
  • LinearWaveformRenderer now also conforms to the new SpectralAwareWaveformRenderer protocol (additive).
  • New Waveform.AmplitudeScaling (defaults to .absolute, preserves prior behavior). Adds an amplitudeScaling: parameter to Waveform.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.
  • WaveformAnalyzer and WaveformImageDrawer now return Result<[Float] | DSImage, Error> when used with completion handlers.
  • WaveformAnalyzer is stateless and takes the URL in samples(fromAudioAt:count:qos:) instead of its constructor.
  • WaveformView has a new constructor that exposes the underlying WaveformShape, see #78.

In 13.0.0

  • dampeningdamping everywhere (most notably in Waveform.Configuration). See #64.
  • .outlined and .gradientOutlined styles were added to Waveform.Style.
  • Waveform.Position was removed. Move positioning responsibility to the parent view.

In 12.0.0

  • The rendering pipeline was split out from analysis — implement WaveformRenderer for custom renderers.
  • New CircularWaveformRenderer.
  • position removed from Waveform.Configuration, see 0447737.
  • New Waveform.Style options need accounting for in switch statements.

In 11.0.0

  • The library was split into DSWaveformImage and DSWaveformImageViews. Add the additional import DSWaveformImageViews if you use the native views.
  • SwiftUI views moved from Binding to plain values.

In 9.0.0

  • Public API names tightened; all types grouped under the Waveform enum namespace (WaveformConfigurationWaveform.Configuration, etc.).

In 7.0.0

  • Colors moved into associated values on the respective style enum 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:

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.

Buy Me A Coffee Play Snekris

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.

 

 

Screenshot

Regenerating screenshots

The README images live in Promotion/readme/ and are produced by the WaveformScreenshots SPM executable target:

swift 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:

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
Dépôts similaires
RVC-Project/Retrieval-based-Voice-Conversion-WebUI

Easily train a good VC model with voice data <= 10 mins!

PythonPyPIMIT Licensechangesovits
36.5k5.1k
svc-develop-team/so-vits-svc

SoftVC VITS Singing Voice Conversion

PythonPyPIGNU Affero General Public License v3.0aiaudio-analysis
28.1k5k
FunAudioLLM/SenseVoice

Open-source SenseVoiceSmall model for Mandarin, Cantonese, English, Japanese, and Korean ASR, language ID, emotion recognition, and audio event detection.

CMIT Licenseasrspeech-recognition
funaudiollm.github.io
8.9k795
MTG/essentia

C++ library for audio and music analysis, description and synthesis, including Python bindings

C++GNU Affero General Public License v3.0audiomusic
essentia.upf.edu
3.6k623
libAudioFlux/audioFlux

A library for audio and music analysis, feature extraction.

CMIT Licenseaudioaudio-analysis
audioflux.top
3.3k145
CPJKU/madmom

Python audio and music signal processing library

PythonPyPIOtheraudio-analysissignal-processing
madmom.readthedocs.io
1.7k277
mikeroyal/PipeWire-Guide

PipeWire Guide. Learn about how PipeWire gives your Linux system a Professional Audio/Video Processing workflow.

Shellpipewireaudio
1.5k41
BillyDM/awesome-audio-dsp

My curated list of audio DSP and plugin development resources (Github fork)

Creative Commons Zero v1.0 Universaldspaudio
1.4k79
acoustid/chromaprint

C library for generating audio fingerprints used by AcoustID

C++Otherchromaprintacoustid
1.3k173
tlecomte/friture

Real-time audio visualizations (spectrum, spectrogram, etc.)

PythonPyPIGNU General Public License v3.0audio-analysisaudio
friture.org
1.1k186
aiXander/Realtime_PyAudio_FFT

Realtime audio analysis in Python to extract audio features from streaming audio and send them over OSC to any client app.

PythonPyPIMIT Licenserealtime-audiofft
1.1k205