Запись и передача звука с устройства на устройство при помощи Multipeer Connectivity


Добрый день, дорогой читатель! Некоторое время назад я решил попробовать записать и передать записанный звук с устройства на устройство. Как средство передачи записанного звука выбор пал на фреймворк MultipeerConnectivity. В этой статье я расскажу как это сделать.

Первым делом нам необходимо два устройства для записи и воспроизведения звука. Соответственно нам необходимо написать класс для выполнения этих действий.

Запись аудио с устройства в реальном времени


Для записи аудио используется обычный AVAudioEngine и AVAudioMixerNode которые поставляются вместе с фреймворком AVFoundation.

Пример записи аудио:

final class Recorder {
    
    private let engine = AVAudioEngine()
    private let mixer = AVAudioMixerNode()
    
    var onRecordedAction: ((Data) -> Void)?
    
    init() {
        setupAudioSession()
    }
    
    private func setupAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.record)
            try audioSession.setMode(.measurement)
            try audioSession.setActive(true)
        } catch {
            debugPrint(error.localizedDescription)
        }
    }
    
    func startRecording() {
        let input = engine.inputNode
        let inputFormat = input.outputFormat(forBus: 0)
        engine.attach(mixer)
        engine.connect(input, to: mixer, format: inputFormat)
        mixer.installTap(onBus: 0, bufferSize: 1024, format: mixer.outputFormat(forBus: 0)) { [weak self] buffer, _ in
            self?.onRecordedAction?(buffer.data)
        }
        engine.prepare()
        do {
            try engine.start()
        } catch {
            debugPrint(error.localizedDescription)
        }
    }
    
    func stopRecording() {
        engine.stop()
    }
}

В общем ничего необычного, при помощи микшера мы получаем данные в реальном времени и отправляем их в нашу функцию onRecodedAction. Для того, чтобы передать дальше записанное нами аудио, нам нужно конвертировать его в data. Для этого я подготовил следующей extension.

Пример конвертирования PCMBuffer в Data:

extension AVAudioPCMBuffer {
    var data: Data {
        let channels = UnsafeBufferPointer(start: floatChannelData, count: 1)
        let data = Data(bytes: channels[0], count: Int(frameCapacity * format.streamDescription.pointee.mBytesPerFrame))
        return data
    }
}

Воспроизведение полученного аудио


Для воспроизведения аудио используется все тот же фреймворк, в итоге ничего сложного, если в двух словах, то мы просто создаем node и закрепляем ее за engine, дальше конвертируем нашу data обратно в PCMBuffer и отдаем нашей node для воспроизведения.

Пример воспроизведения:

final class Player {
    
    private let engine = AVAudioEngine()
    private var playerNode = AVAudioPlayerNode()
    
    init() {
        setupAudioSession()
    }
    
    private func setupAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.playback)
            try audioSession.setActive(true)
        } catch {
            debugPrint(error.localizedDescription)
        }
    }
    
    private func setupPlayer(buffer: AVAudioPCMBuffer) {
        engine.attach(playerNode)
        engine.connect(playerNode, to: engine.mainMixerNode, format: buffer.format)
        engine.prepare()
    }
    
    private func tryStartEngine() {
        do {
            try engine.start()
        } catch {
            debugPrint(error.localizedDescription)
        }
    }
    
    func addPacket(packet: Data) {
        guard let format = AVAudioFormat.common, let buffer = packet.pcmBuffer(format: format) else {
            debugPrint("Cannot convert buffer from Data")
            return
        }
        if !engine.isRunning {
            setupPlayer(buffer: buffer)
            tryStartEngine()
            playerNode.play()
        }
        playerNode.volume = 1
        playerNode.scheduleBuffer(buffer, completionHandler: nil)
    }
}

Пример extension для перевода Data обратно в PCMBuffer и нашего AVAudioFormat для воспроизведения аудио:

private extension Data {

    func pcmBuffer(format: AVAudioFormat) -> AVAudioPCMBuffer? {
        let streamDesc = format.streamDescription.pointee
        let frameCapacity = UInt32(count) / streamDesc.mBytesPerFrame
        guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCapacity) else { return nil }
        buffer.frameLength = buffer.frameCapacity
        let audioBuffer = buffer.audioBufferList.pointee.mBuffers
        withUnsafeBytes { addr in
            guard let baseAddress = addr.baseAddress else {
                return
            }
            audioBuffer.mData?.copyMemory(from: baseAddress, byteCount: Int(audioBuffer.mDataByteSize))
        }
        return buffer
    }
}

extension AVAudioFormat {
    static let common = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 1, interleaved: false)
}

Передача записанного аудио с устройства на устройство


Ну и наконец-то мы подошли к самому главному — передача записанного аудио с устройства на устройство при помощи MultipeerConnectivity. Для этого нам необходимо создать объект MCPeerID (Будет определять наше устройство) и два экземпляра класса MCNearbyServiceAdvertiser и MCNearbyServiceBrowser, которые будут использоваться для поиска устройств и для того, чтобы другие устройства могли нас найти (Так же принять от других устройств запрос на подключение). Так же мы создаем сессию при помощи которой будем передавать записанное аудио и «манипулировать» нашими устройствами.

Пример класса для передачи и получения данных:

private struct Constants {
    static var serviceType = "bn-radio"
    static var timeOut: Double = 10
}

final class Connectivity: NSObject {
    
    private var advertiser: MCNearbyServiceAdvertiser? = nil
    private var browser: MCNearbyServiceBrowser? = nil
    private let peerID: MCPeerID
    private let session: MCSession
    private var invitationHandler: ((Bool, MCSession) -> Void)? = nil
    
    var onDeviceFoundedAction: ((MCPeerID) -> Void)?
    var onDeviceLostedAction: ((MCPeerID) -> Void)?
    var onInviteAction: ((MCPeerID) -> Void)?
    var onConnectingAction: (() -> Void)?
    var onConnectedAction: (() -> Void)?
    var onDisconnectedAction: (() -> Void)?
    var onPacketReceivedAction: ((Data) -> Void)?
    
    init(deviceID: String) {
        peerID = MCPeerID(displayName: deviceID)
        session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .none)
        super.init()
        session.delegate = self
    }
    
    func startHosting() {
        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: Constants.serviceType)
        advertiser?.delegate = self
        advertiser?.startAdvertisingPeer()
    }
    
    func findHost() {
        browser = MCNearbyServiceBrowser(peer: peerID, serviceType: Constants.serviceType)
        browser?.delegate = self
        browser?.startBrowsingForPeers()
    }
    
    func stop() {
        advertiser?.stopAdvertisingPeer()
        browser?.stopBrowsingForPeers()
    }
    
    func invite(peerID: MCPeerID) {
        browser?.invitePeer(peerID, to: session, withContext: nil, timeout: Constants.timeOut)
    }
    
    func handleInvitation(isAccepted: Bool) {
        invitationHandler?(isAccepted, session)
    }
    
    func send(data: Data) {
        try? self.session.send(data, toPeers: session.connectedPeers, with: .unreliable)
    }
}

extension Connectivity: MCSessionDelegate {
    
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        switch state {
        case .connecting:
            onConnectingAction?()
        case .connected:
            onConnectedAction?()
        case .notConnected:
            onDisconnectedAction?()
        @unknown default:
            debugPrint("Error during session state changed on: \(state)")
        }
    }
    
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        onPacketReceivedAction?(data)
    }
    
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        
    }
    
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        
    }
    
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        
    }
    
    func session(_ session: MCSession, didReceiveCertificate certificate: [Any]?, fromPeer peerID: MCPeerID, certificateHandler: @escaping (Bool) -> Void) {
        certificateHandler(true)
    }
}

extension Connectivity: MCNearbyServiceAdvertiserDelegate {
    
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        self.invitationHandler = invitationHandler
        onInviteAction?(peerID)
    }
}

extension Connectivity: MCNearbyServiceBrowserDelegate {
    
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        onDeviceFoundedAction?(peerID)
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        onDeviceLostedAction?(peerID)
    }
}

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

Во время использования MultipeerConnectivity были выявлены некоторые отрицательные стороны, к примеру дистанция действия подключения, по этому я не советую использовать данные подход для передачи данных, если вам нужно передавать что-то схожее с аудио в реальном времени на постоянной основе.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое