Squ*_*chu 6 avfoundation ios swift avaudioengine avaudioplayernode
我正在为 iOS 制作一个基本的音乐应用程序,按下音符会播放相应的声音。我正在尝试让存储在缓冲区中的多个声音以最小的延迟同时播放。但是,我每次只能播放一种声音。
我最初使用多个 AVAudioPlayer 对象设置声音,为每个播放器分配一个声音。虽然它确实同时播放多个声音,但它似乎无法同时启动两个声音(似乎它会在第一个声音启动后稍微延迟第二个声音)。此外,如果我以非常快的速度按下音符,引擎似乎无法跟上,并且在我按下后面的音符后,后面的声音就会很好地开始。
我正在尝试解决这个问题,从我所做的研究来看,使用 AVAudioEngine 播放声音似乎是最好的方法,我可以在缓冲区数组中设置声音,然后让它们播放从这些缓冲区中。
class ViewController: UIViewController
{
// Main Audio Engine and it's corresponding mixer
var audioEngine: AVAudioEngine = AVAudioEngine()
var mainMixer = AVAudioMixerNode()
// One AVAudioPlayerNode per note
var audioFilePlayer: [AVAudioPlayerNode] = Array(repeating: AVAudioPlayerNode(), count: 7)
// Array of filepaths
let noteFilePath: [String] = [
Bundle.main.path(forResource: "note1", ofType: "wav")!,
Bundle.main.path(forResource: "note2", ofType: "wav")!,
Bundle.main.path(forResource: "note3", ofType: "wav")!]
// Array to store the note URLs
var noteFileURL = [URL]()
// One audio file per note
var noteAudioFile = [AVAudioFile]()
// One audio buffer per note
var noteAudioFileBuffer = [AVAudioPCMBuffer]()
override func viewDidLoad()
{
super.viewDidLoad()
do
{
// For each note, read the note URL into an AVAudioFile,
// setup the AVAudioPCMBuffer using data read from the file,
// and read the AVAudioFile into the corresponding buffer
for i in 0...2
{
noteFileURL.append(URL(fileURLWithPath: noteFilePath[i]))
// Read the corresponding url into the audio file
try noteAudioFile.append(AVAudioFile(forReading: noteFileURL[i]))
// Read data from the audio file, and store it in the correct buffer
let noteAudioFormat = noteAudioFile[i].processingFormat
let noteAudioFrameCount = UInt32(noteAudioFile[i].length)
noteAudioFileBuffer.append(AVAudioPCMBuffer(pcmFormat: noteAudioFormat, frameCapacity: noteAudioFrameCount)!)
// Read the audio file into the buffer
try noteAudioFile[i].read(into: noteAudioFileBuffer[i])
}
mainMixer = audioEngine.mainMixerNode
// For each note, attach the corresponding node to the audioEngine, and connect the node to the audioEngine's mixer.
for i in 0...2
{
audioEngine.attach(audioFilePlayer[i])
audioEngine.connect(audioFilePlayer[i], to: mainMixer, fromBus: 0, toBus: i, format: noteAudioFileBuffer[i].format)
}
// Start the audio engine
try audioEngine.start()
// Setup the audio session to play sound in the app, and activate the audio session
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.soloAmbient)
try AVAudioSession.sharedInstance().setMode(AVAudioSession.Mode.default)
try AVAudioSession.sharedInstance().setActive(true)
}
catch let error
{
print(error.localizedDescription)
}
}
func playSound(senderTag: Int)
{
let sound: Int = senderTag - 1
// Set up the corresponding audio player to play its sound.
audioFilePlayer[sound].scheduleBuffer(noteAudioFileBuffer[sound], at: nil, options: .interrupts, completionHandler: nil)
audioFilePlayer[sound].play()
}
Run Code Online (Sandbox Code Playgroud)
每个声音的播放不应中断其他声音,仅在再次播放声音时中断其自身的声音。然而,尽管设置了多个缓冲区和播放器,并将每个缓冲区和播放器分配给音频引擎混音器上自己的总线,但播放一种声音仍然会阻止任何其他声音的播放。
此外,虽然省略 .interrupts 确实可以防止声音停止其他声音,但在当前正在播放的声音完成之前,这些声音不会播放。这意味着,如果我播放note1,然后播放note2,然后播放note3,则note1将播放,而note2仅在note1结束后播放,而note3仅在note2结束后播放。
编辑:我能够让audioFilePlayer再次重置到开头,而无需在playSound函数中使用以下代码使用中断。
if audioFilePlayer[sound].isPlaying == true
{
audioFilePlayer[sound].stop()
}
audioFilePlayer[sound].scheduleBuffer(noteAudioFileBuffer[sound], at: nil, completionHandler: nil)
audioFilePlayer[sound].play()
Run Code Online (Sandbox Code Playgroud)
这仍然让我弄清楚如何同时播放这些声音,因为播放另一个声音仍然会停止当前播放的声音。
编辑2:我找到了问题的解决方案。我的回答如下。
事实证明,使用 .interrupt 选项并不是问题(事实上,根据我的经验,这实际上是重新启动正在播放的声音的最佳方式,因为重新启动期间没有明显的暂停,与停止()函数)。阻止多个声音同时播放的实际问题是这行特定的代码。
// One AVAudioPlayerNode per note
var audioFilePlayer: [AVAudioPlayerNode] = Array(repeating: AVAudioPlayerNode(), count: 7)
Run Code Online (Sandbox Code Playgroud)
这里发生的情况是,数组的每个项目都被分配了完全相同的 AVAudioPlayerNode 值,因此它们都有效地共享相同的 AVAudioPlayerNode。因此,AVAudioPlayerNode 函数影响数组中的所有项目,而不仅仅是指定的项目。为了解决这个问题并给每个项目一个不同的 AVAudioPlayerNode 值,我最终更改了上面的行,使其以 AVAudioPlayerNode 类型的空数组开始。
// One AVAudioPlayerNode per note
var audioFilePlayer = [AVAudioPlayerNode]()
Run Code Online (Sandbox Code Playgroud)
然后,我添加了一个新行,在 viewDidLoad() 函数的第二个 for 循环内部的开头添加一个新的 AVAudioPlayerNode 到该数组。
// For each note, attach the corresponding node to the audioEngine, and connect the node to the audioEngine's mixer.
for i in 0...6
{
audioFilePlayer.append(AVAudioPlayerNode())
// audioEngine code
}
Run Code Online (Sandbox Code Playgroud)
这为数组中的每个项目提供了不同的 AVAudioPlayerNode 值。播放声音或重新启动声音不再中断当前正在播放的其他声音。我现在可以同时演奏任何音符,并且在按下音符和播放之间没有任何明显的延迟。