|
5 | 5 | // Created by Bastien Falcou on 12/6/16.
|
6 | 6 | //
|
7 | 7 |
|
| 8 | +import Accelerate |
8 | 9 | import AVFoundation
|
9 | 10 | import UIKit
|
10 | 11 |
|
@@ -150,6 +151,154 @@ public class AudioVisualizationView: BaseNibView {
|
150 | 151 | return self.meteringLevelsClusteredArray
|
151 | 152 | }
|
152 | 153 |
|
| 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 | + |
153 | 302 | // PRAGMA: - Play Mode Handling
|
154 | 303 |
|
155 | 304 | public func play(for duration: TimeInterval) {
|
@@ -207,22 +356,22 @@ public class AudioVisualizationView: BaseNibView {
|
207 | 356 | fatalError("trying to read audio visualization in write mode")
|
208 | 357 | }
|
209 | 358 |
|
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 |
217 | 368 |
|
| 369 | + print(self.meteringLevels) |
218 | 370 | guard self.meteringLevels != nil else {
|
219 | 371 | fatalError("trying to read audio visualization of non initialized sound record")
|
220 | 372 | }
|
221 | 373 |
|
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) |
226 | 375 | }
|
227 | 376 |
|
228 | 377 | // MARK: - Mask + Gradient
|
|
0 commit comments