Skip to content

Commit 8f4e625

Browse files
committed
refactor(AudioContext): move files around
1 parent fe83a94 commit 8f4e625

File tree

4 files changed

+222
-216
lines changed

4 files changed

+222
-216
lines changed

AudioContext.swift

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
//
2+
// AudioContext.swift
3+
// Pods-SoundWave_Example
4+
//
5+
// Created by Bastien Falcou on 4/27/19.
6+
// Inspired from https://stackoverflow.com/a/52280271
7+
//
8+
9+
import Accelerate
10+
import AVFoundation
11+
12+
final class AudioContext {
13+
public let audioURL: URL
14+
public let totalSamples: Int
15+
public let asset: AVAsset
16+
public let assetTrack: AVAssetTrack
17+
18+
private init(audioURL: URL, totalSamples: Int, asset: AVAsset, assetTrack: AVAssetTrack) {
19+
self.audioURL = audioURL
20+
self.totalSamples = totalSamples
21+
self.asset = asset
22+
self.assetTrack = assetTrack
23+
}
24+
25+
public static func load(fromAudioURL audioURL: URL, completionHandler: @escaping (_ audioContext: AudioContext?) -> ()) {
26+
let asset = AVURLAsset(url: audioURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: NSNumber(value: true as Bool)])
27+
28+
guard let assetTrack = asset.tracks(withMediaType: AVMediaType.audio).first else {
29+
fatalError("Couldn't load AVAssetTrack")
30+
}
31+
32+
asset.loadValuesAsynchronously(forKeys: ["duration"]) {
33+
var error: NSError?
34+
let status = asset.statusOfValue(forKey: "duration", error: &error)
35+
switch status {
36+
case .loaded:
37+
guard let formatDescriptions = assetTrack.formatDescriptions as? [CMAudioFormatDescription],
38+
let audioFormatDesc = formatDescriptions.first,
39+
let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDesc) else
40+
{
41+
break
42+
}
43+
let totalSamples = Int((asbd.pointee.mSampleRate) * Float64(asset.duration.value) / Float64(asset.duration.timescale))
44+
let audioContext = AudioContext(audioURL: audioURL, totalSamples: totalSamples, asset: asset, assetTrack: assetTrack)
45+
completionHandler(audioContext)
46+
case .failed, .cancelled, .loading, .unknown:
47+
print("Couldn't load asset: \(error?.localizedDescription ?? "Unknown error")")
48+
completionHandler(nil)
49+
}
50+
}
51+
}
52+
53+
public func render(targetSamples: Int = 100) -> [Float] {
54+
let sampleRange: CountableRange<Int> = 0..<self.totalSamples / 3
55+
56+
guard let reader = try? AVAssetReader(asset: self.asset) else {
57+
fatalError("Couldn't initialize the AVAssetReader")
58+
}
59+
60+
reader.timeRange = CMTimeRange(
61+
start: CMTime(value: Int64(sampleRange.lowerBound), timescale: self.asset.duration.timescale),
62+
duration: CMTime(value: Int64(sampleRange.count), timescale: self.asset.duration.timescale)
63+
)
64+
65+
let outputSettingsDict: [String : Any] = [
66+
AVFormatIDKey: Int(kAudioFormatLinearPCM),
67+
AVLinearPCMBitDepthKey: 16,
68+
AVLinearPCMIsBigEndianKey: false,
69+
AVLinearPCMIsFloatKey: false,
70+
AVLinearPCMIsNonInterleaved: false
71+
]
72+
73+
let readerOutput = AVAssetReaderTrackOutput(track: self.assetTrack, outputSettings: outputSettingsDict)
74+
readerOutput.alwaysCopiesSampleData = false
75+
reader.add(readerOutput)
76+
77+
var channelCount = 1
78+
let formatDescriptions = self.assetTrack.formatDescriptions as! [CMAudioFormatDescription]
79+
for item in formatDescriptions {
80+
guard let fmtDesc = CMAudioFormatDescriptionGetStreamBasicDescription(item) else {
81+
fatalError("Couldn't get the format description")
82+
}
83+
channelCount = Int(fmtDesc.pointee.mChannelsPerFrame)
84+
}
85+
86+
let samplesPerPixel = max(1, channelCount * sampleRange.count / targetSamples)
87+
let filter = [Float](repeating: 1.0 / Float(samplesPerPixel), count: samplesPerPixel)
88+
89+
var outputSamples = [Float]()
90+
var sampleBuffer = Data()
91+
92+
// 16-bit samples
93+
reader.startReading()
94+
defer { reader.cancelReading() }
95+
96+
while reader.status == .reading {
97+
guard let readSampleBuffer = readerOutput.copyNextSampleBuffer(),
98+
let readBuffer = CMSampleBufferGetDataBuffer(readSampleBuffer) else {
99+
break
100+
}
101+
// Append audio sample buffer into our current sample buffer
102+
var readBufferLength = 0
103+
var readBufferPointer: UnsafeMutablePointer<Int8>?
104+
CMBlockBufferGetDataPointer(readBuffer,
105+
atOffset: 0,
106+
lengthAtOffsetOut: &readBufferLength,
107+
totalLengthOut: nil,
108+
dataPointerOut: &readBufferPointer)
109+
sampleBuffer.append(UnsafeBufferPointer(start: readBufferPointer, count: readBufferLength))
110+
CMSampleBufferInvalidate(readSampleBuffer)
111+
112+
let totalSamples = sampleBuffer.count / MemoryLayout<Int16>.size
113+
let downSampledLength = totalSamples / samplesPerPixel
114+
let samplesToProcess = downSampledLength * samplesPerPixel
115+
116+
guard samplesToProcess > 0 else { continue }
117+
118+
processSamples(fromData: &sampleBuffer,
119+
outputSamples: &outputSamples,
120+
samplesToProcess: samplesToProcess,
121+
downSampledLength: downSampledLength,
122+
samplesPerPixel: samplesPerPixel,
123+
filter: filter)
124+
}
125+
126+
// Process the remaining samples at the end which didn't fit into samplesPerPixel
127+
let samplesToProcess = sampleBuffer.count / MemoryLayout<Int16>.size
128+
if samplesToProcess > 0 {
129+
let downSampledLength = 1
130+
let samplesPerPixel = samplesToProcess
131+
let filter = [Float](repeating: 1.0 / Float(samplesPerPixel), count: samplesPerPixel)
132+
133+
processSamples(fromData: &sampleBuffer,
134+
outputSamples: &outputSamples,
135+
samplesToProcess: samplesToProcess,
136+
downSampledLength: downSampledLength,
137+
samplesPerPixel: samplesPerPixel,
138+
filter: filter)
139+
}
140+
141+
guard reader.status == .completed || true else {
142+
fatalError("Couldn't read the audio file")
143+
}
144+
145+
return self.percentage(outputSamples)
146+
}
147+
148+
private func processSamples(fromData sampleBuffer: inout Data,
149+
outputSamples: inout [Float],
150+
samplesToProcess: Int,
151+
downSampledLength: Int,
152+
samplesPerPixel: Int,
153+
filter: [Float]) {
154+
sampleBuffer.withUnsafeBytes { (samples: UnsafePointer<Int16>) in
155+
var processingBuffer = [Float](repeating: 0.0, count: samplesToProcess)
156+
157+
let sampleCount = vDSP_Length(samplesToProcess)
158+
159+
// Convert 16bit int samples to floats
160+
vDSP_vflt16(samples, 1, &processingBuffer, 1, sampleCount)
161+
162+
// Take the absolute values to get amplitude
163+
vDSP_vabs(processingBuffer, 1, &processingBuffer, 1, sampleCount)
164+
165+
// Get the corresponding dB, and clip the results
166+
getdB(from: &processingBuffer)
167+
168+
// Downsample and average
169+
var downSampledData = [Float](repeating: 0.0, count: downSampledLength)
170+
vDSP_desamp(processingBuffer,
171+
vDSP_Stride(samplesPerPixel),
172+
filter, &downSampledData,
173+
vDSP_Length(downSampledLength),
174+
vDSP_Length(samplesPerPixel))
175+
176+
// Remove processed samples
177+
sampleBuffer.removeFirst(samplesToProcess * MemoryLayout<Int16>.size)
178+
179+
outputSamples += downSampledData
180+
}
181+
}
182+
183+
private func getdB(from normalizedSamples: inout [Float]) {
184+
// Convert samples to a log scale
185+
var zero: Float = 32768.0
186+
vDSP_vdbcon(normalizedSamples, 1, &zero, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count), 1)
187+
188+
// Clip to [noiseFloor, 0]
189+
var ceil: Float = 0.0
190+
var noiseFloorMutable: Float = -80.0 // TODO: CHANGE THIS VALUE
191+
vDSP_vclip(normalizedSamples, 1, &noiseFloorMutable, &ceil, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count))
192+
}
193+
194+
private func percentage(_ array: [Float]) -> [Float] {
195+
guard let firstElement = array.first else {
196+
return []
197+
}
198+
let absArray = array.map { abs($0) }
199+
let minValue = absArray.reduce(firstElement) { min($0, $1) }
200+
let maxValue = absArray.reduce(firstElement) { max($0, $1) }
201+
let delta = maxValue - minValue
202+
return absArray.map { abs(1 - (delta / ($0 - minValue))) }
203+
}
204+
}

0 commit comments

Comments
 (0)