How to create a SoundCloud like waveform in Swift 3

 

The first thing you need to get is the array of floats from the audio file and we’re going to use AVFoundation for that.

import AVFoundation

create a struct outside the class to store the audio information.

struct ReadFile {
    static var arrayFloatValues:[Float] = []
    static var points:[CGFloat] = []

}

-Add the audio file you want to analyze to the project navigator
-Add this code in ViewDidLoad()
Note: Make sure to change the forResource and withExtension parameters.

override func viewDidLoad() {
        super.viewDidLoad()

        //Look for sample2.m4a audio file
        let url = Bundle.main.url(forResource: "sample2", withExtension: "m4a")
        let file = try! AVAudioFile(forReading: url!)//Read File into AVAudioFile
        let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: file.fileFormat.sampleRate, channels: file.fileFormat.channelCount, interleaved: false)//Format of the file

        let buf = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: UInt32(file.length))//Buffer
        try! file.read(into: buf)//Read Floats
        //Store the array of floats in the struct
         readFile.arrayFloatValues = Array(UnsafeBufferPointer(start: buf.floatChannelData?[0], count:Int(buf.frameLength)))

    }

-Create new swift file and name it DrawWaveform
-subclass UIView and import UIKit and Accelerate frameworks

import UIKit
import Accelerate

class DrawWaveform: UIView {

    //This is where we're going to draw the waveform
    override func draw(_ rect: CGRect) {

     }

}

But before drawing we need to downsample the array of floats, because we don’t need to draw ALL of the floats.
To do this we’re going to use Accelerate framework

-create a new function in the new swift file ‘convertToPoints()’

-Copy this code inside the function convertToPoints(). I will explain this code in detail in another post, but it just reduces the size of the array and converts it to CGFloat.


func convertToPoints() {
        var processingBuffer = [Float](repeating: 0.0,
                                       count: Int(readFile.arrayFloatValues.count))
        let sampleCount = vDSP_Length(readFile.arrayFloatValues.count)
        //print(sampleCount)
        vDSP_vabs(readFile.arrayFloatValues, 1, &processingBuffer, 1, sampleCount);
        // print(processingBuffer)

        //THIS IS OPTIONAL
        // convert do dB
        //    var zero:Float = 1;
        //    vDSP_vdbcon(floatArrPtr, 1, &zero, floatArrPtr, 1, sampleCount, 1);
        //    //print(floatArr)
        //
        //    // clip to [noiseFloor, 0]
        //    var noiseFloor:Float = -50.0
        //    var ceil:Float = 0.0
        //    vDSP_vclip(floatArrPtr, 1, &noiseFloor, &ceil,
        //                   floatArrPtr, 1, sampleCount);
        //print(floatArr)

        var multiplier = 1.0
        print(multiplier)
        if multiplier < 1{
            multiplier = 1.0

        }

        let samplesPerPixel = Int(150 * multiplier)
        let filter = [Float](repeating: 1.0 / Float(samplesPerPixel),
                             count: Int(samplesPerPixel))
        let downSampledLength = Int(readFile.arrayFloatValues.count / samplesPerPixel)
        var downSampledData = [Float](repeating:0.0,
                                      count:downSampledLength)
        vDSP_desamp(processingBuffer,
                    vDSP_Stride(samplesPerPixel),
                    filter, &downSampledData,
                    vDSP_Length(downSampledLength),
                    vDSP_Length(samplesPerPixel))

        // print(" DOWNSAMPLEDDATA: \(downSampledData.count)")

        //convert [Float] to [CGFloat] array
        readFile.points = downSampledData.map{CGFloat($0)}

    }

    

Now we’re going to draw the waveform with the downsampled array.
copy this code inside the draw function in the subview.

This uses UIBezierPath and loops thru the array of floats to draw and at the apply stroke to fill it with color!

Where x is the distance between squares and y is the amplitude of the square.

    override func draw(_ rect: CGRect) {

 //downsample and convert to [CGFloat]
        self.convertToPoints()

        var f = 0
        //the waveform on top
        let aPath = UIBezierPath()
        //the waveform on the bottom
        let aPath2 = UIBezierPath()

        //lineWidth
        aPath.lineWidth = 2.0
        aPath2.lineWidth = 2.0

        //start drawing at:
        aPath.move(to: CGPoint(x:0.0 , y:rect.height/2 ))
        aPath2.move(to: CGPoint(x:0.0 , y:rect.height ))

        //Loop the array
        for _ in readFile.points{
                //Distance between points
                var x:CGFloat = 2.5
                //next location to draw
                aPath.move(to: CGPoint(x:aPath.currentPoint.x + x , y:aPath.currentPoint.y ))

                //y is the amplitude of each square
                aPath.addLine(to: CGPoint(x:aPath.currentPoint.x  , y:aPath.currentPoint.y - (readFile.points[f] * 70) - 1.0))

                aPath.close()

                x += 1
                f += 1
        }

        //If you want to stroke it with a Orange color
        UIColor.orange.set()
        aPath.stroke()
        //If you want to fill it as well
        aPath.fill()

        f = 0
        aPath2.move(to: CGPoint(x:0.0 , y:rect.height/2 ))

        //Reflection of waveform
        for _ in readFile.points{
            var x:CGFloat = 2.5
            aPath2.move(to: CGPoint(x:aPath2.currentPoint.x + x , y:aPath2.currentPoint.y ))

            //y is the amplitude of each square
            aPath2.addLine(to: CGPoint(x:aPath2.currentPoint.x  , y:aPath2.currentPoint.y - ((-1.0 * readFile.points[f]) * 50)))

            // aPath.close()
            aPath2.close()

            //print(aPath.currentPoint.x)
            x += 1
            f += 1
        }

        //If you want to stroke it with a Orange color
        UIColor.orange.set()
        //Reflection and make it transparent
        aPath2.stroke(with: CGBlendMode.normal, alpha: 0.5)

        //If you want to fill it as well
        aPath2.fill()

}
 

Next step is go to the storyboard and create a UIVIEW and change the class to ‘DrawWaveform’

Build and Run.

Screen Shot 2017-03-13 at 8.38.38 PM

Here’s the Github Link

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s