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