mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			191 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			191 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Swift
		
	
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<FileInfo> {
 | 
						|
        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<FileInfo>.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..<fileInfo.sampleCount
 | 
						|
        reader.timeRange = CMTimeRange(start: CMTime(value: Int64(range.lowerBound), timescale: fileInfo.asset.duration.timescale),
 | 
						|
            duration: CMTime(value: Int64(range.count), timescale: fileInfo.asset.duration.timescale))
 | 
						|
        let outputSettings: [String:Any] = [
 | 
						|
            AVFormatIDKey : Int(kAudioFormatLinearPCM),
 | 
						|
            AVLinearPCMBitDepthKey : 16,
 | 
						|
            AVLinearPCMIsBigEndianKey : false,
 | 
						|
            AVLinearPCMIsFloatKey : false,
 | 
						|
            AVLinearPCMIsNonInterleaved : false
 | 
						|
        ]
 | 
						|
        let output = AVAssetReaderTrackOutput(track: fileInfo.track, outputSettings: outputSettings)
 | 
						|
        output.alwaysCopiesSampleData = false
 | 
						|
        reader.add(output)
 | 
						|
        var channelCount = 1
 | 
						|
        let formatDescriptions = fileInfo.track.formatDescriptions as! [CMAudioFormatDescription]
 | 
						|
        for audioFormatDescription in formatDescriptions {
 | 
						|
            guard let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(audioFormatDescription) else {
 | 
						|
                return Promise(error: Error.parsingFailed)
 | 
						|
            }
 | 
						|
            channelCount = Int(asbd.pointee.mChannelsPerFrame)
 | 
						|
        }
 | 
						|
        let samplesPerPixel = max(1, channelCount * range.count / targetSampleCount)
 | 
						|
        let filter = [Float](repeating: 1 / Float(samplesPerPixel), count: samplesPerPixel)
 | 
						|
        var result = [Float]()
 | 
						|
        var sampleBuffer = Data()
 | 
						|
        // Read the file
 | 
						|
        reader.startReading()
 | 
						|
        defer { reader.cancelReading() }
 | 
						|
        while reader.status == .reading {
 | 
						|
            guard let readSampleBuffer = output.copyNextSampleBuffer(),
 | 
						|
                let readBuffer = CMSampleBufferGetDataBuffer(readSampleBuffer) else { break }
 | 
						|
            var readBufferLength = 0
 | 
						|
            var readBufferPointer: UnsafeMutablePointer<Int8>?
 | 
						|
            CMBlockBufferGetDataPointer(readBuffer,
 | 
						|
                                        atOffset: 0,
 | 
						|
                                        lengthAtOffsetOut: &readBufferLength,
 | 
						|
                                        totalLengthOut: nil,
 | 
						|
                                        dataPointerOut: &readBufferPointer)
 | 
						|
            sampleBuffer.append(UnsafeBufferPointer(start: readBufferPointer, count: readBufferLength))
 | 
						|
            CMSampleBufferInvalidate(readSampleBuffer)
 | 
						|
            let sampleCount = sampleBuffer.count / MemoryLayout<Int16>.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<Int16>.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<Int16> 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<Int16>.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))
 | 
						|
    }
 | 
						|
}
 |