Как воспроизводить несколько звуков из буфера одновременно с использованием узлов, подключенных к микшеру AVAudioEngine

Я делаю базовое музыкальное приложение для 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()

    }

Каждый звук должен воспроизводиться, не прерывая другие звуки, прерывая только свой собственный звук при повторном воспроизведении звуков. Однако, несмотря на настройку нескольких буферов и проигрывателей и назначение каждого из них на его собственную шину на микшере audioEngine, воспроизведение одного звука по-прежнему останавливает воспроизведение любых других звуков.

Кроме того, хотя исключение .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()

Это все еще заставляет меня выяснять, как воспроизводить эти звуки одновременно, поскольку воспроизведение другого звука все равно остановит текущий воспроизводимый звук.

Изменить 2: я нашел решение своей проблемы. Мой ответ ниже.


person Squeakychu    schedule 04.08.2019    source источник


Ответы (1)


Оказалось, что наличие опции .interrupt не было проблемой (на самом деле, это оказался лучший способ перезапустить звук, который воспроизводился по моему опыту, поскольку во время перезапуска не было заметной паузы, в отличие от stop () функция). Фактическая проблема, которая препятствовала одновременному воспроизведению нескольких звуков, заключалась в этой конкретной строке кода.

// One AVAudioPlayerNode per note
    var audioFilePlayer: [AVAudioPlayerNode] = Array(repeating: AVAudioPlayerNode(), count: 7)

Здесь произошло то, что каждому элементу массива было присвоено одно и то же значение AVAudioPlayerNode, поэтому все они фактически использовали один и тот же AVAudioPlayerNode. В результате функции AVAudioPlayerNode воздействовали на все элементы в массиве, а не только на указанный элемент. Чтобы исправить это и присвоить каждому элементу другое значение AVAudioPlayerNode, я изменил указанную выше строку так, чтобы она начиналась как пустой массив типа AVAudioPlayerNode.

// One AVAudioPlayerNode per note
    var audioFilePlayer = [AVAudioPlayerNode]()

Затем я добавил новую строку, чтобы добавить к этому массиву новый AVAudioPlayerNode в начале внутри второго цикла for функции viewDidLoad ().

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

Это дало каждому элементу в массиве другое значение AVAudioPlayerNode. Воспроизведение звука или перезапуск звука больше не прерывают другие воспроизводимые в данный момент звуки. Теперь я могу играть любую из нот одновременно и без какой-либо заметной задержки между нажатием ноты и воспроизведением.

person Squeakychu    schedule 04.08.2019
comment
Помните, что в Swift экземпляры классов имеют ссылочную семантику (структуры и перечисления имеют семантику значений)! - person Nicolas Miari; 01.05.2020
comment
Пользовательский интерфейс зависает, когда больше аудиофайлов. для i в 0 ... 31 {} - person Ishwar Hingu; 07.05.2021