How to record an audio stream for save it in file / swift 4.2
Asked Answered
T

3

6

I am creating a radio application for iPhone (coded in Swift 4.2) and I want to add a function allowing me to record and save in a file, the sound produced by my radio (read from an AVPlayer) when I push a button. Which code should I use?

The code is in Swift 4.2, with Xcode 10.1. I search on the web : "How to record an audio stream swift 4.2", "How to record audio from AVPlayer swift 4.2", but I can't find an answer.

My code:

import UIKit
import AVFoundation
import MediaPlayer

class ViewControllerPlayer: UIViewController { 

    var URl = "http://link_of_audio_stream"
    var player:AVPlayer?
    var playerItem:AVPlayerItem?
    var playerLayer:AVPlayerLayer?

    override func viewDidLoad() {
        super.viewDidLoad()

        let url = URL(string: URl)
        let playerItem1:AVPlayerItem = AVPlayerItem(url: url!)
        player = AVPlayer(playerItem: playerItem1)

    }

    @IBAction func Play(_ sender: Any) {
            player?.play()
    }
    @IBAction func Pause(_ sender: Any) {
            player?.pause()
    }
private var audioRecorder: AVAudioRecorder!

    func startRecording() throws {
        guard let newFileURL = createURLForNewRecord() else {
            throw RecordingServiceError.canNotCreatePath
        }
        do {
            var urlString = URL(string: URl)
            urlString = newFileURL
            audioRecorder = try AVAudioRecorder(url: newFileURL,
                                                settings: [AVFormatIDKey:Int(kAudioFormatMPEG4AAC),
                                                           AVSampleRateKey: 8000,
                                                           AVNumberOfChannelsKey: 1,
                                                           AVEncoderAudioQualityKey: AVAudioQuality.min.rawValue])
            audioRecorder.delegate = self as? AVAudioRecorderDelegate
            audioRecorder.prepareToRecord()

            audioRecorder.record(forDuration: TimeConstants.recordDuration) 
            //error: Use of unresolved identifier 'TimeConstants'

        } catch let error {
            print(error)
        }
    }

    func STOPREC1() throws {
        audioRecorder.stop()
        audioRecorder = nil
            print("Recording finished successfully.")
    }

    enum RecordingServiceError: String, Error {
        case canNotCreatePath = "Can not create path for new recording"
    }

    private func createURLForNewRecord() -> URL? {
        guard let appGroupFolderUrl = FileManager.getAppFolderURL() else {
            return nil
        }

        let date = String(describing: Date())
        let fullFileName = "Enregistrement radio " + date + ".m4a"
        let newRecordFileName = appGroupFolderUrl.appendingPathComponent(fullFileName)
        return newRecordFileName
    }
}
    extension FileManager {
        class func getAppFolderURL() -> URL? {
            let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
            let documentsDirectory = paths[0]
            return documentsDirectory
        }
    }
Tuyere answered 15/1, 2019 at 20:32 Comment(0)
T
5

After multiple internet search, I found the solution.

I found this Swift Class somewhere on internet named « CachingPlayerItem.swift », it will allow the to record an online audio stream.

import Foundation
import AVFoundation

fileprivate extension URL {
    
    func withScheme(_ scheme: String) -> URL? {
        var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
        components?.scheme = scheme
        return components?.url
    }
    
}

@objc protocol CachingPlayerItemDelegate {
    
    /// Is called when the media file is fully downloaded.
    @objc optional func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data)
    
    /// Is called every time a new portion of data is received.
    @objc optional func playerItem(_ playerItem: CachingPlayerItem, didDownloadBytesSoFar bytesDownloaded: Int, outOf bytesExpected: Int)
    
    /// Is called after initial prebuffering is finished, means
    /// we are ready to play.
    @objc optional func playerItemReadyToPlay(_ playerItem: CachingPlayerItem)
    
    /// Is called when the data being downloaded did not arrive in time to
    /// continue playback.
    @objc optional func playerItemPlaybackStalled(_ playerItem: CachingPlayerItem)
    
    /// Is called on downloading error.
    @objc optional func playerItem(_ playerItem: CachingPlayerItem, downloadingFailedWith error: Error)
    
}

open class CachingPlayerItem: AVPlayerItem {
    
    class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
        
        var playingFromData = false
        var mimeType: String? // is required when playing from Data
        var session: URLSession?
        var mediaData: Data?
        var response: URLResponse?
        var pendingRequests = Set<AVAssetResourceLoadingRequest>()
        weak var owner: CachingPlayerItem?
        var fileURL: URL!
        var outputStream: OutputStream?
        
        func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
            
            if playingFromData {
                
                // Nothing to load.
                
            } else if session == nil {
                
                // If we're playing from a url, we need to download the file.
                // We start loading the file on first request only.
                guard let initialUrl = owner?.url else {
                    fatalError("internal inconsistency")
                }

                startDataRequest(with: initialUrl)
            }
            
            pendingRequests.insert(loadingRequest)
            processPendingRequests()
            return true
            
        }
        
        func startDataRequest(with url: URL) {
            
            var recordingName = "record.mp3"
            if let recording = owner?.recordingName{
                recordingName = recording
            }
            
            fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
                .appendingPathComponent(recordingName)
            let configuration = URLSessionConfiguration.default
            configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
            session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
            session?.dataTask(with: url).resume()
            outputStream = OutputStream(url: fileURL, append: true)
            outputStream?.schedule(in: RunLoop.current, forMode: RunLoop.Mode.default)
            outputStream?.open()
            
        }
        
        func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
            pendingRequests.remove(loadingRequest)
        }
        
        // MARK: URLSession delegate
        
        func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
            let bytesWritten = data.withUnsafeBytes{outputStream?.write($0, maxLength: data.count)}
        }
        
        func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
            completionHandler(Foundation.URLSession.ResponseDisposition.allow)
            mediaData = Data()
            self.response = response
            processPendingRequests()
        }
        
        func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
            if let errorUnwrapped = error {
                owner?.delegate?.playerItem?(owner!, downloadingFailedWith: errorUnwrapped)
                return
            }
            processPendingRequests()
            owner?.delegate?.playerItem?(owner!, didFinishDownloadingData: mediaData!)
        }
        
        // MARK: -
        
        func processPendingRequests() {
            
            // get all fullfilled requests
            let requestsFulfilled = Set<AVAssetResourceLoadingRequest>(pendingRequests.compactMap {
                self.fillInContentInformationRequest($0.contentInformationRequest)
                if self.haveEnoughDataToFulfillRequest($0.dataRequest!) {
                    $0.finishLoading()
                    return $0
                }
                return nil
            })
        
            // remove fulfilled requests from pending requests
            _ = requestsFulfilled.map { self.pendingRequests.remove($0) }

        }
        
        func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
            if playingFromData {
                contentInformationRequest?.contentType = self.mimeType
                contentInformationRequest?.contentLength = Int64(mediaData!.count)
                contentInformationRequest?.isByteRangeAccessSupported = true
                return
            }
            
            guard let responseUnwrapped = response else {
                // have no response from the server yet
                return
            }
            
            contentInformationRequest?.contentType = responseUnwrapped.mimeType
            contentInformationRequest?.contentLength = responseUnwrapped.expectedContentLength
            contentInformationRequest?.isByteRangeAccessSupported = true
            
        }
        
        func haveEnoughDataToFulfillRequest(_ dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
            
            let requestedOffset = Int(dataRequest.requestedOffset)
            let requestedLength = dataRequest.requestedLength
            let currentOffset = Int(dataRequest.currentOffset)
            
            guard let songDataUnwrapped = mediaData,
                songDataUnwrapped.count > currentOffset else {
                return false
            }
            
            let bytesToRespond = min(songDataUnwrapped.count - currentOffset, requestedLength)
            let dataToRespond = songDataUnwrapped.subdata(in: Range(uncheckedBounds: (currentOffset, currentOffset + bytesToRespond)))
            dataRequest.respond(with: dataToRespond)
            
            return songDataUnwrapped.count >= requestedLength + requestedOffset
            
        }
        
        deinit {
            session?.invalidateAndCancel()
        }
        
    }
    
    fileprivate let resourceLoaderDelegate = ResourceLoaderDelegate()
    fileprivate let url: URL
    fileprivate let initialScheme: String?
    fileprivate var customFileExtension: String?
    
    
    weak var delegate: CachingPlayerItemDelegate?
    
    func stopDownloading(){
        resourceLoaderDelegate.session?.invalidateAndCancel()
    }
    
    open func download() {
        if resourceLoaderDelegate.session == nil {
            resourceLoaderDelegate.startDataRequest(with: url)
        }
    }
    
    private let cachingPlayerItemScheme = "cachingPlayerItemScheme"
    var recordingName = "record.mp3"
    /// Is used for playing remote files.
    convenience init(url: URL, recordingName: String) {
        self.init(url: url, customFileExtension: nil, recordingName: recordingName)
    }
    
    /// Override/append custom file extension to URL path.
    /// This is required for the player to work correctly with the intended file type.
    init(url: URL, customFileExtension: String?, recordingName: String) {
        
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
            let scheme = components.scheme,
            var urlWithCustomScheme = url.withScheme(cachingPlayerItemScheme) else {
            fatalError("Urls without a scheme are not supported")
        }
        self.recordingName = recordingName
        self.url = url
        self.initialScheme = scheme
        
        if let ext = customFileExtension {
            urlWithCustomScheme.deletePathExtension()
            urlWithCustomScheme.appendPathExtension(ext)
            self.customFileExtension = ext
        }
        
        let asset = AVURLAsset(url: urlWithCustomScheme)
        asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
        super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
        
        resourceLoaderDelegate.owner = self
        
        addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
        
    }
    
    /// Is used for playing from Data.
    init(data: Data, mimeType: String, fileExtension: String) {
        
        guard let fakeUrl = URL(string: cachingPlayerItemScheme + "://whatever/file.\(fileExtension)") else {
            fatalError("internal inconsistency")
        }
        
        self.url = fakeUrl
        self.initialScheme = nil
        
        resourceLoaderDelegate.mediaData = data
        resourceLoaderDelegate.playingFromData = true
        resourceLoaderDelegate.mimeType = mimeType
        
        let asset = AVURLAsset(url: fakeUrl)
        asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
        super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
        resourceLoaderDelegate.owner = self
        
        addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
        
    }
    
    // MARK: KVO
    
    override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        delegate?.playerItemReadyToPlay?(self)
    }
    
    // MARK: Notification hanlers
    
    @objc func playbackStalledHandler() {
        delegate?.playerItemPlaybackStalled?(self)
    }

    // MARK: -
    
    override init(asset: AVAsset, automaticallyLoadedAssetKeys: [String]?) {
        fatalError("not implemented")
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
        removeObserver(self, forKeyPath: "status")
        resourceLoaderDelegate.session?.invalidateAndCancel()
    }
    
}

After, in you main swift file, you put this code to record:

let recordingName = "my_rec_name.mp3"
var playerItem: CachingPlayerItem!
let url_stream = URL(string: "http://my_url_stream_link")
playerItem = CachingPlayerItem(url: url_stream!, recordingName: recordingName ?? "record.mp3")
var player1 = AVPlayer(playerItem: playerItem)
player1.automaticallyWaitsToMinimizeStalling = false

And to stop the record, you use this code:

playerItem.stopDownloading()
recordingName = nil
playerItem = nil

Recordings will be saved on the directory of your app.

Tuyere answered 26/6, 2021 at 15:21 Comment(0)
C
0

I had a really hard time with this one so I am posting an answer.

Remember to add these lines to your info.plist:

enter image description here

Here is my controller that records the voice input and returns it to a previous controller:

import Foundation
import UIKit
import Speech

class SpeechToTextViewController: UIViewController {

@IBOutlet weak var animationView: UIView!
@IBOutlet weak var circleView: UIView!
@IBOutlet weak var micImage: UIImageView!
@IBOutlet weak var listeningLabel: UILabel!
@IBOutlet weak var buttonStartView: UIView!
@IBOutlet weak var cancelRecordingButton: UIButton!
@IBOutlet weak var stopRecordingButton: UIButton!
@IBOutlet weak var startRecordingButton: UIButton!

private let audioEngine = AVAudioEngine()
private let speechRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier:"en-US"))
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest!
private var recognitionTask: SFSpeechRecognitionTask?
private var isRecording: Bool = false

var delegate: SpeechToTextViewDelegate?

override func viewDidLoad() {
    super.viewDidLoad()
    self.view.backgroundColor = UIColor(white: 1.0, alpha: 0.25)
    self.stopRecordingButton.isHidden = true
    self.listeningLabel.isHidden = true
}

@IBAction func startStopRecording(_ sender: Any) {
    isRecording = !isRecording
    if isRecording && !audioEngine.isRunning {
        self.cancelRecordingButton.isHidden = true
        self.startRecordingButton.isHidden = true
        self.stopRecordingButton.isHidden = false
        self.listeningLabel.isHidden = false
        UIView.animate(withDuration: 1, animations: {}) { _ in
            UIView.animate(withDuration: 1, delay: 0.25, options: [.autoreverse, .repeat], animations: {
                self.circleView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
            })
        }
        do {
            try recordSpeech()
        } catch {
            print(error)
        }
    } else {
        self.listeningLabel.isHidden = true
        stopRecording()
    }
}

func recordSpeech() throws {
    recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
    let node = audioEngine.inputNode
    let recordingFormat = node.outputFormat(forBus: 0)
    node.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) {buffer, _ in
        self.recognitionRequest.append(buffer)
    }
    audioEngine.prepare()
    try audioEngine.start()
    guard let myRecognizer = SFSpeechRecognizer() else {
        print("myRecognizer is unable to be created")
        return
    }
    if !myRecognizer.isAvailable
    {
        print("myRecognizer is not available")
        return
    }
    recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest, resultHandler: { result, error in
        var isFinal = false
        if let result = result
        {
            isFinal = result.isFinal
            self.delegate?.appendMessage(result.bestTranscription.formattedString)
        }
        if error != nil || isFinal {
            if error != nil {
                print("error trying to capture speech to text")
                print(error!)
            }
            self.stopRecording()
        }
    })
}

func stopRecording() {
    if audioEngine.isRunning {
        self.audioEngine.stop()
        self.recognitionRequest.endAudio()
        // Cancel the previous task if it's running
        if let recognitionTask = recognitionTask {
            recognitionTask.cancel()
            self.recognitionTask = nil
        }
    }
    delegate?.doneTalking()
    self.dismiss(animated: true, completion: nil)
}

@IBAction func cancelRecording(_ sender: Any) {
    delegate?.doneTalking()
    self.dismiss(animated: true, completion: nil)
}

}

Coarsen answered 18/3, 2019 at 17:26 Comment(2)
Thank you for your code but that's not what I asked. I just want to record an audio stream online (not the voice) and save it to a file. PS : Look my code.Tuyere
I don't see how this answer is relevant at all to the question.Skirl
Z
-2

Use AVAudioRecorder for recording:

private var audioRecorder: AVAudioRecorder!

After you declared an audio recorder you can write a recording method:

func startRecording() throws {
        guard let newFileURL = createURLForNewRecord() else {
            throw RecordingServiceError.canNotCreatePath
        }
        do {
            currentFileURL = newFileURL
            audioRecorder = try AVAudioRecorder(url: newFileURL,
                                                settings: [AVFormatIDKey:Int(kAudioFormatMPEG4AAC),
                                                           AVSampleRateKey: 8000,
                                                           AVNumberOfChannelsKey: 1,
                                                           AVEncoderAudioQualityKey: AVAudioQuality.min.rawValue])
            audioRecorder.delegate = self
            audioRecorder.prepareToRecord()
            audioRecorder.record(forDuration: TimeConstants.recordDuration)
        } catch let error {
            print(error)
        }
}

And use some helper methods and structs:

enum RecordingServiceError: String, Error {
    case canNotCreatePath = "Can not create path for new recording"
}

private func createURLForNewRecord() -> URL? {
        guard let appGroupFolderUrl = FileManager.getAppFolderURL() else {
            return nil
        }

        let fileNamePrefix = DateFormatter.stringFromDate(Date())
        let fullFileName = "Record_" + fileNamePrefix + ".m4a"
        let newRecordFileName = appGroupFolderUrl.appendingPathComponent(fullFileName)
        return newRecordFileName
}

extension FileManager {
    class func getAppFolderURL() -> URL? {
        return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "you app bundle")
    }
}
Zamindar answered 15/1, 2019 at 21:16 Comment(4)
This method shoul start recording your audio stream audioRecorder.record(forDuration: TimeConstants.recordDuration)Zamindar
When I use "audioRecorder.record(forDuration: TimeConstants.recordDuration)", I have an error : Use of unresolved identifier 'TimeConstants'. I change my code, do not hesitate to have a look.Tuyere
I don't understand how this answer addresses the question. The question is asking how to save a stream of audio (presumably from a URL online), to a file locally on the device.Skirl
@Skirl I made path to in createURLForNewRecord() method and put it on AVAudioRecorder init so when record is finished you get recorded audio in this fileZamindar

© 2022 - 2024 — McMap. All rights reserved.