Как преобразовать текст в речь на iOS с помощью SDK?

Я пытаюсь передать потоковое аудио, полученное из Speech SDK, с помощью SPXPushAudioOutputStream. Я получаю все данные без проблем и могу записать их в формат wav или mp3, а затем воспроизвести с помощью приведенного ниже кода.

struct ContentView: View {
    @State private var inputText = """
    Die Gesundheitspolitik bleibt ein hartes Pflaster für Reformen. Bundesrätin Elisabeth Baume-Schneider forderte alle Akteure am Sonntag «nachdrücklich» auf, ihren Teil der Verantwortung zu übernehmen und «konkrete, mehrheitsfähige Sparvorschläge» vorzulegen. Mit Blick auf die vergangenen Jahrzehnte kann man darüber nur schmunzeln.
    Solange besagte Akteure ihren Besitzstand eisern verteidigen und solange die politischen Kräfte aus allen Lagern ihrem Lobbydruck nachgeben, wird sich nichts ändern. Auch in den Kantonen überwiegen die Hemmungen, Spitäler zu schliessen und über die Grenzen hinweg die Zusammenarbeit zu verstärken. Ausnahmen bestätigen die Regel.
    Das sagen die Ökonomen
    Deshalb stellt sich die Frage, ob man nicht das zunehmend absurde Kopfprämiensystem abschaffen und auf ein durch Steuergelder finanziertes Gesundheitswesen umstellen sollte, wie in anderen Ländern. watson hat diese Frage den Gesundheitsökonomen Heinz Locher und Willy Oggier gestellt – und interessante Antworten erhalten.
    """
    @State private var resultText = ""
    @State private var isPlaying = false
    @State private var audioPlayer: AVAudioPlayer?
    @State private var synthesisCompleted = false
    
    let speechKey = "censored"
    let serviceRegion = "switzerlandnorth"
    
    var body: some View {
        VStack {
            TextField("Enter text to synthesize", text: $inputText)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button(action: synthesisToPushAudioOutputStream) {
                Text("Synthesize Speech")
            }
            .padding()
            
            Button(action: playAudio) {
                Text(isPlaying ? "Stop" : "Play")
            }
            .padding()
            .disabled(!synthesisCompleted)
            
            Text(resultText)
                .padding()
        }
        .onChange(of: resultText) { newValue in
            debug("Result text changed to: \(newValue)", function: "body.onChange")
            synthesisCompleted = newValue.contains("Speech synthesis completed")
            debug("Synthesis completed: \(synthesisCompleted)", function: "body.onChange")
        }
    }
    
    private func synthesisToPushAudioOutputStream() {
        let startTime = Date()
        debug("Starting speech synthesis...", function: #function)
        let filePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("pushStream.mp3")
        debug("File path: \(filePath.path)", function: #function)
        
        if !FileManager.default.fileExists(atPath: filePath.path) {
            debug("File doesn't exist. Creating new file...", function: #function)
            FileManager.default.createFile(atPath: filePath.path, contents: nil, attributes: nil)
        } else {
            debug("File already exists. Will overwrite.", function: #function)
        }
        
        guard let fileHandle = try? FileHandle(forWritingTo: filePath) else {
            debug("Failed to open file handle", function: #function)
            updateResultText("Failed to open file at \(filePath.path)")
            return
        }
        debug("File handle opened successfully", function: #function)
        
        var totalBytesWritten: UInt = 0
        let stream = SPXPushAudioOutputStream(writeHandler: { data -> UInt in
            fileHandle.write(data)
            totalBytesWritten += UInt(data.count)
            debug("Wrote \(data.count) bytes. Total: \(totalBytesWritten) bytes", function: "SPXPushAudioOutputStream.writeHandler")
            return UInt(data.count)
        }, closeHandler: {
            fileHandle.closeFile()
            debug("File closed. Total bytes written: \(totalBytesWritten)", function: "SPXPushAudioOutputStream.closeHandler")
        })!
        
        debug("Configuring audio and speech...", function: #function)
        let audioConfig = try? SPXAudioConfiguration(streamOutput: stream)
        let speechConfig = try? SPXSpeechConfiguration(subscription: speechKey, region: serviceRegion)
        
        guard let config = speechConfig, let audio = audioConfig else {
            debug("Failed to create speech or audio configuration", function: #function)
            updateResultText("Speech Config Error")
            return
        }
        
        config.setSpeechSynthesisOutputFormat(.audio24Khz160KBitRateMonoMp3)
        debug("Set output format to MP3", function: #function)
        
        updateResultText("Synthesizing...")
        
        debug("Creating speech synthesizer...", function: #function)
        let synthesizer = try? SPXSpeechSynthesizer(speechConfiguration: config, audioConfiguration: audio)
        guard let synth = synthesizer else {
            debug("Failed to create speech synthesizer", function: #function)
            updateResultText("Speech Synthesis Error")
            return
        }
        
        debug("Starting text-to-speech...", function: #function)
        let speechResult = try? synth.speakText(inputText)
        if let result = speechResult {
            if result.reason == SPXResultReason.canceled {
                let details = try! SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
                debug("Speech synthesis canceled: \(details.errorDetails ?? "Unknown error")", function: #function)
                updateResultText("Canceled: \(details.errorDetails ?? "Unknown error")")
            } else if result.reason == SPXResultReason.synthesizingAudioCompleted {
                let synthesisTime = Date().timeIntervalSince(startTime)
                debug("Speech synthesis completed successfully in \(String(format: "%.2f", synthesisTime)) seconds", function: #function)
                updateResultText("Speech synthesis completed in \(String(format: "%.2f", synthesisTime)) seconds.")
                
                // Add a small delay to ensure file writing is complete
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    // Get file size
                    do {
                        let attributes = try FileManager.default.attributesOfItem(atPath: filePath.path)
                        let fileSize = attributes[.size] as? Int64 ?? 0
                        debug("File size: \(fileSize) bytes", function: "DispatchQueue.asyncAfter")
                    } catch {
                        debug("Error getting file size: \(error)", function: "DispatchQueue.asyncAfter")
                    }
                    
                    // Get audio duration
                    let asset = AVAsset(url: filePath)
                    let duration = asset.duration
                    let durationSeconds = CMTimeGetSeconds(duration)
                    debug("Audio duration: \(durationSeconds) seconds", function: "DispatchQueue.asyncAfter")
                    self.updateResultText("Speech synthesis completed in \(String(format: "%.2f", synthesisTime)) seconds. Audio Duration: \(String(format: "%.2f", durationSeconds)) seconds, Size: \(FileManager.default.sizeFormatted(ofPath: filePath.path) ?? "Unknown")")
                }
            } else {
                debug("Speech synthesis failed with reason: \(result.reason)", function: #function)
                updateResultText("Speech synthesis error.")
            }
        } else {
            debug("Speech synthesis failed (no result)", function: #function)
            updateResultText("Speech synthesis error.")
        }
    }
    
    private func updateResultText(_ text: String) {
        DispatchQueue.main.async {
            self.resultText = text
            debug("Updated result text: \(text)", function: #function)
            self.synthesisCompleted = text.contains("Speech synthesis completed")
            debug("Synthesis completed: \(self.synthesisCompleted)", function: #function)
        }
    }
    
    private func playAudio() {
        if isPlaying {
            audioPlayer?.stop()
            isPlaying = false
            debug("Audio playback stopped", function: #function)
        } else {
            let filePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("pushStream.mp3")
            debug("Attempting to play audio from: \(filePath.path)", function: #function)
            
            do {
                audioPlayer = try AVAudioPlayer(contentsOf: filePath)
                audioPlayer?.play()
                isPlaying = true
                debug("Audio playback started", function: #function)
                if let duration = audioPlayer?.duration {
                    debug("Audio duration: \(duration) seconds", function: #function)
                }
            } catch {
                updateResultText("Error playing audio: \(error.localizedDescription)")
                debug("Detailed error playing audio: \(error)", function: #function)
            }
        }
    }
    
    private func debug(_ message: String, function: String) {
        let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
        print("[\(timestamp)] [\(function)] \(message)")
    }
}

// Add this extension for formatting file size
extension FileManager {
    func sizeFormatted(ofPath path: String) -> String? {
        guard let attributes = try? attributesOfItem(atPath: path) else { return nil }
        let size = attributes[.size] as? Int64 ?? 0
        return ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
    }
}

Однако я не могу, хоть убей, понять, как я буду это транслировать. У меня очень мало знаний об AVPlayer, так что это, очевидно, не поможет, но я попробовал использовать все подходы, которые смог найти в сети... любые указания на потенциальные решения будут высоко оценены!

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
89
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Для потоковой передачи звука, созданного из Speech SDK, с помощью SPXPushAudioOutputStream вы можете изменить существующий код для воспроизведения звука во время его потоковой передачи.

Я настроил SPXPushAudioOutputStream для потоковой передачи данных на AVAudioEngine для воспроизведения в реальном времени.

private func synthesisToPushAudioOutputStream() {
    let startTime = Date()
    debug("Starting speech synthesis...", function: #function)
    
    guard let audioEngine = audioEngine else {
        debug("Audio engine is not initialized", function: #function)
        updateResultText("Audio Engine Error")
        return
    }
    
    // Prepare audio engine and player node
    audioEngine.attach(audioPlayerNode)
    let format = audioEngine.mainMixerNode.outputFormat(forBus: 0)
    audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: format)
    
    let stream = SPXPushAudioOutputStream(writeHandler: { data -> UInt in
        if let pcmBuffer = self.convertDataToPCMBuffer(data: data, format: format) {
            self.audioPlayerNode.scheduleBuffer(pcmBuffer, completionHandler: nil)
        }
        return UInt(data.count)
    }, closeHandler: {
        audioEngine.stop()
        debug("Audio engine stopped", function: "SPXPushAudioOutputStream.closeHandler")
    })!
    
    let audioConfig = try? SPXAudioConfiguration(streamOutput: stream)
    let speechConfig = try? SPXSpeechConfiguration(subscription: speechKey, region: serviceRegion)
    
    guard let config = speechConfig, let audio = audioConfig else {
        debug("Failed to create speech or audio configuration", function: #function)
        updateResultText("Speech Config Error")
        return
    }
    
    config.setSpeechSynthesisOutputFormat(.audio16Khz16KbpsMonoPcm)
    debug("Set output format to PCM", function: #function)
    
    updateResultText("Synthesizing...")
    
    let synthesizer = try? SPXSpeechSynthesizer(speechConfiguration: config, audioConfiguration: audio)
    guard let synth = synthesizer else {
        debug("Failed to create speech synthesizer", function: #function)
        updateResultText("Speech Synthesis Error")
        return
    }
    
    debug("Starting text-to-speech...", function: #function)
    let speechResult = try? synth.speakText(inputText)
    if let result = speechResult {
        if result.reason == SPXResultReason.canceled {
            let details = try! SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
            debug("Speech synthesis canceled: \(details.errorDetails ?? "Unknown error")", function: #function)
            updateResultText("Canceled: \(details.errorDetails ?? "Unknown error")")
        } else if result.reason == SPXResultReason.synthesizingAudioCompleted {
            let synthesisTime = Date().timeIntervalSince(startTime)
            debug("Speech synthesis completed successfully in \(String(format: "%.2f", synthesisTime)) seconds", function: #function)
            updateResultText("Speech synthesis completed in \(String(format: "%.2f", synthesisTime)) seconds.")
            synthesisCompleted = true
        } else {
            debug("Speech synthesis failed with reason: \(result.reason)", function: #function)
            updateResultText("Speech synthesis error.")
        }
    } else {
        debug("Speech synthesis failed (no result)", function: #function)
        updateResultText("Speech synthesis error.")
    }
}

private func convertDataToPCMBuffer(data: Data, format: AVAudioFormat) -> AVAudioPCMBuffer? {
    let audioBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: UInt32(data.count) / format.streamDescription.pointee.mBytesPerFrame)
    audioBuffer?.frameLength = audioBuffer!.frameCapacity
    let audioBufferPointer = audioBuffer?.floatChannelData?[0]
    data.copyBytes(to: UnsafeMutableBufferPointer(start: audioBufferPointer, count: data.count / MemoryLayout<Float>.size))
    return audioBuffer
}

import UIKit
import MicrosoftCognitiveServicesSpeech

let EmbeddedSpeechSynthesisVoicesFolderName = "TTS"
let EmbeddedSpeechSynthesisVoiceName = "YourEmbeddedSpeechSynthesisVoiceName"
let EmbeddedSpeechSynthesisVoiceKey = "YourEmbeddedSpeechSynthesisVoiceKey"

class ViewController: UIViewController, UITextFieldDelegate {
    
    var textField: UITextField!
    var synthButton: UIButton!
    
    var inputText: String!
    var embeddedSpeechConfig: SPXEmbeddedSpeechConfiguration?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let bundle = Bundle(for: type(of: self))
        if let absoluteModelPath = bundle.path(forResource: EmbeddedSpeechSynthesisVoicesFolderName, ofType: nil) {
            do {
                embeddedSpeechConfig = try SPXEmbeddedSpeechConfiguration(fromPath: absoluteModelPath)
                embeddedSpeechConfig?.setSpeechSynthesisVoice(EmbeddedSpeechSynthesisVoiceName, key: EmbeddedSpeechSynthesisVoiceKey)
            } catch {
                print("Error: \(error) in initializing embedded speech configuration.")
                embeddedSpeechConfig = nil
            }
        } else {
            print("Error: Unable to locate the specified embedded speech synthesis voice.")
        }
        
        setupUI()
    }
    
    func setupUI() {
        textField = UITextField(frame: CGRect(x: 100, y: 250, width: 200, height: 50))
        textField.textColor = UIColor.black
        textField.borderStyle = UITextField.BorderStyle.roundedRect
        textField.placeholder = "Type something to synthesize."
        textField.delegate = self
        
        inputText = ""
        
        synthButton = UIButton(frame: CGRect(x: 100, y: 400, width: 200, height: 50))
        synthButton.setTitle("Synthesize", for: .normal)
        synthButton.addTarget(self, action: #selector(synthesisButtonClicked), for: .touchUpInside)
        synthButton.setTitleColor(UIColor.black, for: .normal)
        
        self.view.addSubview(textField)
        self.view.addSubview(synthButton)
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let demotext = textField.text, let textRange = Range(range, in: text) {
            self.inputText = demotext.replacingCharacters(in: textRange, with: string)
        }
        return true
    }
    
    @objc func synthesisButtonClicked() {
        DispatchQueue.global(qos: .userInitiated).async {
            self.synthesisToWAV()
        }
    }
    
    func synthesisToWAV() {
        let synthesizer = try! SPXSpeechSynthesizer(embeddedSpeechConfiguration: embeddedSpeechConfig!)
        if inputText.isEmpty {
            return
        }
        
        do {
            let audioConfig = try SPXAudioConfiguration.fromDefaultSpeakerOutput()
            let result = try synthesizer.synthesizeSpeech(inputText, audioConfig: audioConfig)
            
            guard let audioData = result.audioData else {
                print("Error: Audio data is nil.")
                return
            }
            
            let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
            let filePath = documentsPath.appendingPathComponent("output.wav")
            let fileURL = URL(fileURLWithPath: filePath)
            
            do {
                try audioData.write(to: fileURL)
                print("Speech synthesized and saved to: \(fileURL)")
            } catch {
                print("Error writing file: \(error)")
            }
            
        } catch {
            print("Error synthesizing speech: \(error)")
        }
    }
}    

Метод synthesisToWAV() синтезирует текст в речь с помощью SPXSpeechSynthesizer и сохраняет синтезированный звук в виде .wav файла.

sample_swift_ios.wav:

Я использовал этот git для преобразования текста в речь с использованием языка Swift.

Большое большое спасибо! Это привело меня к правильному решению, и я очень счастлив. Он не был идеальным, и мне пришлось адаптировать несколько незначительных вещей, таких как аудиоформат, но в целом это было очень полезно!

keleko 06.07.2024 21:05

Конечно, но можете ли вы добавить к своему сообщению примечание со ссылкой на мой ответ с окончательным решением, чтобы, если кто-то ищет рабочий код, он также указывал на мой код?

keleko 09.07.2024 10:09

@kekeko Да, можешь.

Naveen Sharma 09.07.2024 13:45

Основываясь на ответе Sampath, вот код, который наконец сработал у меня (включая AudioPlayerNode для воспроизведения):

import SwiftUI
import AVFoundation
import AVFAudio
import MicrosoftCognitiveServicesSpeech

struct ContentView: View {
    @State private var inputText = """
    Die Gesundheitspolitik bleibt ein hartes Pflaster für Reformen...
    """
    @State private var resultText = ""
    @State private var isPlaying = false
    @State private var audioPlayer: AVAudioPlayer?
    @State private var synthesisCompleted = false
    @State private var audioEngine = AVAudioEngine()
    @State private var audioPlayerNode = AVAudioPlayerNode()

let speechKey = "ENTERYOUROWNKEY"
let serviceRegion = "switzerlandnorth"

var body: some View {
    VStack {
        TextField("Enter text to synthesize", text: $inputText)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
        
        Button(action: synthesisToPushAudioOutputStream) {
            Text("Synthesize Speech")
        }
        .padding()
        
        Button(action: playAudio) {
            Text(isPlaying ? "Stop" : "Play")
        }
        .padding()
        .disabled(!synthesisCompleted)
        
        Text(resultText)
            .padding()
    }
    .onChange(of: resultText) { newValue in
        debug("Result text changed to: \(newValue)", function: "body.onChange")
        synthesisCompleted = newValue.contains("Speech synthesis completed")
        debug("Synthesis completed: \(synthesisCompleted)", function: "body.onChange")
    }
}

private func synthesisToPushAudioOutputStream() {
    let startTime = Date()
    debug("Starting speech synthesis...", function: #function)
    
    // Prepare audio engine and player node
    audioEngine.attach(audioPlayerNode)
    let format = audioEngine.mainMixerNode.outputFormat(forBus: 0)
    audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: format)
    
    let stream = SPXPushAudioOutputStream(writeHandler: { data -> UInt in
        if let pcmBuffer = self.convertDataToPCMBuffer(data: data, format: format) {
            self.audioPlayerNode.scheduleBuffer(pcmBuffer, completionHandler: nil)
        }
        return UInt(data.count)
    }, closeHandler: {
        audioEngine.stop()
        debug("Audio engine stopped", function: "SPXPushAudioOutputStream.closeHandler")
    })!
    
    let audioConfig = try? SPXAudioConfiguration(streamOutput: stream)
    let speechConfig = try? SPXSpeechConfiguration(subscription: speechKey, region: serviceRegion)
    
    guard let config = speechConfig, let audio = audioConfig else {
        debug("Failed to create speech or audio configuration", function: #function)
        updateResultText("Speech Config Error")
        return
    }
    
    config.setSpeechSynthesisOutputFormat(.raw16Khz16BitMonoPcm)
    debug("Set output format to PCM", function: #function)
    
    updateResultText("Synthesizing...")
    
    let synthesizer = try? SPXSpeechSynthesizer(speechConfiguration: config, audioConfiguration: audio)
    guard let synth = synthesizer else {
        debug("Failed to create speech synthesizer", function: #function)
        updateResultText("Speech Synthesis Error")
        return
    }
    
    debug("Starting text-to-speech...", function: #function)
    let speechResult = try? synth.speakText(inputText)
    if let result = speechResult {
        if result.reason == SPXResultReason.canceled {
            let details = try! SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
            debug("Speech synthesis canceled: \(details.errorDetails ?? "Unknown error")", function: #function)
            updateResultText("Canceled: \(details.errorDetails ?? "Unknown error")")
        } else if result.reason == SPXResultReason.synthesizingAudioCompleted {
            let synthesisTime = Date().timeIntervalSince(startTime)
            debug("Speech synthesis completed successfully in \(String(format: "%.2f", synthesisTime)) seconds", function: #function)
            updateResultText("Speech synthesis completed in \(String(format: "%.2f", synthesisTime)) seconds.")
            synthesisCompleted = true
        } else {
            debug("Speech synthesis failed with reason: \(result.reason)", function: #function)
            updateResultText("Speech synthesis error.")
        }
    } else {
        debug("Speech synthesis failed (no result)", function: #function)
        updateResultText("Speech synthesis error.")
    }
}

private func convertDataToPCMBuffer(data: Data, format: AVAudioFormat) -> AVAudioPCMBuffer? {
    let audioBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: UInt32(data.count) / format.streamDescription.pointee.mBytesPerFrame)
    audioBuffer?.frameLength = audioBuffer!.frameCapacity
    let audioBufferPointer = audioBuffer?.floatChannelData?[0]
    data.copyBytes(to: UnsafeMutableBufferPointer(start: audioBufferPointer, count: data.count / MemoryLayout<Float>.size))
    return audioBuffer
}

private func updateResultText(_ text: String) {
    DispatchQueue.main.async {
        self.resultText = text
        debug("Updated result text: \(text)", function: #function)
        self.synthesisCompleted = text.contains("Speech synthesis completed")
        debug("Synthesis completed: \(self.synthesisCompleted)", function: #function)
    }
}

private func playAudio() {
    if isPlaying {
        audioPlayer?.stop()
        isPlaying = false
        debug("Audio playback stopped", function: #function)
    } else {
        let filePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("pushStream.mp3")
        debug("Attempting to play audio from: \(filePath.path)", function: #function)
        
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: filePath)
            audioPlayer?.play()
            isPlaying = true
            debug("Audio playback started", function: #function)
            if let duration = audioPlayer?.duration {
                debug("Audio duration: \(duration) seconds", function: #function)
            }
        } catch {
            updateResultText("Error playing audio: \(error.localizedDescription)")
            debug("Detailed error playing audio: \(error)", function: #function)
        }
    }
}

private func debug(_ message: String, function: String) {
    let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
    print("[\(timestamp)] [\(function)] \(message)")
}
}

// Add this extension for formatting file size
extension FileManager {
    func sizeFormatted(ofPath path: String) -> String? {
        guard let attributes = try? attributesOfItem(atPath: path) else { return nil }
        let size = attributes[.size] as? Int64 ?? 0
        return ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
    }
}

Другие вопросы по теме