Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Created April 10, 2025 07:47
Show Gist options
  • Save Codelaby/343ab24724f4c86ec3f5014e0c254441 to your computer and use it in GitHub Desktop.
Save Codelaby/343ab24724f4c86ec3f5014e0c254441 to your computer and use it in GitHub Desktop.
Wave form Scrubber
//
// AudiowaveDemo.swift
// RangeSlidersPlaygroun
//
// Created by Codelaby on 9/4/25.
//
import SwiftUI
import AVKit
struct AudiowaveDemo: View {
@State private var progress: CGFloat = 0
var body: some View {
NavigationStack {
if let audioURL = remoteAudioURL {
List {
Section("Audio.wav") {
WaveformScrubber(url: audioURL, progress: $progress) { info in
print(info)
} onGestureActive: { status in
print(status)
}
.frame(height: 60)
}
}
}
}
}
var localAudioURL: URL? {
Bundle.main.url(forResource: "sample-15s", withExtension: "wav")
}
var localMP3AudioURL: URL? {
Bundle.main.url(forResource: "Yến Vô Hiết - 燕无歇 - Mây Bae Cover Official", withExtension: "mp3")
}
var remoteAudioURL: URL? {
URL(string: "https://cdn.freesound.org/previews/797/797925_10996917-lq.mp3")
}
}
#Preview {
AudiowaveDemo()
}
//sample-15s
struct WaveformScrubber: View {
var config: Config = .init()
var url: URL
@Binding var progress: CGFloat
var info: (AudioInfo) -> () = { _ in }
var onGestureActive: (Bool) -> () = { _ in }
/// View Properties
@State private var samples: [Float] = []
@State private var dowsizedSample: [Float] = []
var body: some View {
//Text("Hello world!")
ZStack {
WaveformShape(samples: dowsizedSample)
}
.frame(maxWidth: .infinity)
// .onAppear {
// initializedAudioFile()
// }
.onGeometryChange(for: CGSize.self) {
$0.size
} action: { newValue in
initializedAudioFile(newValue)
}
}
struct Config {
var spacing: Float = 2
var shapeWidth: Float = 2
var activeTint: Color = .black
var inactiveTint: Color = .gray
}
struct AudioInfo {
var duration: TimeInterval = 0
}
}
// MARK: Wave form shape
fileprivate struct WaveformShape: Shape {
var samples: [Float]
var spacing: Float = 2
var width: Float = 2
nonisolated func path(in rect: CGRect) -> Path {
Path { path in
var x: CGFloat = 0
for sample in samples {
let height = max(CGFloat(sample) * rect.height, 1)
path.addRect(CGRect(
origin: .init(x: x + CGFloat(width), y: -height / 2),
size: .init(width: CGFloat(width), height: height)
))
x += CGFloat(spacing + width)
}
}
.offsetBy(dx: 0, dy: rect.height / 2)
}
}
// MARK: Extract Audio data
extension WaveformScrubber {
func initializedAudioFile(_ size: CGSize) {
guard samples.isEmpty else { return }
Task.detached(priority: .high) {
do {
let audioFile = try AVAudioFile(forReading: url)
let audioInfo = extractAudioInfo(audioFile)
let samples = try extractAudioSamples(audioFile)
//print(samples.count)
let downSampleCount = Int(Float(size.width) / (config.spacing + config.shapeWidth))
let downSamples = downSampleAudioSamples(samples, downSampleCount)
//print(downSamples.count)
await MainActor.run {
self.samples = samples
self.dowsizedSample = downSamples
self.info(audioInfo)
}
} catch {
print("🐞error", error.localizedDescription)
}
}
}
nonisolated func extractAudioSamples(_ file: AVAudioFile) throws -> [Float] {
let format = file.processingFormat
let frameCount = UInt32(file.length)
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
return []
}
try file.read(into: buffer)
if let channel = buffer.floatChannelData {
let samples = Array(UnsafeBufferPointer(start: channel[0], count: Int(buffer.frameLength)))
return samples
}
return []
}
nonisolated func downSampleAudioSamples(_ samples: [Float], _ count: Int) -> [Float] {
let chunk = samples.count / count
var downsamples: [Float] = []
for index in 0..<count {
let start = index * chunk
let end = min((index + 1) * chunk, samples.count)
let chunkSamples = samples[start..<end]
let maxValue = chunkSamples.max() ?? 0
downsamples.append(maxValue)
}
return downsamples
}
nonisolated func extractAudioInfo(_ file: AVAudioFile) -> AudioInfo {
let format = file.processingFormat
let sampleRate = format.sampleRate
let duration = file.length / Int64(sampleRate)
return .init(duration: TimeInterval(duration))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment