1

I have a class PlayAudio to read an audio file and play. In PlayAudio, I have @objc updateUI function to add to CADisplayLink. I have another class Updater where I initialize and control isPaused of CADisplayLink. I've instantiated @Published var playAudio: PlayAudio so I can call it from View as updater.playAudio. My question is, although I can print playAudio.positionSliderValue real time in active CADisplayLink, playAudio.positionSliderValue does not update the UI in View. How can I achieve it? I want to activate and deActivate CADisplayLink from a separate class to maintain weak ownership (If I'm not mistaken...). When @State var volume is updated, volume slider also updates, so I think I'm successfully updating the value itself, but I can't figure it out that update to trigger updates in UI. Any thoughts or suggestions are appreciated. Thanks.

import SwiftUI
import AVKit

struct ContentView: View {
    
    @ObservedObject var updater = Updater()
    @State var volume = 0.0
 
    var body: some View {
        Text("\(volume)")
        VStack {
            Slider(value:
            // in order to get continuous value changes, I do this instead of $updater.playAudio.volumeSliderValue
                Binding(get: {
                    updater.playAudio.volumeSliderValue
                }, set: { (newValue) in
                    updater.playAudio.volumeSliderValue = newValue
                    updater.playAudio.setVolume()
                    volume = newValue
                    
                })
                , in: 0...1)
            Button(action: {
                updater.playAudio.play()
                // activate CADisplayLink
                updater.activate()
                // run CADisplayLink
                updater.updater?.isPaused = false
            }, label: {
                Text("Play File")
        })
            Slider(value:
                    // in order to get continuous value changes, I do this instead of $playAudio.positionSliderValue
                    Binding(get: {
                        updater.playAudio.positionSliderValue
                    }, set: { (newValue) in
                        updater.playAudio.positionSliderValue = newValue
                        updater.playAudio.seek()
                    })
                   , in: 0.0...updater.playAudio.positionSliderTotal) { _ in
                updater.playAudio.seek()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class Updater: ObservableObject {
    var updater: CADisplayLink?

    @Published var playAudio: PlayAudio
    
    init(){
        self.playAudio = PlayAudio()
        self.updater = CADisplayLink(target: playAudio, selector: #selector(playAudio.updateUI))
    }

    func activate() {
        self.updater?.add(to: .main, forMode: .default)
    }

    func deActivate() {
        self.updater?.invalidate()
    }
}

class PlayAudio: ObservableObject {
    var sampleRate = Double()
    var totalFrame = AVAudioFramePosition()
    var startTime = AVAudioTime()
    var newFramePosition = AVAudioFramePosition()
    
    let url = Bundle.main.urls(forResourcesWithExtension: "mp4", subdirectory: nil)?.first
    var audioFile = AVAudioFile()
    var engine = AVAudioEngine()
    var avAudioPlayerNode = AVAudioPlayerNode()
    @Published var volumeSliderValue: Double = 0.7
    @Published var positionSliderTotal: Double = 0.0
    @Published var positionSliderValue: Double = 0.0
    
    @objc func updateUI() {
        positionSliderValue = Double(currentFrame)
        // this prints ok, but I want it to update the UI in the View
        print(positionSliderValue)
    }
    
    init () {
        readFile()
        schedulePlayer()
        getTotalFrameDouble()
    }
    
    var currentFrame: AVAudioFramePosition {
        guard let lastRenderTime = avAudioPlayerNode.lastRenderTime,
              let playerTime = avAudioPlayerNode.playerTime(forNodeTime: lastRenderTime)
        else {
            return 0
        }
        return playerTime.sampleTime + newFramePosition
    }
    
    func getTotalFrameDouble() {
        positionSliderTotal = Double(totalFrame)
        print(positionSliderValue)
    }
    
    func readFile() {
        guard let url = url else {
            return
        }
        do {
            self.audioFile = try AVAudioFile(forReading: url)
        } catch let error {
            print(error)
        }
        self.sampleRate = audioFile.processingFormat.sampleRate
        self.totalFrame = audioFile.length
    }
    
    func setupEngine() {
        engine.attach(avAudioPlayerNode)
        engine.connect(avAudioPlayerNode, to: engine.mainMixerNode, format: audioFile.processingFormat)
        engine.prepare()
        do {
            try engine.start()
        } catch let error {
            print(error)
        }
    }
    
    func schedulePlayer() {
        newFramePosition = 0
        engine.reset()
        setupEngine()
        avAudioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
    }
    
    func play() {
        let outputFormat = avAudioPlayerNode.outputFormat(forBus: AVAudioNodeBus(0))
        let lastRenderTime = avAudioPlayerNode.lastRenderTime?.sampleTime ?? 0
        // need to convert from AVAudioFramePosition to AVAudioTime
        startTime = AVAudioTime(sampleTime: AVAudioFramePosition(Double(lastRenderTime)), atRate: Double(outputFormat.sampleRate))
        avAudioPlayerNode.play(at: startTime)
    }
    
    func seek() {
        // player time (needs to be converted to player node time
        newFramePosition = AVAudioFramePosition(positionSliderValue)
        let framesToPlay = totalFrame - newFramePosition
        avAudioPlayerNode.stop()
        
        if framesToPlay > 100 {
            avAudioPlayerNode.scheduleSegment(audioFile, startingFrame: newFramePosition, frameCount: AVAudioFrameCount(framesToPlay), at: nil, completionHandler: nil)
        }
        play()
    }
    
    func setVolume() {
        avAudioPlayerNode.volume = Float(volumeSliderValue)
    }
}

deekay
  • 35
  • 6
  • 1
    ObservedObject observers only one level properties, and does not hierarchy of internal other ObservableObject. You need to separate sub-views dependent on playAudio and observe it inside those subviews explicitly. – Asperi Dec 17 '20 at 11:58
  • Thank you for the information! I didn't know ObservedObject only observes only one level. I will modify it and see how I can improve. – deekay Dec 17 '20 at 12:33

0 Answers0