tutorial Système de métronome solide Swift




timer.scheduledtimer swift 4 (2)

J'essaye de construire un système solide fiable pour construire un métronome dans mon application en utilisant SWIFT.

J'ai construit ce qui semble être un système solide en utilisant NSTimer jusqu'à présent. Le seul problème que j'ai en ce moment est lorsque le chronomètre démarre les 2 premiers clics sont hors de temps, mais alors il entre dans une période solide.

Maintenant, après toutes mes recherches, j'ai vu des gens mentionner que vous devriez utiliser d'autres outils audio ne reposant pas sur NSTimer .. Ou si vous choisissez d'utiliser NSTimer alors il devrait être sur son propre thread. Maintenant, je vois beaucoup de gens confus par cela. Y compris moi-même et j'aimerai aller au fond de cette affaire de Metronome et obtenir ceci résolu et le partager avec tous ceux qui luttent.

METTRE À JOUR

J'ai donc mis en place et nettoyé à ce stade après les commentaires que j'avais reçu la dernière fois. À ce stade, voici comment mon code est structuré. C'est en train de jouer. Mais je reçois toujours 2 clics rapides au début et ensuite il s'installe.

Je m'excuse de mon ignorance pour celui-ci. J'espère que je suis sur le bon chemin.

Je suis actuellement en train de prototyper une autre méthode. Où j'ai un très petit fichier audio avec un clic et un espace mort à la fin de celui-ci avec la durée correcte jusqu'à ce qu'un point de boucle pour des tempos spécifiques. Je suis en boucle et fonctionne très bien. Mais la seule chose est que je ne parviens pas à détecter les points de boucle pour les mises à jour visuelles, donc mon NStimer de base ne détecte que les intervalles de synchronisation sous l'audio en cours de traitement et semble très bien correspondre sans retard. Mais je préférerais tout obtenir avec ce NSTimer. Si vous pouvez facilement repérer mon erreur serait génial pour un autre coup de pied dans la bonne direction et je suis sûr que cela peut fonctionner bientôt! Merci beaucoup.

    //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
        }

    }

D'accord! Vous ne pouvez pas faire les choses correctement en fonction du temps, car nous devons en quelque sorte nous occuper des convertisseurs DA et de leur fréquence d'échantillonnage. Nous devons leur dire l'échantillon exact pour commencer à jouer le son. Ajoutez une application iOS avec deux boutons Démarrer et Arrêter et insérez ce code dans ViewController.swift. Je garde les choses simples et c'est juste une idée de comment nous pouvons faire cela. Désolé de forcer essayer ... Celui-ci est fait avec swift 3. Consultez aussi mon projet sur 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()

        }

    }

Un métronome construit uniquement avec NSTimer ne sera pas très précis , comme l'explique Apple dans sa documentation.

En raison des diverses sources d'entrée qu'une boucle d'exécution typique gère, la résolution effective de l'intervalle de temps pour une minuterie est limitée à l'ordre de 50 à 100 millisecondes. Si le temps de déclenchement d'une minuterie se produit pendant une longue alerte ou lorsque la boucle d'exécution est dans un mode qui ne surveille pas la minuterie, la minuterie ne se déclenche pas jusqu'à la prochaine fois que la boucle d'exécution vérifie le minuteur.

Je suggérerais d'utiliser un NSTimer qui se déclenche de l'ordre de 50 fois par tick souhaité (par exemple, si vous voulez 60 ticks par minute, le NSTimeInterval être d'environ 1/50 de seconde.

Vous devez ensuite stocker un CFAbsoluteTime qui stocke le "dernier tick" heure, et le comparer à l'heure actuelle. Si la valeur absolue de la différence entre l'heure actuelle et la "dernière tick" est inférieure à une certaine tolérance (je ferais cela environ 4 fois le nombre de ticks par intervalle, par exemple, si vous avez choisi 1/50 de seconde par NSTimer feu, vous devez appliquer une tolérance d'environ 4/50 de seconde), vous pouvez jouer le "tick".

Vous devrez peut-être calibrer les tolérances pour obtenir la précision désirée, mais ce concept général rendra votre métronome beaucoup plus précis.

Voici quelques informations supplémentaires sur un autre post SO . Il comprend également du code qui utilise la théorie dont j'ai parlé. J'espère que ça aide!

Mise à jour La façon dont vous calculez vos tolérances est incorrecte. Dans vos calculs, notez que la tolérance est inversement proportionnelle au carré du bpm. Le problème avec ceci est que la tolérance sera finalement inférieure au nombre de fois que la minuterie se déclenche par seconde. Jetez un oeil à ce graphique pour voir ce que je veux dire. Cela générera des problèmes à des BPM élevés. L'autre source potentielle d'erreur est votre condition de limite supérieure. Vous n'avez vraiment pas besoin de vérifier une limite supérieure de votre tolérance, car théoriquement, le temporisateur aurait déjà dû être déclenché. Par conséquent, si le temps écoulé est supérieur à la durée théorique, vous pouvez le déclencher indépendamment. (Par exemple, si le temps écoulé est de 0.1s et que le temps réel avec le vrai BPM devrait être de 0.05s, vous devriez aller de l'avant et tirer le timer de toute façon, peu importe votre tolérance).

Voici ma minuterie "tick" fonction, qui semble fonctionner correctement. Vous devez l'adapter à vos besoins (avec les temps forts, etc.) mais cela fonctionne dans le concept.

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
    }
}

Mon timer est initialisé comme ceci: nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)





nstimer