create - swift timer scheduledtimer example
Sistema de metrónomo sólido Swift (2)
¡De acuerdo! No puede hacer las cosas bien basándose en el tiempo, porque de alguna manera tenemos que lidiar con los convertidores DA y su frecuencia: frecuencia de muestreo. Necesitamos decirles la muestra exacta para comenzar a reproducir el sonido. Agregue una aplicación de iOS de vista única con dos botones de inicio y detención e inserte este código en ViewController.swift. Mantengo las cosas simples y es solo una Idea de cómo podemos hacer esto. Perdón por forzar intente ... Este es hecho con veloz 3. También vea mi proyecto en GitHub https://github.com/AlexShubin/MetronomeIdea
Swift 3
import UIKit
import AVFoundation
class Metronome {
var audioPlayerNode:AVAudioPlayerNode
var audioFile:AVAudioFile
var audioEngine:AVAudioEngine
init (fileURL: URL) {
audioFile = try! AVAudioFile(forReading: fileURL)
audioPlayerNode = AVAudioPlayerNode()
audioEngine = AVAudioEngine()
audioEngine.attach(self.audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)
try! audioEngine.start()
}
func generateBuffer(forBpm bpm: Int) -> AVAudioPCMBuffer {
audioFile.framePosition = 0
let periodLength = AVAudioFrameCount(audioFile.processingFormat.sampleRate * 60 / Double(bpm))
let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: periodLength)
try! audioFile.read(into: buffer)
buffer.frameLength = periodLength
return buffer
}
func play(bpm: Int) {
let buffer = generateBuffer(forBpm: bpm)
self.audioPlayerNode.play()
self.audioPlayerNode.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
}
func stop() {
audioPlayerNode.stop()
}
}
class ViewController: UIViewController {
var metronome:Metronome
required init?(coder aDecoder: NSCoder) {
let fileUrl = Bundle.main.url(forResource: "Click", withExtension: "wav")
metronome = Metronome(fileURL: fileUrl!)
super.init(coder: aDecoder)
}
@IBAction func StartPlayback(_ sender: Any) {
metronome.play(bpm: 120)
}
@IBAction func StopPlayback(_ sender: Any) {
metronome.stop()
}
}
Estoy intentando construir un sistema sólido confiable para construir un metrónomo en mi aplicación usando SWIFT.
He construido lo que parece ser un sistema sólido usando NSTimer hasta ahora. El único problema que estoy teniendo ahora es cuando el temporizador comienza los primeros 2 clics están fuera de tiempo, pero luego se captura en un marco de tiempo sólido.
Ahora, después de toda mi investigación, he visto a personas mencionar que debes usar otras herramientas de audio que no dependen de NSTimer. O si eliges usar NSTimer, entonces debería estar en su propio hilo. Ahora veo a muchos confundidos por esto Incluyéndome a mí y me gustaría llegar al fondo de este negocio de Metrónomo y resolverlo y compartirlo con todos aquellos que están luchando.
ACTUALIZAR
Así que lo he implementado y depurado en este punto después de los comentarios que recibí por última vez. En este punto, aquí está cómo está estructurado mi código. Su reproducción. Pero sigo recibiendo 2 clics rápidos al principio y luego se instala.
Me disculpo por mi ignorancia por esta. Espero estar en el camino correcto.
Actualmente estoy creando prototipos de otro método también. Donde tengo un archivo de audio muy pequeño con un clic y espacio muerto al final con la duración correcta hasta un punto de bucle para tempos específicos. Estoy devolviéndolo y funciona muy bien. Pero lo único es que no consigo detectar los puntos de bucle para las actualizaciones visuales, así que tengo mi NStimer básico simplemente detectando los intervalos de tiempo debajo del audio que se procesa y parece coincidir muy bien en todo momento y sin demora. Pero aún prefiero obtenerlo todo con este NSTimer. Si puede detectar fácilmente mi error, sería genial para una patada más en la dirección correcta y estoy seguro de que puede funcionar pronto. Muchas gracias.
//VARIABLES
//AUDIO
var clickPlayer:AVAudioPlayer = AVAudioPlayer()
let soundFileClick = NSBundle.mainBundle().pathForResource("metronomeClick", ofType: ".mp3")
//TIMERS
var metroTimer = NSTimer()
var nextTimer = NSTimer()
var previousClick = CFAbsoluteTimeGetCurrent() //When Metro Starts Last Click
//Metro Features
var isOn = false
var bpm = 60.0 //Tempo Used for beeps, calculated into time value
var barNoteValue = 4 //How Many Notes Per Bar (Set To Amount Of Hits Per Pattern)
var noteInBar = 0 //What Note You Are On In Bar
//********* FUNCTIONS ***********
func startMetro()
{
MetronomeCount()
barNoteValue = 4 // How Many Notes Per Bar (Set To Amount Of Hits Per Pattern)
noteInBar = 0 // What Note You Are On In Bar
isOn = true //
}
//Main Metro Pulse Timer
func MetronomeCount()
{
previousClick = CFAbsoluteTimeGetCurrent()
metroTimer = NSTimer.scheduledTimerWithTimeInterval(60.0 / bpm, target: self, selector: Selector ("MetroClick"), userInfo: nil, repeats: true)
nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)
}
func MetroClick()
{
tick(nextTimer)
}
func tick(timer:NSTimer)
{
let elapsedTime:CFAbsoluteTime = CFAbsoluteTimeGetCurrent() - previousClick
let targetTime:Double = 60/timer.userInfo!.objectForKey("bpm")!.doubleValue!
if (elapsedTime > targetTime) || (abs(elapsedTime - targetTime) < 0.003)
{
previousClick = CFAbsoluteTimeGetCurrent()
//Play the click here
if noteInBar == barNoteValue
{
clickPlayer.play() //Play Sound
noteInBar = 1
}
else//If We Are Still On Same Bar
{
clickPlayer.play() //Play Sound
noteInBar++ //Increase Note Value
}
countLabel.text = String(noteInBar) //Update UI Display To Show Note We Are At
}
}
Un metrónomo creado exclusivamente con NSTimer
no será muy preciso , como explica Apple en su documentación.
Debido a las diversas fuentes de entrada que maneja un típico ciclo de ejecución, la resolución efectiva del intervalo de tiempo para un temporizador se limita al orden de 50-100 milisegundos. Si el tiempo de disparo de un temporizador se produce durante una llamada prolongada o mientras el ciclo de ejecución está en un modo que no está supervisando el temporizador, el temporizador no se dispara hasta la próxima vez que el ciclo de ejecución comprueba el temporizador.
Yo sugeriría usar un NSTimer
que dispara en el orden de 50 veces por tick deseado (por ejemplo, si desea un ticks de 60 por minuto, tendría el NSTimeInterval
de aproximadamente 1/50 de segundo.
A continuación, debe almacenar CFAbsoluteTime
que almacena la CFAbsoluteTime
del "último tic" y compararla con la hora actual. Si el valor absoluto de la diferencia entre la hora actual y la hora del "último tic" es menor que cierta tolerancia (lo haría aproximadamente 4 veces el número de tics por intervalo, por ejemplo, si elige 1/50 de segundo por NSTimer fuego, debe aplicar una tolerancia de alrededor de 4/50 de segundo), puede ejecutar el "tic".
Es posible que necesite calibrar las tolerancias para obtener la precisión deseada, pero este concepto general hará que su metrónomo sea mucho más preciso.
Aquí hay más información sobre otra publicación SO . También incluye algún código que usa la teoría que discutí. ¡Espero que esto ayude!
Actualización La forma en que calcula sus tolerancias es incorrecta. En sus cálculos, observe que la tolerancia es inversamente proporcional al cuadrado de los bpm. El problema con esto es que la tolerancia eventualmente será menor que la cantidad de veces que el temporizador se dispara por segundo. Echa un vistazo a este gráfico para ver a qué me refiero. Esto generará problemas con altos BPM. La otra posible fuente de error es su condición de límite superior. Realmente no necesita verificar un límite superior en su tolerancia, porque en teoría, el temporizador ya debería haberse disparado para entonces. Por lo tanto, si el tiempo transcurrido es mayor que el tiempo teórico, puede disparar independientemente. (Por ejemplo, si el tiempo transcurrido es 0.1 s y el tiempo real con los BPM verdaderos debe ser 0.05 s, debe continuar y disparar el temporizador de todos modos, sin importar cuál sea su tolerancia).
Aquí está mi función de "tic" del temporizador, que parece funcionar bien. Debe ajustarlo para que se ajuste a sus necesidades (con los tiempos bajos, etc.) pero funciona en concepto.
func tick(timer:NSTimer) {
let elapsedTime:CFAbsoluteTime = CFAbsoluteTimeGetCurrent() - lastTick
let targetTime:Double = 60/timer.userInfo!.objectForKey("bpm")!.doubleValue!
if (elapsedTime > targetTime) || (abs(elapsedTime - targetTime) < 0.003) {
lastTick = CFAbsoluteTimeGetCurrent()
# Play the click here
}
}
Mi temporizador se inicializa así: nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)