Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
iOSDC2023:聴いて話すiOS 現実世界の「音」との連携
Search
たまねぎ
September 02, 2023
Programming
450
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
iOSDC2023:聴いて話すiOS 現実世界の「音」との連携
たまねぎ
September 02, 2023
More Decks by たまねぎ
See All by たまねぎ
個人への依存を、AIで分散させる
chocoyama
0
23
AIのAIによるAIのための出力評価と改善
chocoyama
3
1k
[FlutterKaigi2024] Effective Form 〜Flutterによる複雑なフォーム開発の実践〜
chocoyama
1
13k
ハードウェア対応のリアル.pdf
chocoyama
0
140
20分でわかる!速習resultBuilder(iOSDC 2022)
chocoyama
7
4.1k
SwiftUIっぽくした話
chocoyama
1
770
SwiftUIとGraphQLでプロダクトの継続的な破壊に立ち向かう
chocoyama
6
3k
Other Decks in Programming
See All in Programming
GitHubハンズオン講座 — 実務レベルのチーム開発のフローを身につけよう
junhat6
0
110
速く作れる。その次は、速く確かめられる開発へ 〜AIネイティブ開発を支える、Shift Down〜 / Can build fast. Next, moving to development where we can verify fast.
rkaga
7
5.1k
SREの越境 / SRE Collaboration
y0hgi
2
290
UnityでSystem.Net.WebSocketsなWebSocketサーバが動かないのでUnity Monoのコードを覗いてみた / about implementing websocket server with unity mono
drumath2237
1
490
スマートフォンでモールス信号を送受信する 〜スマートフォンのLEDとカメラで作る光通信の設計と実装〜
atsuki_seo
0
210
半永久的に提供し続けられるプライベートクラウドを目指して ― 利用者の認知負荷を抑えるAPI抽象化とハードウェア世代交代の基盤設計
tomokon
0
280
Androidだけじゃない、DroidKaigiで広がった私の技術スタック
akkeylab
0
100
スマート反転とウェブアクセシビリティ
camiha
0
220
setup-vp GitLab対応の裏側
naokihaba
0
140
巨大モノリシックアプリ モダン化大作戦
ktcryomm
1
1.2k
thread_parallel_with_free-threaded_Python_and_NumPy.pdf
riku_sakamoto
0
370
AI時代のコードレビューは人に向けるな、仕組みに向けろ
texmeijin
5
2.8k
Featured
See All Featured
How People are Using Generative and Agentic AI to Supercharge Their Products, Projects, Services and Value Streams Today
helenjbeal
1
340
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
250
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
420
Exploring anti-patterns in Rails
aemeredith
4
520
The SEO Collaboration Effect
kristinabergwall1
1
580
Rebuilding a faster, lazier Slack
samanthasiow
85
9.7k
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.5k
Reality Check: Gamification 10 Years Later
codingconduct
0
2.3k
Ten Tips & Tricks for a 🌱 transition
stuffmc
1
240
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
35
2.9k
Done Done
chrislema
187
17k
Transcript
ௌ͍ͯ͢iOS ݱ࣮ੈքͷʮԻʯͱͷ࿈ܞ iOSDC Japan 2023 ͨ·Ͷ͗ (@_chocoyama)
ࣗݾհ • LayerX ← STORES ← Ϡϑʔ • iOSͱFlutterΛΑ͘৮Δ •
స৬ͯ͠1ϲ݄ͷϐνϐν ͨ·Ͷ͗
ࣗݾհ ࣮3࿈ଓొஃ
ձࣾհ
ԻΛ׆༻ͨ͠ػೳ࣮ɺ ؆୯͔ͭڧྗʹͳ͍ͬͯΔ ͱ͍͏͜ͱΛ͠·͢
ΞδΣϯμ • දʹؔ͢ΔiOSඪ४ػೳͷ ಛͱ͍ํ • ࣮ͨ͠σϞͷհ ͢͜ͱ ͞ͳ͍͜ͱ • ཁૉٕज़ͷৄࡉͳղઆ
• Έͷਂ͍ཧղ
ΞδΣϯμ 1. ݱ࣮ੈքͷʮԻʯΛ׆༻ͨ͠ػೳ 2. ԻೝࣝͱτϥϯεΫϦϓτ 3. ൃʹΑΔϑΟʔυόοΫ 4. ԻϚονϯά 5.
Իྨ 6. ·ͱΊ
ݱ࣮ੈքͷ ʮԻʯΛ׆༻ͨ͠ػೳ
• Siri • FaceTime • Shazam • ϊΠζݕ • Voice
Control • VoiceOver • Իೖྗ • ԻಡΈ্͛ • ԻϝϞ ࣮ଟ༷ͳඪ४ػೳ • AVFoundation • CallKit • Core Audio • ShazamKit • SiriKit • SoundAnalysis • Speech ਐԽ͢ΔFramework
• ଟ͘ͷAppͰ׆༻͍ͯ͠ͳ͍ • ԻΛ׆༻ͨ͠ମݧઃܭك • ΩϟονΞοϓͷϞνϕʔγϣϯ্͕ΓͮΒ͍ ࣮ࡍͷͱ͜Ζ… ʮ࣮ݱͰ͖Δ͜ͱʯʮ׆༻Ͱ͖ͦ͏ͳ͜ͱʯΛΠϝʔδ͘͢͢͠Δ
ԻೝࣝͱτϥϯεΫϦϓτ
• ϚΠΫԻϑΝΠϧʹΑͬ ͯԻσʔλΛऔΓࠐΈ • γεςϜ͕ԻΛղੳ͠ɺς Ωετσʔλʹม • AppଆͰ݁ՌͷΛ׆༻ Իೝࣝ
• ԻೝࣝͷͨΊͷAPI • ԻσʔλΛςΩετʹม ͨ͠Γɺಛஅʹ׆༻Ͱ ͖Δ • iOS17Ҏ߱ɺݴޠϞσϧͷ ΧελϚΠζΛαϙʔτ SFSpeechRecognizer
ࣄલ४උ̍ɿAudioEngineͷϔϧύʔ struct AudioEngine { private let audioEngine = AVAudioEngine() func
start( bufferSize: AVAudioFrameCount, handler: @escaping (AVAudioPCMBuffer, AVAudioTime) -> Void ) throws { // Իೝࣝͷ߹ͷΦʔσΟΦઃఆ let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.record, mode: .measurement, options: []) try audioSession.setActive(true) // Իೖྗͷ४උ audioEngine.inputNode.installTap( onBus: 0, bufferSize: bufferSize, format: audioEngine.inputNode.outputFormat(forBus: 0), block: handler ) audioEngine.prepare() try audioEngine.start() } }
Իೝࣝؔ࿈ͷΠϯελϯεͱϦΫΤετΛ࡞ class Transcriptor: ObservableObject { // ԻೝࣝΛ࣮ߦ͢ΔΠϯελϯε private let speechRecognizer
= SFSpeechRecognizer(locale: Locale(identifier: “ja-JP"))! // ϦΞϧλΠϜೖྗͰͳ͘ϑΝΠϧೖྗͷ߹ɺSFSpeechURLRecognitionRequestΛ͏ private let request = SFSpeechAudioBufferRecognitionRequest() private let audioEngine = AudioEngine() // ೝࣝ݁ՌΛར༻ଆʹ௨ @Published private(set) var bestTranscription: SFTranscription? init() { // ϦΫΤετͷηοτΞοϓΛߦ͏ if speechRecognizer.supportsOnDeviceRecognition { // ΦϯσόΠε࣮ߦɿར༻੍ݶ͕ͳ͘ɺϓϥΠόγʔอͨΕΔ // αʔόʔ࣮ߦɿར༻੍ݶ͕͋ΓɺσόΠε֎ʹσʔλ͕ૹ৴͞ΕΔ͕ɺਫ਼͕ߴ͍ request.requiresOnDeviceRecognition = true } request.shouldReportPartialResults = true } }
ϨίʔσΟϯάͷ։࢝ func startRecording() async throws { // ར༻Մ൱ͷνΣοΫ guard case
.authorized = await withCheckedContinuation({ SFSpeechRecognizer.requestAuthorization($0.resume(returning:)) }), speechRecognizer.isAvailable else { throw TranscriptorError.unavailable } // ΦʔσΟΦΤϯδϯͷىಈ try audioEngine.start(bufferSize: 2048) { buffer, _ in // όοϑΝαΠζ͝ͱͷσʔλΛೝࣝϦΫΤετʹՃ͍ͯ͘͠ request.append(buffer) } // ೝࣝॲཧͷ։࢝ speechRecognizer.recognitionTask(with: request) { result, _ in DispatchQueue.main.async { self.bestTranscription = result?.bestTranscription } } }
DEMOɿೝࣝͨ͠ԻΛදࣔ struct ContentView: View { @StateObject private var transcriptor =
Transcriptor() private var recognizedText: String? { // Իೝࣝ݁Ռ͔ΒϑΥʔϚοτࡁΈจࣈྻΛऔಘ transcriptor.bestTranscription?.formattedString } var body: some View { VStack { if let recognizedText { Text(recognizedText) } RecordingButton { try await transcriptor.startRecording() } } } }
• LayerXϞόΠϧνʔϜ্ཱ͔ͪ͛ͨΓ • iOSϝΠϯͷΤϯδχΞ͕ࣗ1ਓ • ࣸਅΛࡱΓʹདྷͯ͘Ε͍ͯΔਓ͍ͳ͍ • ʮ͍ɺνʔζʂʯͰγϟολʔΛΔࣗࡱΓΞϓϦΛ࡞͖ͬͯͨ มΘΔ͕…
࠾༻ใ ࠾༻ϙʔλϧɿhttps://jobs.layerx.co.jp/
ൃʹΑΔϑΟʔυόοΫ
• จষΛԻʹม͢Δٕज़ • ΞΫηγϏϦςΟπʔϧ ϢʔβʔͷϑΟʔυόοΫ IFͱͯ͠׆༻͞ΕΔ ԻൃʢText to Speechʣ
• iOSͷςΩετԻมAPI • ϓϨʔϯςΩετSSMLܗࣜͷσʔλΛೖྗʹͱΔ ※ Speech Synthesis Markup LanguageʢԻ߹ϚʔΫΞοϓݴޠʣ
• ൃݴޠεϐʔυͳͲΛίϯτϩʔϧՄೳ • iOS17͔ΒύʔιφϧϘΠεʹରԠʢӳޠͷΈʣ AVSpeechSynthesizer
import AVFoundation // PlaneText͔ΒUtteranceͷ࡞ let utterance = AVSpeechUtterance(string: text) utterance.prefersAssistiveTechnologySettings
= true // ΞγετઃఆҾܧ utterance.rate = 0.5 // εϐʔυ (0 ~ 1) utterance.pitchMultiplier = 1 // ϐον (0.5 ~ 2) utterance.volume = 1 // Իྔ (0 ~ 1) // SSML͔ΒUtteranceͷ࡞ let ssml = """ <speak> <prosody rate="fast" pitch="+2st" volume="loud"> ͜Μʹͪɺͨ·Ͷ͗Ͱ͢ʂ </prosody> </speak> """ let utterance = AVSpeechUtterance(ssmlRepresentation: ssml) // Voiceͷઃఆ utterance.voice = .init(language: "ja-JP") utterance.voice = .init(identifier: AVSpeechSynthesisVoiceIdentifierAlex) utterance.voice = AVSpeechSynthesisVoice.speechVoices().randomElement() • AVSpeechUtteranceʹ ൃσʔλΛηοτ • SSMLΛΘͳͯ͘ ɺ֤छϓϩύςΟ ઃఆՄೳ • AVSpeechSynthesisVoi ceΛηοτͯ͠ɺϏϧ τΠϯԻͷར༻Ͱ ͖Δ AVSpeechSynthesizerͰͷൃ
// AVSpeechSynthesizerͷ࡞ let synthesizer = AVSpeechSynthesizer() // ࠶ੜ synthesizer.speak(utterance) //
Ұ࣌ఀࢭ synthesizer.pauseSpeaking(at: .immediate) synthesizer.pauseSpeaking(at: .word) // ࠶։ synthesizer.continueSpeaking() // ఀࢭ synthesizer.stopSpeaking(at: .immediate) synthesizer.stopSpeaking(at: .word) • AVSpeechSynthesizer ʹAVSpeechUtterance Λͯ͠ൃ͢Δ • ίϯτϩʔϧ༻ͷAPI Λ׆༻ͯ͠ࡉ੍͔͍ޚ Ͱ͖Δ AVSpeechSynthesizerͰͷൃ
DEMOɿೖྗͨ͠จࣈྻΛൃ class SpeechSynthesizer: ObservableObject { @Published var text = “͜Μʹͪɺͨ·Ͷ͗Ͱ͢ʂ”
@Published var selectedVoice = AVSpeechSynthesisVoice .speechVoices() .first { $0.language == "ja-JP" }! @Published var rate: Float = 0.5 @Published var pitchMultiplier: Float = 1 @Published var volume: Float = 1 private let synthesizer: AVSpeechSynthesizer = { let s = AVSpeechSynthesizer() s.usesApplicationAudioSession = false return s }() var voices: [AVSpeechSynthesisVoice] { AVSpeechSynthesisVoice.speechVoices() } func speak() { let utterance = AVSpeechUtterance(string: text) utterance.voice = selectedVoice utterance.rate = rate utterance.pitchMultiplier = pitchMultiplier utterance.volume = volume synthesizer.speak(utterance) } }
DEMOɿԻͰͷର 4'4QFFDI3FDPHOJ[FS "74QFFDI4ZOUIFTJ[FS 0QFO"*"1*
ԻϚονϯά
• ԻͱҰக͢ΔσʔλΛ୳͢ ٕज़ • ͷϚΠΫ͔ΒԻΛऔಘ • ରͷίϯςϯπࣗମͦͷ ࠶ੜҐஔΛಛఆ͢Δ ԻϚονϯά
• ԻݯΧλϩά୳ • Իڹγάωνϟ͔ΒྨࣅԻݯΛಛఆ • ΧελϜΧλϩάͷ࡞ • ಠࣗͷΦʔσΟΦDBΛ࡞͠ɺ ԻΛϚονϯά
• ϓϨΠϦετཧ • ೝָࣝͨ͠ۂͷϥΠϒϥϦಉظ ShazamKit
import ShazamKit class Matcher: ObservableObject { @Published private(set) var matchedItem:
SHMatchedMediaItem? private let audioEngine = AudioEngine() func startMatching() async throws { let session = SHSession() // ϦΞϧλΠϜͳΦʔσΟΦϚονϯάͷ४උ try audioEngine.start(bufferSize: 2048) { buffer, audioTime in session.matchStreamingBuffer(buffer, at: audioTime) } // ೝࣝͨ͠ϝσΟΞΞΠςϜΛड͚औΔ for await case .match(let match) in session.results { await MainActor.run { matchedItem = match.mediaItems.first } } } } • ʢࣄલʹʣDeveloper ϙʔλϧͰAppService ΛՃ • SHSessionΛ࡞ • ΦʔσΟΦσʔλΛ sessionʹྲྀ͠ࠐΉ • ΧλϩάσʔλͱϚο νͨ݁͠ՌΛऔಘ ָۂಛఆ
SHMatchedMediaItemͷϓϩύςΟ܈ func explore(_ mediaItem: SHMatchedMediaItem) { mediaItem.title // λΠτϧ mediaItem.subtitle
// αϒλΠτϧ mediaItem.artist // ΞʔςΟετ໊ mediaItem.artworkURL // ΞʔτϫʔΫURL mediaItem.genres // δϟϯϧͷྻ mediaItem.timeRanges // ࣌ؒൣғ mediaItem.matchOffset. // ϚονՕॴ mediaItem.predictedCurrentMatchOffset // ݱࡏͷϚονՕॴͷ༧ଌ mediaItem.webURL. // ShazamΧλϩάϖʔδͷϦϯΫ mediaItem.appleMusicID // AppleMusicID mediaItem.appleMusicURL // AppleMusicϖʔδͷϦϯΫ mediaItem.songs // MusicKitͷSongΦϒδΣΫτ // etc… }
SHMatchedMediaItemͷ׆༻ // ָۂͷίϯτϩʔϧʹɺMusikKitΛ͏ import MusicKit func play(_ mediaItem: SHMatchedMediaItem) async
throws { guard case .authorized = await MusicAuthorization.request() else { return } // SHMatchedMediaItemͷAppleMusicؔ࿈ͷϓϩύςΟΛࢀর SystemMusicPlayer.shared.queue = .init(for: mediaItem.songs) try await SystemMusicPlayer.shared.play() }
DEMOɿฉ͖औͬͨԻݯΛಛఆ struct ContentView: View { @StateObject private var matcher =
Matcher() var body: some View { VStack(spacing: 56) { if let mediaItem = matcher.matchedItem { MatchedMediaItemView(mediaItem) } else if matcher.isActive { MatchedMediaItemView.loading() } RecordingButton { try? await matcher.startMatching() } }.padding() } }
• Shazam CLIΛͬͯ࡞ΕΔ • Իݯ͔ΒSignature (.shazamsignature)Λ࡞ • ҙͷϝλσʔλ ΛؚΊͨϑΝΠϧ(.csv)Λ༻ҙ •
SignatureͱCSVΛඥ͚ͯΧλϩάʢ.shazamcatalogʣΛ࡞ • ԻϚονϯάΦϑηοτΛ༻͍ͨମݧߏஙʹ׆༻Մೳ ΧελϜΧλϩά
Իྨ
Իྨ • ԻσʔλͷύλʔϯΛࣝผ • ػցֶशϞσϧΛݩʹٕͨ͠ ज़ • ԻݯͷछྨΛಛఆͷΧςΰϦ ʹ͚Δ
• Իྨ༻ͷϑϨʔϜϫʔΫ • ΦϯσόΠεͰ࣮ߦՄೳ • iOS15Ҏ߱ϏϧτΠϯ͞Εͨ ϞσϧΛ͑Δ • औΓࠐΜͩԻͷಛύλʔϯ Λೝࣝ͠ɺ300छྨʹྨ
SoundAnalysis
SNAudioStreamAnalyzer class SoundAnalyzer: NSObject, ObservableObject { @Published private(set) var result:
SNClassificationResult? private let audioEngine = AudioEngine() func startAnalyze() throws { // SNClassifySoundRequestΛ࡞ let request = try SNClassifySoundRequest(classifierIdentifier: .version1) // ϑΝΠϧೖྗͷ߹ɺSNAudioFileAnalyzerΛ͏ let analyzer = SNAudioStreamAnalyzer(format: audioEngine.format) try analyzer.add(request, withObserver: self) try audioEngine.start(bufferSize: 2048) { buffer, time in // ԻσʔλΛྲྀ͜͠Ή analyzer.analyze(buffer, atAudioFramePosition: time.sampleTime) } } }
SNAudioStreamAnalyzer extension SoundAnalyzer: SNResultsObserving { // ೝࣝ݁Ռ͕௨͞Εͯ͘Δ func request(_ request:
SNRequest, didProduce result: SNResult) { DispatchQueue.main.async { self.result = result as? SNClassificationResult self.result?.classifications.first?.identifier // ೝࣝͨ͠Իͷϥϕϧ self.result?.classifications.first?.confidence // ೝࣝͨ͠Իͷ৴པ } } }
DEMOɿ ໐͍ͬͯΔָثΛಛఆ struct ContentView: View { // … @StateObject private
var soundAnalyzer = SoundAnalyzer() var body: some View { ZStack(alignment: .bottom) { ScrollView { LazyVStack(spacing: 0) { // … BandImage(soundAnalyzer.result) } } RecordingButton { try? soundAnalyzer.analyze() }.padding(.vertical) } } }
DEMOɿಈըΛʮস͍ʯʮٽ͖ʯͰߜΓࠐΈ ಈըϑΝΠϧͷ63- 4/"VEJP'JMF"OBMZ[FS con fi dence >= 0.9 identi fi
er == “laughter” $.5JNF3BOHF
·ͱΊ
·ͱΊ • SFSpeechRecognizerɿݴޠೝࣝ • τϥϯεΫϦϓτɺݴޠΛϑοΫʹͨ͠ΞΫγϣϯ • AVSpeechSynthesizerɿൃ • Իग़ྗʹΑΔϑΟʔυόοΫ •
ShazamKitɿϚονϯά • ࣄલʹΧλϩάͷ༻ҙ͕ඞཁ • ݴޠͰ͋Δඞཁ͕ແ͘ɺOffsetͷ׆༻Ͱ͖Δ • SoundAnalysisɿྨ • ϏϧτΠϯϞσϧͰ͋Ε͙͢ʹ׆༻Մೳ • ΧελϜͷϞσϧΛ༻ҙͯ͠ɺҙͷྨΈࠐΊΔ
·ͱΊ • ඪ४ػೳ͚ͩͰଟ༷ͳମݧΛ࣮ݱͰ͖Δ • հ͖͠Εͳ͔ͬͨػೳෳ • ͍ॴݶΒΕΔ͕ɺ༗ޮʹ͑ϢχʔΫͳମݧΛఏڙͰ͖Δ
͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