AVAudio и автономный ручной режим рендеринга - не может записывать буферы более высоких частот в выходной файл

Я читаю входной файл и в автономном режиме ручного рендеринга хочу выполнить амплитудную модуляцию и записать результат в выходной файл.

Ради тестирования я генерирую чистые синусоидальные волны - это хорошо работает для частот ниже 6000 Гц. Для более высоких частот (моя цель состоит в том, чтобы работать примерно с 20 000 Гц), сигнал (таким образом, прослушивание выходного файла) искажается, и спектр заканчивается на 8 000 Гц - больше нет чистого спектра с множеством пиков между 0 и 8 000 Гц.

Вот мой фрагмент кода:

    let outputFile: AVAudioFile

    do {
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let outputURL = documentsURL.appendingPathComponent("output.caf")
        outputFile = try AVAudioFile(forWriting: outputURL, settings: sourceFile.fileFormat.settings)
    } catch {
        fatalError("Unable to open output audio file: \(error).")
    }

    var sampleTime: Float32 = 0

    while engine.manualRenderingSampleTime < sourceFile.length {
        do {
            let frameCount = sourceFile.length - engine.manualRenderingSampleTime
            let framesToRender = min(AVAudioFrameCount(frameCount), buffer.frameCapacity)
            
            let status = try engine.renderOffline(framesToRender, to: buffer)
            
            switch status {
            
            case .success:
                // The data rendered successfully. Write it to the output file.
                let sampleRate:Float = Float((mixer.outputFormat(forBus: 0).sampleRate))

                let modulationFrequency: Float = 20000.0
                
                for i in stride(from:0, to: Int(buffer.frameLength), by: 1) {
                    let val = sinf(2.0 * .pi * modulationFrequency * Float(sampleTime) / Float(sampleRate))
                    // TODO: perform modulation later
                    buffer.floatChannelData?.pointee[Int(i)] = val
                    sampleTime = sampleTime + 1.0
                }

                try outputFile.write(from: buffer)
                
            case .insufficientDataFromInputNode:
                // Applicable only when using the input node as one of the sources.
                break
                
            case .cannotDoInCurrentContext:
                // The engine couldn't render in the current render call.
                // Retry in the next iteration.
                break
                
            case .error:
                // An error occurred while rendering the audio.
                fatalError("The manual rendering failed.")
            @unknown default:
                fatalError("unknown error")
            }
        } catch {
            fatalError("The manual rendering failed: \(error).")
        }
    }

Мой вопрос: есть ли s.th. не так с моим кодом? Или у кого-нибудь есть идеи, как создавать выходные файлы с синусоидальными волнами более высоких частот?

Я полагаю, что ручной режим рендеринга недостаточно быстр, чтобы иметь дело с более высокими частотами.


Обновление: тем временем я проанализировал выходной файл с помощью Audacity. Выше представлена ​​форма волны 1.000 Гц, ниже - частота 20.000 Гц:  введите описание изображения здесь

Когда я увеличиваю масштаб, я вижу следующее:  введите описание изображения здесь

Сравнивая спектры двух выходных файлов, я получаю следующее:  введите описание изображения здесь

Странно, что с более высокими частотами амплитуда стремится к нулю. Вдобавок я вижу больше частот во втором спектре.

Новый вопрос в связи с результатом - правильность следующего алгоритма:

// Process the audio in `renderBuffer` here
for i in 0..<Int(renderBuffer.frameLength) {
    let val = sinf(1000.0*Float(index) *2 * .pi / Float(sampleRate))
    renderBuffer.floatChannelData?.pointee[i] = val
    index += 1
}

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

Обновление 2:

Я изменил настройки следующим образом:

    settings[AVFormatIDKey] = kAudioFormatAppleLossless
    settings[AVAudioFileTypeKey] = kAudioFileCAFType
    settings[AVSampleRateKey] = readBuffer.format.sampleRate
    settings[AVNumberOfChannelsKey] = 1
    settings[AVLinearPCMIsFloatKey] = (readBuffer.format.commonFormat == .pcmFormatInt32)
    settings[AVSampleRateConverterAudioQualityKey] = AVAudioQuality.max
    settings[AVLinearPCMBitDepthKey] = 32
    settings[AVEncoderAudioQualityKey] = AVAudioQuality.max

Теперь качество выходного сигнала лучше, но не идеально. Я получаю более высокие амплитуды, но всегда более одной частоты в анализаторе спектра. Может быть, обходной путь может заключаться в применении фильтра высоких частот?

Тем временем я работал с чем-то вроде SignalGenerator, передавая управляемый буфер (с синусоидальными волнами) прямо на громкоговоритель - в этом случае выход идеален. Я думаю, что маршрутизация сигнала в файл вызывает такие проблемы.


person Ulrich Vormbrock    schedule 20.11.2020    source источник


Ответы (1)


Скорость ручного режима рендеринга не является проблемой, поскольку скорость в контексте ручного рендеринга не имеет значения.

Вот скелетный код для ручного рендеринга из исходного файла в выходной файл:

// Open the input file
let file = try! AVAudioFile(forReading: URL(fileURLWithPath: "/tmp/test.wav"))

let engine = AVAudioEngine()
let player = AVAudioPlayerNode()

engine.attach(player)

engine.connect(player, to:engine.mainMixerNode, format: nil)

// Run the engine in manual rendering mode using chunks of 512 frames
let renderSize: AVAudioFrameCount = 512

// Use the file's processing format as the rendering format
let renderFormat = AVAudioFormat(commonFormat: file.processingFormat.commonFormat, sampleRate: file.processingFormat.sampleRate, channels: file.processingFormat.channelCount, interleaved: true)!
let renderBuffer = AVAudioPCMBuffer(pcmFormat: renderFormat, frameCapacity: renderSize)!

try! engine.enableManualRenderingMode(.offline, format: renderFormat, maximumFrameCount: renderBuffer.frameCapacity)

try! engine.start()
player.play()

// The render format is also the output format
let output = try! AVAudioFile(forWriting: URL(fileURLWithPath: "/tmp/foo.wav"), settings: renderFormat.settings, commonFormat: renderFormat.commonFormat, interleaved: renderFormat.isInterleaved)

// Read using a buffer sized to produce `renderSize` frames of output
let readBuffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: renderSize)!

// Process the file
while true {
    do {
        // Processing is finished if all frames have been read
        if file.framePosition == file.length {
            break
        }

        try file.read(into: readBuffer)
        player.scheduleBuffer(readBuffer, completionHandler: nil)

        let result = try engine.renderOffline(readBuffer.frameLength, to: renderBuffer)

        // Process the audio in `renderBuffer` here

        // Write the audio
        try output.write(from: renderBuffer)
        if result != .success {
            break
        }
    }
    catch {
        break
    }
}

player.stop()
engine.stop()

Вот фрагмент, показывающий, как установить одинаковую частоту дискретизации для всего движка:

// Replace:
//engine.connect(player, to:engine.mainMixerNode, format: nil)

// With:
let busFormat = AVAudioFormat(standardFormatWithSampleRate: file.fileFormat.sampleRate, channels: file.fileFormat.channelCount)

engine.disconnectNodeInput(engine.outputNode, bus: 0)
engine.connect(engine.mainMixerNode, to: engine.outputNode, format: busFormat)

engine.connect(player, to:engine.mainMixerNode, format: busFormat)

Убедитесь, что частота дискретизации одинакова во всем:

NSLog("%@", engine)
________ GraphDescription ________
AVAudioEngineGraph 0x7f8194905af0: initialized = 0, running = 0, number of nodes = 3

     ******** output chain ********

     node 0x600001db9500 {'auou' 'ahal' 'appl'}, 'U'
         inputs = 1
             (bus0, en1) <- (bus0) 0x600001d80b80, {'aumx' 'mcmx' 'appl'}, [ 2 ch,  48000 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

     node 0x600001d80b80 {'aumx' 'mcmx' 'appl'}, 'U'
         inputs = 1
             (bus0, en1) <- (bus0) 0x600000fa0200, {'augn' 'sspl' 'appl'}, [ 2 ch,  48000 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
         outputs = 1
             (bus0, en1) -> (bus0) 0x600001db9500, {'auou' 'ahal' 'appl'}, [ 2 ch,  48000 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

     node 0x600000fa0200 {'augn' 'sspl' 'appl'}, 'U'
         outputs = 1
             (bus0, en1) -> (bus0) 0x600001d80b80, {'aumx' 'mcmx' 'appl'}, [ 2 ch,  48000 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
______________________________________
person sbooth    schedule 21.11.2020
comment
Спасибо за подсказку и за фрагмент кода! К сожалению, у меня такая же проблема. Скажем: я заполняю выходной буфер чистой синусоидой около 20 000 Гц, спектр (например, экспорт выходного файла в Audacity) показывает много строк не только с частотой 20 000 Гц, но и ниже. К сожалению, я не могу вставить снимок экрана со спектром, но поведение следующее: если взять горизонтальную ось за ось времени, количество промежуточных пиков (обозначающих частоту) постепенно увеличивается со временем. кроме того, со временем сигнал становится громче. - person Ulrich Vormbrock; 22.11.2020
comment
пожалуйста, взгляните на мое обновление выше: с более высокими частотами у меня как-то возникают проблемы с перекрытиями - person Ulrich Vormbrock; 22.11.2020
comment
Возможно, вам потребуется настроить AVAudioEngine для использования желаемой частоты дискретизации повсюду, специально установив форматы шины, в противном случае SRC может возникать за кулисами. - person sbooth; 22.11.2020
comment
Я так и сделал, но на это не повлияло: engine.connect (player, to: engine.mainMixerNode, format: AVAudioFormat.init (standardFormatWithSampleRate: sampleRate, каналы: 1)) - person Ulrich Vormbrock; 22.11.2020
comment
Чтобы использовать одинаковую частоту дискретизации повсюду, вам необходимо отключить главный микшер от выхода. Вы можете распечатать хорошее резюме движка для отладки, используя NSLog и %@ - person sbooth; 22.11.2020
comment
извините - отключите главный микшер от выхода - не понимаю. Не могли бы вы написать фрагмент кода? - person Ulrich Vormbrock; 22.11.2020
comment
Спасибо за ваш фрагмент! К сожалению, выходной сигнал (в выходном файле) все равно остается искаженным :-( - person Ulrich Vormbrock; 22.11.2020