2015-11-20 63 views
2

我想建立一個可靠的固態系統中使用SWIFT在我的應用程序建立一個節拍器。斯威夫特固體節拍器系統

我已經建立了使用NSTimer到目前爲止似乎是一個堅實的系統..我現在唯一的問題是當計時器開始第2次點擊是關閉時間,但隨後它捕捉到一個堅實的時間表。

現在我所有的研究後,我看到有人提到你應該使用其他音頻工具,不是靠的NSTimer ..或者,如果你選擇使用的NSTimer那麼就應該是在自己的線程。現在我看到很多人對此感到困惑,包括我自己,我很想深入這個節拍器業務的底部,並解決這個問題並與所有正在掙扎的人分享。

UPDATE

所以我已經實現,並在反饋我最後一次收到後,這一點清理。這裏是我的代碼的結構。它的播放。但我仍然得到一開始2次快速點擊,然後它在定居。

我對我的無知這一個道歉。我希望我走在正確的道路上。

我目前原型的另一種方法爲好。在哪裏我有一個非常小的音頻文件,只需點擊一下,死循環結束,並持續正確的持續時間,直到獲得特定節奏的循環點。我正在循環使用,效果很好。但唯一的事情是我沒有得到檢測迴路點視覺更新,所以我有我的基本的NSTimer只是檢測正在處理的音頻下的定時間隔,它似乎對決非常好整個無延遲。但我仍然寧願用這個NSTimer來得到它。如果您可以輕易發現我的錯誤,那麼我們可以在正確的方向上再踢一次,我相信它很快就會有效!非常感謝。

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

    } 

回答

5

NSTimer純粹內置節拍器會不會很準確,蘋果在其文檔中解釋道。

因爲一個典型的運行循環管理的各種輸入源,用於定時器的時間間隔的有效分辨率被限制爲50-100毫秒的數量級上。如果在長時間標註期間發生定時器的觸發時間,或者運行循環處於未監視定時器的模式下,定時器將在下次運行循環檢查定時器時才觸發。

我會建議使用NSTimer是50次,每次所需刻度的順序觸發(例如,如果您想每分鐘60個蜱,你將有NSTimeInterval是約1/50秒

然後,您應該存儲一個CFAbsoluteTime,它存儲「最後一次打勾」時間,並將其與當前時間進行比較。如果當前時間與「最後打勾」時間之間的差值的絕對值小於一些寬容(我會做這個大約4次,每次間隔蜱的數量,例如,如果你每次的NSTimer火災第二的選擇了1/50,你應該申請的第二個的4/50左右的誤差),你可以播放「剔號」。 「

您可能需要校準公差才能達到您想要的精度,但是這個一般概念會使您的節拍器更精確。

這是關於another SO post的更多信息。它還包含一些使用我所討論的理論的代碼。我希望這有幫助!

更新 您計算容差的方式不正確。在您的計算中,注意容差與bpm的平方成反比。問題在於容差最終會小於定時器每秒觸發的次數。看看this graph看看我的意思。這會在高BPM上產生問題。另一個潛在的錯誤來源是您的最高邊界條件。你真的不需要檢查你的容忍度的上限,因爲從理論上講,計時器應該已經開啓了。因此,如果經過時間大於理論時間,則無論如何都可以開火。 (例如,如果經過的時間是0.1s,並且真BPM的實際時間應該是0.05s,那麼無論您的容差是什麼,都應該繼續並啓動計時器。

這是我的計時器「打勾」功能,這似乎工作正常。你需要調整它以適應你的需求(與弱拍等),但它的概念。

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

我定時器初始化像這樣:nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)

+0

感謝您的創意。我很欣賞詳細的反饋。我會研究這一點,看看我能從這個建議中得出什麼。會及時向大家發佈。 – CakeGamesStudios

+0

嗯...所以我看着這似乎我被卡住....我已經設置它,並存儲我現在的時間點擊使用CFAbsoluteTime它將存儲時間就好了,但每次我存儲一個新的值來比較它也是通過當前時間進行的,所以總是有一個偏移量被加到存儲的時間上,用來比較前一次和當前時間以獲得我的容忍度。我花了相當長的時間試圖讓這個工作。我也仔細看過了這個例子,但仍然不清楚該怎麼做。任何進一步的解釋將不勝感激。謝謝! – CakeGamesStudios

+0

你能用CFAbsoluteTime發佈你的更新代碼嗎? – vigneshv

3

好!你不能根據時間把事情做好,因爲我們需要處理DA轉換器及其頻率 - 採樣率。我們需要告訴他們確切的樣本開始播放聲音。使用兩個按鈕添加單個視圖iOS應用程序啓動和停止並將此代碼插入ViewController.swift。我保持簡單,這只是我們如何做到這一點的想法。對不起,迫使嘗試...這是一個與SWIFT 3.做還檢查了我的項目在GitHub上https://github.com/AlexShubin/MetronomeIdea

斯威夫特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() 

     } 

    }