import Accelerate import PromiseKit enum AudioUtilities { private static let noiseFloor: Float = -80 private struct FileInfo { let sampleCount: Int let asset: AVAsset let track: AVAssetTrack } enum Error : LocalizedError { case noAudioTrack case noAudioFormatDescription case loadingFailed case parsingFailed var errorDescription: String? { switch self { case .noAudioTrack: return "No audio track." case .noAudioFormatDescription: return "No audio format description." case .loadingFailed: return "Couldn't load asset." case .parsingFailed: return "Couldn't parse asset." } } } static func getVolumeSamples(for audioFileURL: URL, targetSampleCount: Int) -> Promise<[Float]> { return loadFile(audioFileURL).then { fileInfo in AudioUtilities.parseSamples(from: fileInfo, with: targetSampleCount) } } private static func loadFile(_ audioFileURL: URL, isRetry: Bool = false) -> Promise { let asset = AVURLAsset(url: audioFileURL) guard let track = asset.tracks(withMediaType: AVMediaType.audio).first else { if isRetry { return Promise(error: Error.loadingFailed) } else { // Workaround for issue where MP3 files sent by Android get saved as M4A var newAudioFileURL = audioFileURL.deletingPathExtension() let fileName = newAudioFileURL.lastPathComponent newAudioFileURL = newAudioFileURL.deletingLastPathComponent() newAudioFileURL = newAudioFileURL.appendingPathComponent("\(fileName).mp3") let fileManager = FileManager.default if fileManager.fileExists(atPath: newAudioFileURL.path) { return loadFile(newAudioFileURL, isRetry: true) } else { do { try FileManager.default.copyItem(at: audioFileURL, to: newAudioFileURL) } catch { return Promise(error: Error.loadingFailed) } return loadFile(newAudioFileURL, isRetry: true) } } } let (promise, seal) = Promise.pending() asset.loadValuesAsynchronously(forKeys: [ #keyPath(AVAsset.duration) ]) { var nsError: NSError? let status = asset.statusOfValue(forKey: #keyPath(AVAsset.duration), error: &nsError) switch status { case .loaded: guard let formatDescriptions = track.formatDescriptions as? [CMAudioFormatDescription], let audioFormatDescription = formatDescriptions.first, let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDescription) else { return seal.reject(Error.noAudioFormatDescription) } let sampleCount = Int((asbd.pointee.mSampleRate) * Float64(asset.duration.value) / Float64(asset.duration.timescale)) let fileInfo = FileInfo(sampleCount: sampleCount, asset: asset, track: track) seal.fulfill(fileInfo) default: print("Couldn't load asset due to error: \(nsError?.localizedDescription ?? "no description provided").") seal.reject(Error.loadingFailed) } } return promise } private static func parseSamples(from fileInfo: FileInfo, with targetSampleCount: Int) -> Promise<[Float]> { // Prepare the reader guard let reader = try? AVAssetReader(asset: fileInfo.asset) else { return Promise(error: Error.parsingFailed) } let range = 0..? CMBlockBufferGetDataPointer(readBuffer, atOffset: 0, lengthAtOffsetOut: &readBufferLength, totalLengthOut: nil, dataPointerOut: &readBufferPointer) sampleBuffer.append(UnsafeBufferPointer(start: readBufferPointer, count: readBufferLength)) CMSampleBufferInvalidate(readSampleBuffer) let sampleCount = sampleBuffer.count / MemoryLayout.size let downSampledLength = sampleCount / samplesPerPixel let samplesToProcess = downSampledLength * samplesPerPixel guard samplesToProcess > 0 else { continue } processSamples(from: &sampleBuffer, outputSamples: &result, samplesToProcess: samplesToProcess, downSampledLength: downSampledLength, samplesPerPixel: samplesPerPixel, filter: filter) } // Process any remaining samples let samplesToProcess = sampleBuffer.count / MemoryLayout.size if samplesToProcess > 0 { let downSampledLength = 1 let samplesPerPixel = samplesToProcess let filter = [Float](repeating: 1.0 / Float(samplesPerPixel), count: samplesPerPixel) processSamples(from: &sampleBuffer, outputSamples: &result, samplesToProcess: samplesToProcess, downSampledLength: downSampledLength, samplesPerPixel: samplesPerPixel, filter: filter) } guard reader.status == .completed else { return Promise(error: Error.parsingFailed) } // Return return Promise { $0.fulfill(result) } } private static func processSamples(from sampleBuffer: inout Data, outputSamples: inout [Float], samplesToProcess: Int, downSampledLength: Int, samplesPerPixel: Int, filter: [Float]) { sampleBuffer.withUnsafeBytes { (samples: UnsafeRawBufferPointer) in var processingBuffer = [Float](repeating: 0, count: samplesToProcess) let sampleCount = vDSP_Length(samplesToProcess) // Create an UnsafePointer from the samples let unsafeBufferPointer = samples.bindMemory(to: Int16.self) let unsafePointer = unsafeBufferPointer.baseAddress! // Convert 16 bit int samples to floats vDSP_vflt16(unsafePointer, 1, &processingBuffer, 1, sampleCount) // Take the absolute values to get the amplitude vDSP_vabs(processingBuffer, 1, &processingBuffer, 1, sampleCount) // Get the corresponding dB values and clip the results getdB(from: &processingBuffer) // Downsample and average var downSampledData = [Float](repeating: 0, count: downSampledLength) vDSP_desamp(processingBuffer, vDSP_Stride(samplesPerPixel), filter, &downSampledData, vDSP_Length(downSampledLength), vDSP_Length(samplesPerPixel)) // Remove the processed samples sampleBuffer.removeFirst(samplesToProcess * MemoryLayout.size) // Update the output samples outputSamples += downSampledData } } static func getdB(from normalizedSamples: inout [Float]) { // Convert samples to a log scale var zero: Float = 32768.0 vDSP_vdbcon(normalizedSamples, 1, &zero, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count), 1) // Clip to [noiseFloor, 0] var ceil: Float = 0.0 var noiseFloorMutable = AudioUtilities.noiseFloor vDSP_vclip(normalizedSamples, 1, &noiseFloorMutable, &ceil, &normalizedSamples, 1, vDSP_Length(normalizedSamples.count)) } }