Синхронизируйте AVAudioPlayerNode и начало записи AVAudioEngine

Я использую AVAudioEngine как для воспроизведения, так и для записи звука. В моем случае мне нужно воспроизводить звук именно тогда, когда я начинаю запись звука. В настоящее время моя запись, кажется, начинается до того, как воспроизводится звук. Как сделать так, чтобы звук и запись начинались одновременно? В идеале я бы хотел, чтобы запись начиналась и звук воспроизводился одновременно, вместо того, чтобы синхронизировать их при постобработке.

Вот мой код на данный момент:

class Recorder {
  enum RecordingState {
    case recording, paused, stopped
  }
  
  private var engine: AVAudioEngine!
  private var mixerNode: AVAudioMixerNode!
  private var state: RecordingState = .stopped
    
    

  private var audioPlayer = AVAudioPlayerNode()
  
  init() {
    setupSession()
    setupEngine()
    
  }
    
    
  fileprivate func setupSession() {
      let session = AVAudioSession.sharedInstance()
    try? session.setCategory(.playAndRecord, options: [.mixWithOthers, .defaultToSpeaker])
      try? session.setActive(true, options: .notifyOthersOnDeactivation)
   }
    
    fileprivate func setupEngine() {
      engine = AVAudioEngine()
      mixerNode = AVAudioMixerNode()

      // Set volume to 0 to avoid audio feedback while recording.
      mixerNode.volume = 0

      engine.attach(mixerNode)

    engine.attach(audioPlayer)
        
      makeConnections()

      // Prepare the engine in advance, in order for the system to allocate the necessary resources.
      engine.prepare()
    }

    
    fileprivate func makeConnections() {
       
      let inputNode = engine.inputNode
      let inputFormat = inputNode.outputFormat(forBus: 0)
        print("Input Sample Rate: \(inputFormat.sampleRate)")
      engine.connect(inputNode, to: mixerNode, format: inputFormat)
      
      let mainMixerNode = engine.mainMixerNode
      let mixerFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: inputFormat.sampleRate, channels: 1, interleaved: false)
    
      engine.connect(mixerNode, to: mainMixerNode, format: mixerFormat)

      let path = Bundle.main.path(forResource: "effect1.wav", ofType:nil)!
      let url = URL(fileURLWithPath: path)
      let file = try! AVAudioFile(forReading: url)
      audioPlayer.scheduleFile(file, at: nil)
      engine.connect(audioPlayer, to: mainMixerNode, format: nil)
        
        }
    
    
    //MARK: Start Recording Function
    func startRecording() throws {
        print("Start Recording!")
      let tapNode: AVAudioNode = mixerNode
      let format = tapNode.outputFormat(forBus: 0)

      let documentURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        
      // AVAudioFile uses the Core Audio Format (CAF) to write to disk.
      // So we're using the caf file extension.
        let file = try AVAudioFile(forWriting: documentURL.appendingPathComponent("recording.caf"), settings: format.settings)
       
      tapNode.installTap(onBus: 0, bufferSize: 4096, format: format, block: {
        (buffer, time) in

        try? file.write(from: buffer)
        print(buffer.description)
        print(buffer.stride)
       
        //Do Stuff
        print("Doing Stuff")
      })
    
      
      try engine.start()
        audioPlayer.play()
      state = .recording
    }
    
    
    //MARK: Other recording functions
    func resumeRecording() throws {
      try engine.start()
      state = .recording
    }

    func pauseRecording() {
      engine.pause()
      state = .paused
    }

    func stopRecording() {
      // Remove existing taps on nodes
      mixerNode.removeTap(onBus: 0)
      
      engine.stop()
      state = .stopped
    }
    

    
    
}

person coder    schedule 01.04.2021    source источник
comment
К сожалению (и, возможно, удивительно), я не думаю, что есть точный способ определить задержку без ее измерения.   -  person sbooth    schedule 06.04.2021
comment
@sbooth Как мне измерить задержку?   -  person coder    schedule 06.04.2021
comment
Идеальный способ - это использовать петлевой кабель от выхода устройства к его входу. Вы отправляете тестовый сигнал на выход, регистрируете время его отправки, а затем обнаруживаете тот же сигнал на входе. Разница во времени - это задержка приема-передачи. Есть способы оценить с AVAudioEngine, но в моих экспериментах ни один из них не является точным по выборке.   -  person sbooth    schedule 06.04.2021
comment
Хммм ... Я мог бы это сделать, но что будет, если пользователь уменьшит громкость? Это затруднит обнаружение выходного сигнала. Если у вас есть способ оценить задержку приема-передачи с помощью AVAudioEngine, поделитесь им в качестве ответа. Оценка лучше, чем ничего!   -  person coder    schedule 07.04.2021
comment
Некоторое обсуждение есть на stackoverflow.com/questions/65600996/   -  person sbooth    schedule 08.04.2021


Ответы (1)


Вы пробовали запускать плеер перед установкой крана?

// Stop the player to be sure the engine.start calls the prepare function
audioPlayer.stop()
try engine.start()
audioPlayer.play()
state = .recording

tapNode.installTap(onBus: 0, bufferSize: 4096, format: format, block: {
        (buffer, time) in
        try? file.write(from: buffer)
      })

Если в этом случае ваша запись немного позже, возможно, попробуйте компенсировать это с помощью player.outputPresentationLatency. Согласно доку, это максимальное значение. То есть время могло быть немного хуже. Надеюсь, стоит попробовать.

print(player.outputPresentationLatency)
// 0.009999999776482582

let nanoseconds = Int(player.outputPresentationLatency * pow(10,9))
let dispatchTimeInterval = DispatchTimeInterval.nanoseconds(nanoseconds)
            
player.play()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + dispatchTimeInterval) {
    self.installTap()
    self.state = .recording
}
person Moose    schedule 05.04.2021
comment
Когда я пытаюсь реализовать ваше решение, я получаю сообщение об ошибке 'Value of type 'AVAudioPlayerNode' has no member 'prepareToPlay'. Помните, что audioPlayer имеет тип AVAudioPlayerNode. - person coder; 06.04.2021
comment
Кроме того, задержка должна быть как можно меньше, так как я могу использовать количество сэмплов для расчета времени, в которое определенные звуки или паттерны появляются в моем аудио. - person coder; 06.04.2021
comment
Ой. Прости. AudioEngine.prepare делает то же самое. Я поправляю свой ответ. Я попробую несколько тестов перед ответом, потому что странно, что функции подготовки недостаточно. Может быть, использовать обратные вызовы - person Moose; 06.04.2021
comment
Я отредактировал свой ответ (после кофе). Я был бы очень заинтересован результатом, потому что я работаю над приложением, которому, вероятно, понадобится эта функция и точность. К сожалению, сейчас у меня нет времени проводить эксперименты. Пожалуйста держите меня в курсе. ;) - person Moose; 06.04.2021
comment
Последние несколько дней я был очень занят, поэтому у меня не было возможности провести много тестов. Однако мои первоначальные результаты показывают, что запуск плеера до касания практически ничего не дает. Я почти уверен, что пробовал это раньше. Мне по какой-то причине пришлось удалить строку audioPlayer.stop(), чтобы звук заработал. Если вы знаете, почему он это делает, дайте мне знать. Что касается задержки, я еще не изучал этого, но первоначальное тестирование показывает, что outputPresentationLatency не единственный виновник проблемы. Я скоро вернусь с другими :). - person coder; 07.04.2021