Skip to content

Commit 3c0f9f2

Browse files
committed
feat(AVAudioFileExtensions): first approach implementation to generate metering levels
1 parent d019e6a commit 3c0f9f2

File tree

2 files changed

+214
-11
lines changed

2 files changed

+214
-11
lines changed

SoundWave/Classes/AVAudioFileExtensions.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,60 @@ extension AVAudioFile {
2929
result[channel][sampleIndex] = dbPower
3030
}
3131
}
32+
print(result)
3233
return result
3334
}
3435
}
36+
37+
/// Holds audio information used for building waveforms
38+
final class AudioContext {
39+
/// The audio asset URL used to load the context
40+
public let audioURL: URL
41+
42+
/// Total number of samples in loaded asset
43+
public let totalSamples: Int
44+
45+
/// Loaded asset
46+
public let asset: AVAsset
47+
48+
// Loaded assetTrack
49+
public let assetTrack: AVAssetTrack
50+
51+
private init(audioURL: URL, totalSamples: Int, asset: AVAsset, assetTrack: AVAssetTrack) {
52+
self.audioURL = audioURL
53+
self.totalSamples = totalSamples
54+
self.asset = asset
55+
self.assetTrack = assetTrack
56+
}
57+
58+
public static func load(fromAudioURL audioURL: URL, completionHandler: @escaping (_ audioContext: AudioContext?) -> ()) {
59+
let asset = AVURLAsset(url: audioURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: NSNumber(value: true as Bool)])
60+
61+
guard let assetTrack = asset.tracks(withMediaType: AVMediaType.audio).first else {
62+
fatalError("Couldn't load AVAssetTrack")
63+
}
64+
65+
asset.loadValuesAsynchronously(forKeys: ["duration"]) {
66+
var error: NSError?
67+
let status = asset.statusOfValue(forKey: "duration", error: &error)
68+
switch status {
69+
case .loaded:
70+
guard
71+
let formatDescriptions = assetTrack.formatDescriptions as? [CMAudioFormatDescription],
72+
let audioFormatDesc = formatDescriptions.first,
73+
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDesc)
74+
else { break }
75+
76+
let totalSamples = Int((asbd.pointee.mSampleRate) * Float64(asset.duration.value) / Float64(asset.duration.timescale))
77+
let audioContext = AudioContext(audioURL: audioURL, totalSamples: totalSamples, asset: asset, assetTrack: assetTrack)
78+
completionHandler(audioContext)
79+
return
80+
81+
case .failed, .cancelled, .loading, .unknown:
82+
print("Couldn't load asset: \(error?.localizedDescription ?? "Unknown error")")
83+
}
84+
85+
completionHandler(nil)
86+
}
87+
}
88+
}

SoundWave/Classes/AudioVisualizationView.swift

Lines changed: 160 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Bastien Falcou on 12/6/16.
66
//
77

8+
import Accelerate
89
import AVFoundation
910
import UIKit
1011

@@ -150,6 +151,154 @@ public class AudioVisualizationView: BaseNibView {
150151
return self.meteringLevelsClusteredArray
151152
}
152153

