2015-11-20 61 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() 

     } 

    }