Запись и передача звука с устройства на устройство при помощи 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 были выявлены некоторые отрицательные стороны, к примеру дистанция действия подключения, по этому я не советую использовать данные подход для передачи данных, если вам нужно передавать что-то схожее с аудио в реальном времени на постоянной основе.


ссылка на оригинал статьи https://habr.com/ru/post/483174/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *