Я хочу создать простое метрономное приложение с использованием AVAudioEngine со следующими функциями:
- Точное время (я знаю, я знаю, я должен использовать Audio Units, но я все еще борюсь с материалом Core Audio/обертками Obj-C и т. д.)
- Два разных звука на 1 и на 2/3/4 такта.
- Какая-то визуальная обратная связь (по крайней мере, отображение текущего ритма), которая должна быть синхронизирована со звуком.
Поэтому я создал два коротких звука щелчка (26 мс / 1150 сэмплов @ 16 бит / 44,1 кГц / стерео wav-файлы) и загрузил их в 2 буфера. Их длина будет установлена так, чтобы представлять один период.
Настройка моего пользовательского интерфейса проста: кнопка для переключения старт/пауза и метка для отображения текущего ритма (моя переменная счетчика).
При использовании свойства цикла scheduleBuffer синхронизация в порядке, но, поскольку мне нужно иметь 2 разных звука и способ синхронизации/обновления моего пользовательского интерфейса во время зацикливания кликов, я не могу использовать это. Вместо этого я решил использовать завершениеHandler, который перезапускает мою функцию playClickLoop() - см. мой код ниже.
К сожалению, при реализации этого я действительно не измерял точность времени. Теперь выясняется, что при установке скорости 120 ударов в минуту луп воспроизводится со скоростью всего около 117,5 ударов в минуту — довольно стабильно, но все же слишком медленно. Когда установлено значение 180 ударов в минуту, мое приложение воспроизводится со скоростью около 172,3 ударов в минуту.
Что тут происходит? Введена ли эта задержка с помощью завершенияHandler? Есть ли способ улучшить время? Или весь мой подход неверен?
Заранее спасибо! Алекс
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)
}
}
}
}
DispatchQueue.main.async
, вы отбрасываете все возможности определения точного времени. Теперь вы асинхронны, то есть сделайте это в какое-то время в будущем, и мне все равно, когда. - person matt   schedule 01.05.2021