You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用AudioKit 5.6导出/保存移调后的音频文件?

问题描述

我正在处理一个.m4a格式的音频文件,需要为其添加移调效果,现在希望将处理后的音频导出保存到本地,但不知如何操作。我尝试用renderToFile方法,但无法使其正常工作,以下是我目前的代码实现:

import AudioKit
import SoundpipeAudioKit
import AudioKitEX
import AudioToolbox
import AVFoundation

struct TunerData {
    var pitch: Float = 0.0
    var amplitude: Float = 0.0
    var noteNameWithSharps = "-"
    var noteNameWithFlats = "-"
}

class Conductor {
    let engine = AudioEngine()
    var player: AudioPlayer!
    var pitchShifter: PitchShifter!
    var tracker: PitchTap!
    var mixer: Mixer!
    private var musicTrackPlayer: AVPlayer?
    
    var data = TunerData()
    
    let noteFrequencies = [16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87]
    let noteNamesWithSharps = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]
    let noteNamesWithFlats = ["C", "D♭", "D", "E♭", "E", "F", "G♭", "G", "A♭", "A", "B♭", "B"]
    
    var pitch: Float = 0

    init() {
        print("Initializing Conductor")
        do {
            player = AudioPlayer()
            player.isLooping = true
            
            pitchShifter = PitchShifter(player)
            
            mixer = Mixer(Fader(player), Fader(pitchShifter))
            
            engine.output = mixer
            
            try engine.start()
            startFrequencyTracker()
        } catch {
            print("catch-error: ", error.localizedDescription)
            fatalError()
        }
    }
}

// MARK: - PitchTap
extension Conductor {
    private func startFrequencyTracker() {
        tracker = PitchTap(pitchShifter) { pitch, amp in
            DispatchQueue.main.async {
                self.update(pitch[0], amp[0])
            }
        }
        
        tracker.start()
    }
    
    private func initializeMusicTrackPlayer(url: URL) {
        musicTrackPlayer = .init(url: url)
        musicTrackPlayer?.play()
    }
    
    private func update(_ pitch: AUValue, _ amp: AUValue) {
        // Reduces sensitivity to background noise to prevent random / fluctuating data.
        guard amp > 0.1 else {
            data.pitch = 0
            data.amplitude = 0
            data.noteNameWithFlats = "-"
            data.noteNameWithSharps = "-"
            return
        }
        
        data.pitch = pitch
        data.amplitude = amp
        
        var frequency = pitch
        while frequency > Float(noteFrequencies[noteFrequencies.count - 1]) {
            frequency /= 2.0
        }
        while frequency < Float(noteFrequencies[0]) {
            frequency *= 2.0
        }
        
        var minDistance: Float = 10000.0
        var index = 0
        
        for possibleIndex in 0 ..< noteFrequencies.count {
            let distance = fabsf(Float(noteFrequencies[possibleIndex]) - frequency)
            if distance < minDistance {
                index = possibleIndex
                minDistance = distance
            }
        }
        let octave = Int(log2f(pitch / frequency))
        data.noteNameWithSharps = "\(noteNamesWithSharps[index])\(octave)"
        data.noteNameWithFlats = "\(noteNamesWithFlats[index])\(octave)"
        
        print("pitch", data.pitch, "noteNameWithFlats", self.data.noteNameWithFlats, "frequency", frequency)
        
        let targetFrequency = Float(noteFrequencies[8])
        let pitchShiftRatio = pitch / targetFrequency
        // Apply pitch shift
//        pitchShifter.shift = pitchShiftRatio
    }
    
    private func stopFrequencyTracker() {
        tracker.stop()
    }
}
解决方案

要实现音频导出,你需要调整现有代码,补充音频加载、导出逻辑,同时处理引擎状态与文件权限:

1. 关键代码修改

给Conductor添加音频加载和导出的扩展方法:

extension Conductor {
    // 加载目标.m4a文件到AudioPlayer
    func loadAudioFile(from url: URL) throws {
        let audioFile = try AVAudioFile(forReading: url)
        player = AudioPlayer(file: audioFile)
        // 重建音频处理链
        pitchShifter = PitchShifter(player)
        // 如果只需要导出移调后的音频,移除原音混音;需要混合则保留原mixer逻辑
        mixer = Mixer(Fader(pitchShifter))
        engine.output = mixer
    }
    
    // 导出处理后的音频到指定路径
    func exportProcessedAudio(to outputURL: URL, completion: @escaping (Error?) -> Void) {
        guard let player = player else {
            completion(NSError(domain: "Conductor", code: -1, userInfo: [NSLocalizedDescriptionKey: "未加载音频文件"]))
            return
        }
        
        // 停止实时播放与音高追踪
        tracker.stop()
        engine.stop()
        
        do {
            let audioFormat = player.file.processingFormat
            let audioDuration = player.file.length / audioFormat.sampleRate
            
            // 执行音频渲染
            let renderedFile = try renderToFile(outputURL,
                                              duration: audioDuration,
                                              format: audioFormat,
                                              render: {
                player.play()
                // 等待播放完成以完成渲染
                while player.isPlaying {
                    usleep(10000)
                }
            })
            
            print("导出完成:\(renderedFile.url.path)")
            completion(nil)
        } catch {
            print("导出失败:\(error.localizedDescription)")
            completion(error)
        } finally {
            // 可选:重启引擎恢复实时功能
            try? engine.start()
            startFrequencyTracker()
        }
    }
}

2. 使用示例

在业务逻辑中调用加载与导出方法:

let conductor = Conductor()
// 替换为你的输入音频路径(示例为Bundle内文件)
guard let inputURL = Bundle.main.url(forResource: "source", withExtension: "m4a") else {
    fatalError("未找到输入音频文件")
}
// 输出路径设为Documents目录
let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let outputURL = documentsDir.appendingPathComponent("shifted_audio.m4a")

do {
    try conductor.loadAudioFile(from: inputURL)
    // 设置移调参数,示例为升1个全音(比例值对应半音数)
    conductor.pitchShifter.shift = 1.122462
    // 执行导出
    conductor.exportProcessedAudio(to: outputURL) { error in
        if let error = error {
            print("导出出错:\(error)")
        } else {
            print("文件已保存至:\(outputURL.path)")
        }
    }
} catch {
    print("加载音频失败:\(error)")
}

注意事项

  • 确保App拥有磁盘写入权限,需在Info.plist中添加NSDocumentsFolderUsageDescription说明用途。
  • 若需要导出原音与移调音频的混合版本,保持原代码中mixer = Mixer(Fader(player), Fader(pitchShifter))的逻辑即可。
  • renderToFile要求音频引擎处于停止状态,因此导出前需停止引擎,完成后可按需重启恢复实时功能。

内容的提问来源于stack exchange,提问作者Sotiris Kaniras

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 00:17:33