Search code examples
iosswiftaudiofiltersignal-processing

Data conversions from audio data to TPCircularBuffer and audio unit to filter Heart beat sound


In my iOS application, using swift

i am getting audio from digital health device (BLE). In the form of audio data, to hear heart beats.

  1. We have ADPCM decoded file. That we have decoded using ADPCM decoder.

ADPCM Decoded file

We want to remove the background noise from it.

2. For that, we apply low pass filter and high pass filter using apple's DSP code. We are getting float samples, after after filtering.

So, first question is Conversion of Data to float is correct ?

Apple's DSP Code

      let samples = convertUInt8ToFloats(sampleData: audioData)
      let samples1 =  musicProvider.loadAudioSamples(samples: samples)    


func convertUInt8ToFloats(sampleData: Data) -> [Float] {
        
        let factor = Float(UInt8.max)
        let samples = sampleData.withUnsafeBytes {
            UnsafeBufferPointer<UInt8>(start: $0, count: sampleData.count / MemoryLayout<UInt8>.size)
        }
        var floats: [Float] = Array(repeating: 0.0, count: samples.count)
        for i in 0..<samples.count {
            floats[i] = Float(samples[i]) / factor
        }
        return floats
    }

In Apple's sample code they are loading assets like this, But i can not load assets like this, as i have raw audio file, is above convertUInt8ToFloats conversion same as below ?

    let asset = AVAsset(url: path.absoluteURL)
    
    let reader = try AVAssetReader(asset: asset)
    
    guard let track = try await asset.load(.tracks).first else {
        return nil
    }
    
    let outputSettings: [String: Int] = [
        AVFormatIDKey: Int(kAudioFormatLinearPCM),
        AVNumberOfChannelsKey: 1,
        AVLinearPCMIsBigEndianKey: 0,
        AVLinearPCMIsFloatKey: 1,
        AVLinearPCMBitDepthKey: 32,
        AVLinearPCMIsNonInterleaved: 1
    ]
    
    let output = AVAssetReaderTrackOutput(track: track,
                                          outputSettings: outputSettings)
    
    reader.add(output)
    reader.startReading()
    
    var samplesData = [Float]()
    
    while reader.status == .reading {
        if
            let sampleBuffer = output.copyNextSampleBuffer(),
            let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) {
            
            let bufferLength = CMBlockBufferGetDataLength(dataBuffer)
            let count = bufferLength / 4
            
            let data = [Float](unsafeUninitializedCapacity: count) {
                buffer, initializedCount in
                
                CMBlockBufferCopyDataBytes(dataBuffer,
                                           atOffset: 0,
                                           dataLength: bufferLength,
                                           destination: buffer.baseAddress!)
                
                initializedCount = count
            }
            
            samplesData.append(contentsOf: data)
        }
    }

Solution

  • Actually i tried to play with ffplay -f s16le -ar 16000 -ac 1 -i input2 this format, and i got good sound of heart beat. the format should be s16le.

    This is all the information needed. You need to convert Data to SInt16 (i.e. s16le), and then SInt16 to Float.

    Using the Apple sample code, this would be:

    static func getAudioSamples(forResource: String,
                                withExtension: String) async throws -> (naturalTimeScale: CMTimeScale,
                                                                        data: [Float])? {
        
        guard let path = Bundle.main.url(forResource: forResource,
                                         withExtension: withExtension) else {
            return nil
        }
    
        // Load raw data
        let rawData = try Data(contentsOf: path)
    
        // Convert raw data into [S16Int] as little-endian
        let sIntLEData = rawData.withUnsafeBytes {
            Array($0.bindMemory(to: Int16.self)).map(Int16.init(littleEndian:))
        }
    
        // Scale [SInt16] to [Float] in the range [-1, 1]
        let samplesData = sIntLEData.map { Float($0) / Float(Int16.max) }
    
        // Set the sample-rate
        let naturalTimeScale = CMTimeScale(16_000)
    
        // Return as expected by caller.
        return (naturalTimeScale: naturalTimeScale,
                data: samplesData)
    }
    

    You can then apply filters as in Apple's sample code.

    To reverse the process, multiply rather than divide. You'll want to operate in the Int space and clamp to avoid crashing in cases where your algorithm might stray slightly outside the strict [-1, 1] range (which sometimes happens).

    // This is safe if floatsArray has already been scrubbed to only
    // "reasonable" values. It will crash if there are values outside
    // the range of Int, infinity, or NaN.
    floatsArray.map { Int16(clamping: Int($0 * Float(Int16.max))) }
    

    If your algorithm may emit outrageous values outside the range of Int, or infinity or NaN, you can do some more work to deal with those values and avoid crashing, but this is going to create noise in your output, and you should really just make sure the input is normalized before converting.

    Here's an example of a way to make it safe from crashes, however, if you want it:

    // This is safe for any Float values, but it may be extremely noisy
    // if the values are out of range.
    let sInt16Array = floatsArray.map {
        let scaled = $0 * Float(Int16.max)
        let intValue = Int(exactly: scaled.rounded(.towardZero)) ?? 0
        return Int16(clamping: intValue)
    }