154+
func render(audioContext: AudioContext?, targetSamples: Int = 100) -> [Float]{
155+
guard let audioContext = audioContext else {
156+
fatalError("Couldn't create the audioContext")
157+
}
158+
159+
let sampleRange: CountableRange<Int> = 0..<audioContext.totalSamples/3
160+
161+
guard let reader = try? AVAssetReader(asset: audioContext.asset)
162+
else {
163+
fatalError("Couldn't initialize the AVAssetReader")
164+
}
165+
166+
reader.timeRange = CMTimeRange(start: CMTime(value: Int64(sampleRange.lowerBound), timescale: audioContext.asset.duration.timescale),
167+
duration: CMTime(value: Int64(sampleRange.count), timescale: audioContext.asset.duration.timescale))
168+
169+
let outputSettingsDict: [String : Any] = [
170+
AVFormatIDKey: Int(kAudioFormatLinearPCM),
171+
AVLinearPCMBitDepthKey: 16,
172+
AVLinearPCMIsBigEndianKey: false,
173+
AVLinearPCMIsFloatKey: false,
174+
AVLinearPCMIsNonInterleaved: false
175+
]
176+
177+
let readerOutput = AVAssetReaderTrackOutput(track: audioContext.assetTrack,
178+
outputSettings: outputSettingsDict)
179+
readerOutput.alwaysCopiesSampleData = false
180+
reader.add(readerOutput)
181+
182+
var channelCount = 1
183+
let formatDescriptions = audioContext.assetTrack.formatDescriptions as! [CMAudioFormatDescription]
184+
for item in formatDescriptions {
185+
guard let fmtDesc = CMAudioFormatDescriptionGetStreamBasicDescription(item) else {
186+
fatalError("Couldn't get the format description")
187+
}
188+
channelCount = Int(fmtDesc.pointee.mChannelsPerFrame)
189+
}
190+
191+
let samplesPerPixel = max(1, channelCount * sampleRange.count / targetSamples)
192+
let filter = [Float](repeating: 1.0 / Float(samplesPerPixel), count: samplesPerPixel)
193+
194+
var outputSamples = [Float]()
195+
var sampleBuffer = Data()
196+
197+
// 16-bit samples
198+
reader.startReading()
199+
defer { reader.cancelReading() }
200+
201+
while reader.status == .reading {
202+
guard let readSampleBuffer = readerOutput.copyNextSampleBuffer(),
203+
let readBuffer = CMSampleBufferGetDataBuffer(readSampleBuffer) else {
204+
break
205+
}
206+
// Append audio sample buffer into our current sample buffer
207+
var readBufferLength = 0
208+
var readBufferPointer: UnsafeMutablePointer<Int8>?
209+
CMBlockBufferGetDataPointer(readBuffer,
210+
atOffset: 0,
211+
lengthAtOffsetOut: &readBufferLength,
212+
totalLengthOut: nil,
213+
dataPointerOut: &readBufferPointer)
214+
sampleBuffer.append(UnsafeBufferPointer(start: readBufferPointer, count: readBufferLength))
215+
CMSampleBufferInvalidate(readSampleBuffer)
216+
217+
let totalSamples = sampleBuffer.count / MemoryLayout<Int16>.size
218+
let downSampledLength = totalSamples / samplesPerPixel
219+
let samplesToProcess = downSampledLength * samplesPerPixel
220+
221+
guard samplesToProcess > 0 else { continue }
222+
223+
processSamples(fromData: &sampleBuffer,
224+
outputSamples: &outputSamples,
225+
samplesToProcess: samplesToProcess,
226+
downSampledLength: downSampledLength,
227+
samplesPerPixel: samplesPerPixel,
228+
filter: filter)
229+
//print("Status: \(reader.status)")
230+
}
231+
232+
// Process the remaining samples at the end which didn't fit into samplesPerPixel
233+
let samplesToProcess = sampleBuffer.count / MemoryLayout<Int16>.size
234+
if samplesToProcess > 0 {
235+
let downSampledLength = 1
236+
let samplesPerPixel = samplesToProcess
237+
let filter = [Float](repeating: 1.0 / Float(samplesPerPixel), count: samplesPerPixel)
238+
239+
processSamples(fromData: &sampleBuffer,
240+
outputSamples: &outputSamples,
241+
samplesToProcess: samplesToProcess,
242+
downSampledLength: downSampledLength,
243+
samplesPerPixel: samplesPerPixel,
244+
filter: filter)
245+
//print("Status: \(reader.status)")
246+
}
247+
248+
// if (reader.status == AVAssetReaderStatusFailed || reader.status == AVAssetReaderStatusUnknown)
249+
guard reader.status == .completed || true else {
250+
fatalError("Couldn't read the audio file")
251+
}
252+
253+
return outputSamples
254+
}
255+
256+
func processSamples(fromData sampleBuffer: inout Data,
257+
outputSamples: inout [Float],
258+
samplesToProcess: Int,
259+
downSampledLength: Int,
260+
samplesPerPixel: Int,
261+
filter: [Float]) {
262+
sampleBuffer.withUnsafeBytes { (samples: UnsafePointer<Int16>) in
263+
var processingBuffer = [Float](repeating: 0.0, count: samplesToProcess)
264+
265+
let sampleCount = vDSP_Length(samplesToProcess)
266+
267+
//Convert 16bit int samples to floats
268+
vDSP_vflt16(samples, 1, &processingBuffer, 1, sampleCount)
269+
270+
//Take the absolute values to get amplitude
271+
vDSP_vabs(processingBuffer, 1, &processingBuffer, 1, sampleCount)
272+
273+
//get the corresponding dB, and clip the results
274+
getdB(from: &processingBuffer)
275+
276+
//Downsample and average
277+
var downSampledData = [Float](repeating: 0.0, count: downSampledLength)
278+
vDSP_desamp(processingBuffer,
279+
vDSP_Stride(samplesPerPixel),
280+
filter, &downSampledData,
281+
vDSP_Length(downSampledLength),
282+
vDSP_Length(samplesPerPixel))
283+
284+
//Remove processed samples
285+
sampleBuffer.removeFirst(samplesToProcess * MemoryLayout<Int16>.size)
286+
287+
outputSamples += downSampledData
288+
}
289+
}
290+
291+
func getdB(from normalizedSamples: inout [Float]) {
292+
// Convert samples to a log scale
293+
var zero: Float = 32768.0
294+
vDSP_vdbcon(normalizedSamples, 1, &zero, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count), 1)
295+
296+
//Clip to [noiseFloor, 0]
297+
var ceil: Float = 0.0
298+
var noiseFloorMutable: Float = -80.0 // TODO: CHANGE THIS VALUE
299+
vDSP_vclip(normalizedSamples, 1, &noiseFloorMutable, &ceil, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count))
300+
}
301+
153302
// PRAGMA: - Play Mode Handling
154303

155304
public func play(for duration: TimeInterval) {
@@ -207,22 +356,22 @@ public class AudioVisualizationView: BaseNibView {
207356
fatalError("trying to read audio visualization in write mode")
208357
}
209358

210-
let track: AVAudioFile
211-
do {
212-
track = try AVAudioFile(forReading: url)
213-
self.meteringLevels = try track.buffer().first
214-
} catch {
215-
fatalError("failed to create file from url")
216-
}
359+
var outputArray : [Float] = []
360+
AudioContext.load(fromAudioURL: url, completionHandler: { audioContext in
361+
guard let audioContext = audioContext else {
362+
fatalError("Couldn't create the audioContext")
363+
}
364+
outputArray = self.render(audioContext: audioContext, targetSamples: 300)
365+
})
366+
367+
self.meteringLevels = outputArray
217368

369+
print(self.meteringLevels)
218370
guard self.meteringLevels != nil else {
219371
fatalError("trying to read audio visualization of non initialized sound record")
220372
}
221373

222-
let audioNodeFileLength = AVAudioFrameCount(track.length)
223-
let duration = Double(audioNodeFileLength) / 44100.0 // Divide by the AVSampleRateKey in the recorder settings
224-
225-
self.play(for: duration)
374+
self.play(for: 10)
226375
}
227376

228377
// MARK: - Mask + Gradient

0 commit comments

Comments
 (0)