gpt4 book ai didi

swift - 时序问题 : Metronome using AVAudioEngine scheduleBuffer's completion handler

转载 作者:行者123 更新时间:2023-12-04 07:43:10 26 4
gpt4 key购买 nike

我想使用具有以下功能的 AVAudioEngine 构建一个简单的节拍器应用程序:

  • 可靠的时间安排(我知道,我知道,我应该使用音频单元,但我仍在为核心音频内容/Obj-C 包装器等而苦苦挣扎)
  • 小节的“1”和节拍“2”/“3”/“4”有两种不同的声音。
  • 某种需要与音频同步的视觉反馈(至少是当前节拍的显示)。

  • 所以我创建了两个短点击声音(26ms/1150 个样本 @ 16 位/44,1 kHz/立体声 wav 文件)并将它们加载到 2 个缓冲区中。它们的长度将被设置为代表一个时期。
    我的 UI 设置很简单:一个用于切换开始/暂停的按钮和一个显示当前节拍的标签(我的“计数器”变量)。
    当使用 scheduleBuffer 的 loop 属性时,时间没问题,但是因为我需要有 2 种不同的声音和一种在循环点击时同步/更新我的 UI 的方法,所以我不能使用它。我想出使用 completionHandler 代替它重新启动我的 playClickLoop() 函数 - 请参阅下面附上的代码。
    不幸的是,在执行此操作时,我并没有真正衡量计时的准确性。现在事实证明,当将 bpm 设置为 120 时,它仅以大约 117.5 bpm 的速度播放循环 - 相当稳定,但仍然太慢。当 bpm 设置为 180 时,我的应用程序以大约 172,3 bpm 播放。
    这里发生了什么?这种延迟是通过使用 completionHandler 引入的吗?有什么办法可以改善时间?或者我的整个方法是错误的?
    提前致谢!
    亚历克斯
    import UIKit
    import AVFoundation

    class ViewController: UIViewController {

    private let engine = AVAudioEngine()
    private let player = AVAudioPlayerNode()

    private let fileName1 = "sound1.wav"
    private let fileName2 = "sound2.wav"
    private var file1: AVAudioFile! = nil
    private var file2: AVAudioFile! = nil
    private var buffer1: AVAudioPCMBuffer! = nil
    private var buffer2: AVAudioPCMBuffer! = nil

    private let sampleRate: Double = 44100

    private var bpm: Double = 180.0
    private var periodLengthInSamples: Double { 60.0 / bpm * sampleRate }
    private var counter: Int = 0

    private enum MetronomeState {case run; case stop}
    private var state: MetronomeState = .stop

    @IBOutlet weak var label: UILabel!

    override func viewDidLoad() {

    super.viewDidLoad()

    //
    // MARK: Loading buffer1
    //
    let path1 = Bundle.main.path(forResource: fileName1, ofType: nil)!
    let url1 = URL(fileURLWithPath: path1)
    do {file1 = try AVAudioFile(forReading: url1)
    buffer1 = AVAudioPCMBuffer(
    pcmFormat: file1.processingFormat,
    frameCapacity: AVAudioFrameCount(periodLengthInSamples))
    try file1.read(into: buffer1!)
    buffer1.frameLength = AVAudioFrameCount(periodLengthInSamples)
    } catch { print("Error loading buffer1 \(error)") }

    //
    // MARK: Loading buffer2
    //
    let path2 = Bundle.main.path(forResource: fileName2, ofType: nil)!
    let url2 = URL(fileURLWithPath: path2)
    do {file2 = try AVAudioFile(forReading: url2)
    buffer2 = AVAudioPCMBuffer(
    pcmFormat: file2.processingFormat,
    frameCapacity: AVAudioFrameCount(periodLengthInSamples))
    try file2.read(into: buffer2!)
    buffer2.frameLength = AVAudioFrameCount(periodLengthInSamples)
    } catch { print("Error loading buffer2 \(error)") }

    //
    // MARK: Configure + start engine
    //
    engine.attach(player)
    engine.connect(player, to: engine.mainMixerNode, format: file1.processingFormat)
    engine.prepare()
    do { try engine.start() } catch { print(error) }
    }

    //
    // MARK: Play / Pause toggle action
    //
    @IBAction func buttonPresed(_ sender: UIButton) {

    sender.isSelected = !sender.isSelected

    if player.isPlaying {
    state = .stop
    } else {
    state = .run

    try! engine.start()
    player.play()

    playClickLoop()
    }
    }

    private func playClickLoop() {

    //
    // MARK: Completion handler
    //
    let scheduleBufferCompletionHandler = { [unowned self] /*(_: AVAudioPlayerNodeCompletionCallbackType)*/ in

    DispatchQueue.main.async {

    switch state {

    case .run:
    self.playClickLoop()

    case .stop:
    engine.stop()
    player.stop()
    counter = 0
    }
    }
    }

    //
    // MARK: Schedule buffer + play
    //
    if engine.isRunning {

    counter += 1; if counter > 4 {counter = 1} // Counting from 1 to 4 only

    if counter == 1 {
    //
    // MARK: Playing sound1 on beat 1
    //
    player.scheduleBuffer(buffer1,
    at: nil,
    options: [.interruptsAtLoop],
    //completionCallbackType: .dataPlayedBack,
    completionHandler: scheduleBufferCompletionHandler)
    } else {
    //
    // MARK: Playing sound2 on beats 2, 3 & 4
    //
    player.scheduleBuffer(buffer2,
    at: nil,
    options: [.interruptsAtLoop],
    //completionCallbackType: .dataRendered,
    completionHandler: scheduleBufferCompletionHandler)
    }
    //
    // MARK: Display current beat on UILabel + to console
    //
    DispatchQueue.main.async {
    self.label.text = String(self.counter)
    print(self.counter)
    }
    }
    }
    }

    最佳答案

    正如 Phil Freihofner 上面所建议的,这是我自己问题的解决方案:
    我学到的最重要的一课:scheduleBuffer 命令提供的 completionHandler 回调没有足够早地调用,以在第一个缓冲区仍在播放时触发另一个缓冲区的重新调度。这将导致声音之间的(听不清)间隙并弄乱时间。必须已经有另一个缓冲区“保留”,即在当前缓冲区被调度之前已经被调度。
    考虑到完成回调的时间,使用 scheduleBuffer 的 completionCallbackType 参数并没有太大变化:当将其设置为 .dataRendered 或 .dataConsumed 时,回调已经来不及重新安排另一个缓冲区。使用 .dataPlayedback 只会让事情变得更糟:-)
    因此,为了实现无缝播放(使用正确的时间!)我只是激活了一个每个周期触发两次的计时器。所有奇数定时器事件将重新调度另一个缓冲区。
    有时解决方案是如此简单以至于令人尴尬......但有时你必须首先尝试几乎所有错误的方法才能找到它;-)
    我完整的工作解决方案(包括两个声音文件和 UI)可以在 GitHub 上找到:
    https://github.com/Alexander-Nagel/Metronome-using-AVAudioEngine

    import UIKit
    import AVFoundation

    private let DEBUGGING_OUTPUT = true

    class ViewController: UIViewController{

    private var engine = AVAudioEngine()
    private var player = AVAudioPlayerNode()
    private var mixer = AVAudioMixerNode()

    private let fileName1 = "sound1.wav"
    private let fileName2 = "sound2.wav"
    private var file1: AVAudioFile! = nil
    private var file2: AVAudioFile! = nil
    private var buffer1: AVAudioPCMBuffer! = nil
    private var buffer2: AVAudioPCMBuffer! = nil

    private let sampleRate: Double = 44100

    private var bpm: Double = 133.33
    private var periodLengthInSamples: Double {
    60.0 / bpm * sampleRate
    }
    private var timerEventCounter: Int = 1
    private var currentBeat: Int = 1
    private var timer: Timer! = nil

    private enum MetronomeState {case running; case stopped}
    private var state: MetronomeState = .stopped

    @IBOutlet weak var beatLabel: UILabel!
    @IBOutlet weak var bpmLabel: UILabel!
    @IBOutlet weak var playPauseButton: UIButton!

    override func viewDidLoad() {

    super.viewDidLoad()

    bpmLabel.text = "\(bpm) BPM"

    setupAudio()
    }

    private func setupAudio() {

    //
    // MARK: Loading buffer1
    //
    let path1 = Bundle.main.path(forResource: fileName1, ofType: nil)!
    let url1 = URL(fileURLWithPath: path1)
    do {file1 = try AVAudioFile(forReading: url1)
    buffer1 = AVAudioPCMBuffer(
    pcmFormat: file1.processingFormat,
    frameCapacity: AVAudioFrameCount(periodLengthInSamples))
    try file1.read(into: buffer1!)
    buffer1.frameLength = AVAudioFrameCount(periodLengthInSamples)
    } catch { print("Error loading buffer1 \(error)") }

    //
    // MARK: Loading buffer2
    //
    let path2 = Bundle.main.path(forResource: fileName2, ofType: nil)!
    let url2 = URL(fileURLWithPath: path2)
    do {file2 = try AVAudioFile(forReading: url2)
    buffer2 = AVAudioPCMBuffer(
    pcmFormat: file2.processingFormat,
    frameCapacity: AVAudioFrameCount(periodLengthInSamples))
    try file2.read(into: buffer2!)
    buffer2.frameLength = AVAudioFrameCount(periodLengthInSamples)
    } catch { print("Error loading buffer2 \(error)") }

    //
    // MARK: Configure + start engine
    //
    engine.attach(player)
    engine.connect(player, to: engine.mainMixerNode, format: file1.processingFormat)
    engine.prepare()
    do { try engine.start() } catch { print(error) }
    }

    //
    // MARK: Play / Pause toggle action
    //
    @IBAction func buttonPresed(_ sender: UIButton) {

    sender.isSelected = !sender.isSelected

    if state == .running {

    //
    // PAUSE: Stop timer and reset counters
    //
    state = .stopped

    timer.invalidate()

    timerEventCounter = 1
    currentBeat = 1

    } else {

    //
    // START: Pre-load first sound and start timer
    //
    state = .running

    scheduleFirstBuffer()

    startTimer()
    }
    }

    private func startTimer() {

    if DEBUGGING_OUTPUT {
    print("# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ")
    print()
    }

    //
    // Compute interval for 2 events per period and set up timer
    //
    let timerIntervallInSamples = 0.5 * self.periodLengthInSamples / sampleRate

    timer = Timer.scheduledTimer(withTimeInterval: timerIntervallInSamples, repeats: true) { timer in

    //
    // Only for debugging: Print counter values at start of timer event
    //
    // Values at begin of timer event
    if DEBUGGING_OUTPUT {
    print("timerEvent #\(self.timerEventCounter) at \(self.bpm) BPM")
    print("Entering \ttimerEventCounter: \(self.timerEventCounter) \tcurrentBeat: \(self.currentBeat) ")
    }

    //
    // Schedule next buffer at 1st, 3rd, 5th & 7th timerEvent
    //
    var bufferScheduled: String = "" // only needed for debugging / console output
    switch self.timerEventCounter {
    case 7:

    //
    // Schedule main sound
    //
    self.player.scheduleBuffer(self.buffer1, at:nil, options: [], completionHandler: nil)
    bufferScheduled = "buffer1"

    case 1, 3, 5:

    //
    // Schedule subdivision sound
    //
    self.player.scheduleBuffer(self.buffer2, at:nil, options: [], completionHandler: nil)
    bufferScheduled = "buffer2"

    default:
    bufferScheduled = ""
    }

    //
    // Display current beat & increase currentBeat (1...4) at 2nd, 4th, 6th & 8th timerEvent
    //
    if self.timerEventCounter % 2 == 0 {
    DispatchQueue.main.async {
    self.beatLabel.text = String(self.currentBeat)
    }
    self.currentBeat += 1; if self.currentBeat > 4 {self.currentBeat = 1}
    }

    //
    // Increase timerEventCounter, two events per beat.
    //
    self.timerEventCounter += 1; if self.timerEventCounter > 8 {self.timerEventCounter = 1}


    //
    // Only for debugging: Print counter values at end of timer event
    //
    if DEBUGGING_OUTPUT {
    print("Exiting \ttimerEventCounter: \(self.timerEventCounter) \tcurrentBeat: \(self.currentBeat) \tscheduling: \(bufferScheduled)")
    print()
    }
    }
    }

    private func scheduleFirstBuffer() {

    player.stop()

    //
    // pre-load accented main sound (for beat "1") before trigger starts
    //
    player.scheduleBuffer(buffer1, at: nil, options: [], completionHandler: nil)
    player.play()
    beatLabel.text = String(currentBeat)
    }
    }
    非常感谢大家的帮助!这是一个美妙的社区。
    亚历克斯

    关于swift - 时序问题 : Metronome using AVAudioEngine scheduleBuffer's completion handler,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67341908/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com