Slide 1

Slide 1 text

Ի੠഑৴ΞϓϦʹ͓͚ΔiOSΛ࢖ͬ ͨԻ੠഑৴ͷશͯͱཪଆ iOSDC 2022 Track C 09/11 13:35ʙ

Slide 2

Slide 2 text

ࣗݾ঺հ Name: ԕ౻୓໻(͑Μͨ͘) Job: iOS / AndroidΤϯδχΞͳͲ Career: SIer໿6೥ 2018/3~ εϙʔπϚονϯάΞϓϦ 2019/3~ CBcloud ෺ྲྀITαʔϏε 2೥൒ 2021/12~ Voicy Twitter: @entaku_0818

Slide 3

Slide 3 text

Ի੠഑৴ϓϥοτϑΥʔϜ - Voicy https://corp.voicy.jp/ https://voicy.jp/ • Ի੠ίϯςϯπΛiOS/Android/WebͰఏڙ

Slide 4

Slide 4 text

VoicyͷΞϓϦ͸࠶ੜͱऩ࿥ͷ2छྨͷΞϓ ϦΛఏڙͯ͠·͢ ΠϯετʔϧϦϯΫ

Slide 5

Slide 5 text

ऩ࿥ͱ࠶ੜͰදཪҰମͰ͞·͟·ͳVoicyͷػೳ Ի੠࠶ੜ Ի੠ऩ࿥ ੜ์ૹ ίϝϯτ ϋογϡλάݕ ࡧ ച্੥ٻ ࠩ͠ೖΕ ϓϨϛΞϜ์ૹ

Slide 6

Slide 6 text

Ի੠αʔϏε্Ͱ͸ಛʹॏཁͳೋͭͷػೳ ίϝϯτ ϋογϡλάݕ ࡧ ച্੥ٻ ࠩ͠ೖΕ ϓϨϛΞϜ์ૹ Ի੠࠶ੜ Ի੠ऩ࿥ ੜ์ૹ

Slide 7

Slide 7 text

ར༻͍ͯ͠Δٕज़ Ի੠࠶ੜԻ੠ऩ࿥ ੜ์ૹ AVAudio Agora

Slide 8

Slide 8 text

AVAudioͱ͸ʁ

Slide 9

Slide 9 text

AVFAudioͱ͸ʁ https://developer.apple.com/documentation/avfaudio "7"VEJP4FTTJPO "7"VEJP3PVUJOH"SCJUFS "7"VEJP1MBZFS "7.*%*1MBZFS "7"VEJP3FDPSEFS "VEJP&OHJOF • AVFoundation͔ΒAudioͷػೳΛ੾Γग़ͨ͠΋ͷͰ͢ • iOS14.5͔Β

Slide 10

Slide 10 text

AVFAudioͱ͸ʁ https://developer.apple.com/documentation/avfaudio "7"VEJP4FTTJPO "7"VEJP3PVUJOH"SCJUFS "7"VEJP1MBZFS "7.*%*1MBZFS "7"VEJP3FDPSEFS "VEJP&OHJOF • VoicyͰར༻͍ͯ͠Δػೳ

Slide 11

Slide 11 text

Ի੠ͷऩ࿥

Slide 12

Slide 12 text

࿥ԻͷྲྀΕ ։࢝ ࿥Իத ऴྃ - 1tapͰ࿥ԻՄೳͳઃܭ Ի੠ͷ೾ܗ৘ใදࣔ

Slide 13

Slide 13 text

࿥Իݖݶ AVAudioSession.sharedInstance().requestRecordPermission { granted in self.handlePermissionGranted() } • ࿥ԻݖݶΛऔಘ https://developer.apple.com/documentation/avfaudio/avaudiosession/1616463- recordpermission

Slide 14

Slide 14 text

ηογϣϯઃఆ try audioSession.setCategory(.playAndRecord)ɹ try audioSession.setPreferredSampleRate(44100) try audioSession.setPreferredIOBufferDuration(defaultPreferredIOBufferDurat ion) try audioSession.setActive(true) https://developer.apple.com/documentation/avfaudio/avaudiosession • ࠶ੜऩ࿥ՄೳͳηογϣϯͰ։࢝ /P*NBHF

Slide 15

Slide 15 text

࿥Ի։࢝ var componentDescription = AudioComponentDescription( componentType: OSType(kAudioUnitType_Output), componentSubType: OSType(kAudioUnitSubType_RemoteIO), componentManufacturer: OSType(kAudioUnitManufacturer_Apple), componentFlags: UInt32(0), componentFlagsMask: UInt32(0) ) let component = AudioComponentFindNext(nil, &componentDescription) var tempAudioUnit: AudioUnit? AudioComponentInstanceNew(component!, &tempAudioUnit) https://developer.apple.com/documentation/audiotoolbox • audioUnitΛੜ੒

Slide 16

Slide 16 text

࿥Ի։࢝ var inputCallbackStruct = AURenderCallbackStruct( inputProc: recordingCallback, inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) ) https://developer.apple.com/documentation/audiotoolbox • Callbackͷఆٛ

Slide 17

Slide 17 text

࿥Ի։࢝ // ࿥ԻͷCallback private let recordingCallback: AURenderCallback = { (inRefCon, ioActionFlags, inTimeStamp, inBusNumber, frameCount, _) -> OSStatus in audioObject.handlerBufferData(bufferList: &bufferList, frameCount: frameCount) } • όοϑΝʔ৘ใΛऔಘ

Slide 18

Slide 18 text

೾ܗͷऔಘ let pcmBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: UInt32(bufferSize)) buffers.append(buffer) waveFormHeights.append(buffer.waveFormHeight) • औಘͨ͠όοϑΝʔ৘ใΛ΋ͱʹԻྔΛϨϯμϦϯά

Slide 19

Slide 19 text

೾ܗͷऔಘ 6*$PMMFDUJPO7JFX let pcmBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: UInt32(bufferSize)) buffers.append(buffer) waveFormHeights.append(buffer.waveFormHeight) • औಘͨ͠όοϑΝʔ৘ใΛ΋ͱʹԻྔΛϨϯμϦϯά

Slide 20

Slide 20 text

࠶ฤू • ऩ࿥ͨ͠Ի੠σʔλΛ্ॻ͖

Slide 21

Slide 21 text

Ի੠ऩ࿥ͷऴྃ let settings: [String: Any] = [ AVSampleRateKey: Constants.Recording.sampleRate, AVFormatIDKey: kAudioFormatMPEG4AAC, // MP4ܗࣜͰอଘ AVNumberOfChannelsKey: Constants.Recording.numberOfChannel, AVEncoderAudioQualityKey: AVAudioQuality.high ] let audioFile = try AVAudioFile(forWriting: fileURL, settings: settings) • Local΁อଘ͠Ξοϓϩʔυ

Slide 22

Slide 22 text

AVAudioRecorder͸ར༻͍ͯ͠ͳ͍ https://developer.apple.com/documentation/avfaudio "7"VEJP4FTTJPO "7"VEJP3PVUJOH"SCJUFS "7"VEJP1MBZFS "7.*%*1MBZFS "7"VEJP3FDPSEFS "VEJP&OHJOF • ։ൃ౰ॳ͸audioUnitͰ༷ʑͳ͜ͱΛ΍Ζ͏ͱ͍ͯͨ͠ͷ͔΋͠Εͳ͍ • ͓ͦΒ͘೾ܗʹԻྔͷߴ͞Λऔಘ͢ΔͨΊʹར༻ BVEJP6OJU

Slide 23

Slide 23 text

Ի੠ͷฤू

Slide 24

Slide 24 text

࠶ฤू • αʔϏεϙϦγʔ্Ͱ͖ͳ͍Α͏ʹ͍ͯ͠·͢ʂ

Slide 25

Slide 25 text

Ի੠࠶ੜ

Slide 26

Slide 26 text

Ի੠ͷ։࢝ Ի੠࠶ੜ։࢝ Ի੠࠶ੜத

Slide 27

Slide 27 text

Ի੠ͷ։࢝ func playPlayingChapter() { voicePlayer.play(url: fileURL, startTime: playingChapter.lastPlaytime.msecToSec(), speed: playSpeed.rawValue) } • νϟϓλʔ୯ҐͰͷ։࢝

Slide 28

Slide 28 text

Ի੠ͷఀࢭ func stopPlay() { player?.stop() player = nil } • Ի੠ͷఀࢭ

Slide 29

Slide 29 text

background࠶ੜ try audioSession.setCategory(.playback, mode: .default, options: []) https://developer.apple.com/documentation/avfaudio/avaudiosession/category

Slide 30

Slide 30 text

ੜ์ૹ

Slide 31

Slide 31 text

Agoraͱ͸ʁ

Slide 32

Slide 32 text

Agoraͱ͸ʁ https://www.agora.io/en/ https://jp.vcube.com/sdk

Slide 33

Slide 33 text

AgoraͰ͸ϏσΦ௨࿩΍Ի੠഑৴ͷػೳΛఏڙ https://www.agora.io/en/

Slide 34

Slide 34 text

ͦͷதͰ΋Live Steaming ͱ Real-time MessagingΛར༻ https://www.agora.io/en/

Slide 35

Slide 35 text

Voicyͷੜ์ૹͱ͸ʁ Ի੠ͷ࿈ܞ 
 Live Streaming (RTC) ͓ศΓ΍λΠτϧͷ࿈ܞ RealTimeMessage (RTM)

Slide 36

Slide 36 text

ੜ์ૹ

Slide 37

Slide 37 text

ੜ์ૹͷྲྀΕ ੜ์ૹ։࢝ ࿥Ի ऴྃ

Slide 38

Slide 38 text

ੜ์ૹ // RTC agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: EnvironmentConfig.agoraAppId, delegate: self) agoraKit?.enableAudioVolumeIndication(200, smooth: 3, report_vad: true) agoraKit?.setChannelProfile(AgoraChannelProfile.liveBroadcasting) // RTM agoraRtmKit = AgoraRtmKit(appId: EnvironmentConfig.agoraAppId, delegate: self) agoraRtmChannel = agoraRtmKit?.createChannel(withId: liveId, delegate: self) • Agora্ͰRTCͱRTMͰͦΕͧΕνϟϯωϧΛ࡞੒

Slide 39

Slide 39 text

ੜ์ૹ࿥Ի // ࿥Ի͸αʔόʔαΠυͰ࣮ࢪ͍ͯ͠·͢ // ͲΜͳॲཧ͔Θ͔ΔΑ͏ʹcurlͷAPIΛهࡌ͠·ͨ͠ curl --location --request GET 'https://api.agora.io/v1/apps// cloud_recording/resourceid//sid//mode//query' \ • Agoraͷcloud_recording(࿥Ի)ͷػೳ

Slide 40

Slide 40 text

͓ศΓϝοηʔδͷऔಘ func rtmKit(_ kit: AgoraRtmKit, messageReceived message: AgoraRtmMessage, fromPeer peerId: String) { messageReceived(message: message, uid: peerId) } ~~~ if let message = RTMMessage(rawValue: message.text), let uid = Int(uid) { switch message { case .hands_up: delegate?.onRaiseHand(userId: uid) case .hands_down: delegate?.onDroppedHand(userId: uid) case .invite: delegate?.onGuestInvited() case .canceled_invite: delegate?.onCancelGuestInvitation() case .reject: delegate?.onInvitaionRejected(userId: uid) case .set_audience: changeListener() case .mute: delegate?.onMute(uid: uid, muted: true) case .un_mute: delegate?.onMute(uid: uid, muted: false) case .stamp: delegate?.onStampReceived() case .edit_live_info: delegate?.onEditLiveInfo() case .letter: delegate?.onReceivedLetter(userId: uid) case .return_guest_to_listener: delegate?.onReturnGuestToListener() case .start: delegate?.onLiveStarted() } } • ϝοηʔδΛऔಘ༷͠ʑͳϦΞϧλΠϜॲཧΛ࣮ݱ

Slide 41

Slide 41 text

ੜ์ૹ

Slide 42

Slide 42 text

ੜ์ૹ • AgoraͰੜ์ૹʹࢀՃ self.agoraRtmChannel?.join(completion: { code in self.agoraKit?.setClientRole(agoraClientRole) self.agoraKit?.joinChannel(byToken: entity.agoraToken.rtcToken, channelId: String(liveID), info: nil, uid: UInt(entity.uid))

Slide 43

Slide 43 text

ੜ์ૹ • ͓ศΓΛૹΔ agoraRtmKit?.send(AgoraRtmMessage(text: text.rawValue), toPeer: uid)

Slide 44

Slide 44 text

Ի੠αʔϏε্(Voicy)Ͱ͸ಛʹॏཁͳೋͭͷػೳʹ ͍ͭͯ঺հ͠·ͨ͠ ίϝϯτ ϋογϡλάݕ ࡧ ച্੥ٻ ࠩ͠ೖΕ ϓϨϛΞϜ์ૹ Ի੠࠶ੜ Ի੠ऩ࿥ ੜ์ૹ

Slide 45

Slide 45 text

VoicyͷԻ੠ॲཧͷࠓޙͷల๬

Slide 46

Slide 46 text

ࠓޙͷల๬ɹԻ੠࠶ੜ/Ի੠ऩ࿥ • Ի੠ऩ࿥ͷ඼࣭޲্ • ϚΠΫ - bluetoothϚΠΫͳͲ͸ి೾͕ׯবͯ͠͠ ·͍Ի੠͕௿͔ͬͨΓ͢Δ • ݱࡏ͸ϚΠΫΛ઀ଓ͠ͳ͍ঢ়ଶͰͷऩ࿥Λਪ঑ • ࿈ଓ࠶ੜ / ΦϑϥΠϯ࠶ੜͳͲ • Ϧεφʔ΁͞ΒʹศརͳԻ੠ମݧΛ

Slide 47

Slide 47 text

ࠓޙͷల๬ɹੜ์ૹ • ࿥Ի඼࣭޲্ • AgoraͷCloud-RecordingͩͱͲ͏͠ ͯ΋඼࣭͕ྑ͘ͳ͍ • େਓ਺Ͱ΋଱͑ΒΕΔੜ์ૹ΁ • AgoraͱαʔόʔαΠυؚΊͨΩϟύ γςΟͷ޲্

Slide 48

Slide 48 text

͓·͚

Slide 49

Slide 49 text

ࠓ৽͘͠Ի੠ΞϓϦΛ࡞Δͱͨ͠ΒͲ͏͢ΔΜͩΖ͏ʁ 🤔 ΍ͬͺSwiftUI͔ʁ Swift Concurrencyͱ͔৽ ػೳ΋࢖͍͍ͨͳ

Slide 50

Slide 50 text

ͱ͍͏Θ͚Ͱ࡞Γ·ͨ͠ʂ https://github.com/entaku0818/VoiceMemo

Slide 51

Slide 51 text

αϯϓϧΛࢀߟʹͯ͠࡞Γ·ͨ͠ https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/VoiceMemos ͱ͍͏Θ͚Ͱ࡞Γ·ͨ͠ʂ

Slide 52

Slide 52 text

ಛʹ঺հ͍ͨ͠ͱ͜Ζ

Slide 53

Slide 53 text

Ի੠ॲཧͷDeleagteॲཧͷ؅ཧͷ೉͠͞ • େ͖ͳControllerͰશͯΛίϯτϩʔϧ͕ͪ͠ • ֤ը໘ͰDelegateͰॲཧΛॻ͖͕ͪ

Slide 54

Slide 54 text

Ի੠ͳͲͰ͸ಛʹଟ͍DelegateॲཧΛ·ͱΊ ͯΔ https://github.com/entaku0818/VoiceMemo private final class Delegate: NSObject, AVAudioPlayerDelegate, Sendable { let didFinishPlaying: @Sendable (Bool) -> Void let decodeErrorDidOccur: @Sendable (Error?) -> Void let player: AVAudioPlayer init( url: URL, didFinishPlaying: @escaping @Sendable (Bool) -> Void, decodeErrorDidOccur: @escaping @Sendable (Error?) -> Void ) throws { self.didFinishPlaying = didFinishPlaying self.decodeErrorDidOccur = decodeErrorDidOccur self.player = try AVAudioPlayer(contentsOf: url) super.init() self.player.delegate = self } func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { self.didFinishPlaying(flag) } func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { self.decodeErrorDidOccur(error) } }

Slide 55

Slide 55 text

liveͱ͍͏ঢ়ଶͰDelegateΛఆٛ Ի੠։࢝࣌ͷॲཧΛެ։ https://github.com/entaku0818/VoiceMemo struct AudioPlayerClient { var play: @Sendable (URL) async throws -> Bool } extension AudioPlayerClient { static let live = Self { url in let stream = AsyncThrowingStream { continuation in do { let delegate = try Delegate( url: url, didFinishPlaying: { successful in continuation.yield(successful) continuation.finish() }, decodeErrorDidOccur: { error in continuation.finish(throwing: error) } ) delegate.player.play() continuation.onTermination = { _ in delegate.player.stop() } } catch { continuation.finish(throwing: error) } } return try await stream.first(where: { _ in true }) ?? false } } 4XJGU$PODVSSFODZ Ͱ࣮૷

Slide 56

Slide 56 text

ReducerͰը໘ͰඞཁͳॲཧΛఆٛ https://github.com/entaku0818/VoiceMemo switch state.mode { case .notPlaying: state.mode = .playing(progress: 0) return .run { [url = state.url] send in let start = environment.mainRunLoop.now async let playAudio: Void = send( .audioPlayerClient(TaskResult { try await environment.audioPlayer.play(url) }) ) for try await tick in environment.mainRunLoop.timer(interval: 0.5) { await send(.timerUpdated(tick.date.timeIntervalSince(start.date))) } } .cancellable(id: PlayID.self, cancelInFlight: true) case .playing: state.mode = .notPlaying return .cancel(id: PlayID.self) }

Slide 57

Slide 57 text

ࢀߟࢿྉ

Slide 58

Slide 58 text

ࢀߟࢿྉ • agoraΛ࢖ͬͯϥΠϒ഑৴ػೳΛ1ϲ݄൒ͰϦϦʔεͨ͠࿩ • εϥΠυ • ࠓ೔͔Β෼͔Δ AVAudioEngineͷશͯ • εϥΠυ • ࣮૷ͨ͘͠ͳΔԻ੠ฤू • εϥΠυ • աڈͷԻ੠ʹؔΘΔiOSDCͷηογϣϯ

Slide 59

Slide 59 text

ࢀߟࢿྉ • ΍ͬͺΓ࠷ޙ͸ެࣜ • https://developer.apple.com/documentation/avfaudio • https://docs.agora.io/en

Slide 60

Slide 60 text

એ఻ʂ

Slide 61

Slide 61 text

https://voicy.jp/channel/1305 VoicyͷΤϯδχΞ͕ӡӦ͍ͯ͠Δνϟϯωϧ voi-chordௌ͍͍ͯͩ͘͞ʂ

Slide 62

Slide 62 text

iOSΤϯδχΞΛืू͍ͯ͠·͢ • Ի੠޷͖ͳΤϯδχΞืूதͰ͢ʂڵຯ͋Δํ͸DM͍ͩ͘͞ʂ Notion Twitter: @entaku_0818