Skip to content

Commit 09ff5de

Browse files
committed
Release: v1.0.0
1 parent 4660337 commit 09ff5de

File tree

28 files changed

+1579
-182
lines changed

28 files changed

+1579
-182
lines changed

Example/KWSExample/KWSExample.xcodeproj/project.pbxproj

Lines changed: 588 additions & 0 deletions
Large diffs are not rendered by default.

Example/KWSExample/KWSExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Example/KWSExample/KWSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"colors" : [
3+
{
4+
"idiom" : "universal"
5+
}
6+
],
7+
"info" : {
8+
"author" : "xcode",
9+
"version" : 1
10+
}
11+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"platform" : "ios",
6+
"size" : "1024x1024"
7+
},
8+
{
9+
"appearances" : [
10+
{
11+
"appearance" : "luminosity",
12+
"value" : "dark"
13+
}
14+
],
15+
"idiom" : "universal",
16+
"platform" : "ios",
17+
"size" : "1024x1024"
18+
},
19+
{
20+
"appearances" : [
21+
{
22+
"appearance" : "luminosity",
23+
"value" : "tinted"
24+
}
25+
],
26+
"idiom" : "universal",
27+
"platform" : "ios",
28+
"size" : "1024x1024"
29+
}
30+
],
31+
"info" : {
32+
"author" : "xcode",
33+
"version" : 1
34+
}
35+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// AudioStreamer.swift
3+
// KWSExample
4+
//
5+
// Created by Marat Zainullin on 12/06/2025.
6+
//
7+
8+
import Foundation
9+
import AVFoundation
10+
11+
final class AudioStreamer {
12+
private let engine = AVAudioEngine()
13+
private let inputBus: AVAudioNodeBus = 0
14+
private let sampleRate: Double = 16000
15+
private let frameLength: Double = 3200
16+
17+
public var onBuffer: (([Double]) -> Void)?
18+
19+
20+
init() {
21+
let inputNode = engine.inputNode
22+
let inputFormat = inputNode.outputFormat(forBus: inputBus)
23+
24+
25+
let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: sampleRate, channels: 1, interleaved: true)!
26+
27+
let converter = AVAudioConverter(from: inputFormat, to: outputFormat)!
28+
29+
inputNode.installTap(onBus: inputBus, bufferSize: UInt32((inputFormat.sampleRate * frameLength) / sampleRate), format: inputFormat) { [weak self] buffer, _ in
30+
print("b", buffer)
31+
guard let self else { return }
32+
var newBufferAvailable = true
33+
34+
let inputCallback: AVAudioConverterInputBlock = { inNumPackets, outStatus in
35+
if newBufferAvailable {
36+
outStatus.pointee = .haveData
37+
newBufferAvailable = false
38+
39+
return buffer
40+
} else {
41+
outStatus.pointee = .noDataNow
42+
return nil
43+
}
44+
}
45+
46+
let capacity = AVAudioFrameCount(outputFormat.sampleRate) * buffer.frameLength / AVAudioFrameCount(buffer.format.sampleRate)
47+
48+
let convertedBuffer = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: capacity)!
49+
50+
var error: NSError?
51+
let _ = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputCallback)
52+
53+
let intData = convertedBuffer.floatChannelData!
54+
55+
var newFrames: [Double] = []
56+
for channelIdx in 0..<1 {
57+
newFrames += Array(UnsafeBufferPointer(start: intData[channelIdx], count: Int(convertedBuffer.frameLength))).map{ Double($0)}
58+
}
59+
60+
if newFrames.count != Int(frameLength) {
61+
print("unexpected buffer length", newFrames.count)
62+
return
63+
}
64+
print("newFrames", newFrames)
65+
self.onBuffer?(newFrames)
66+
}
67+
requestMicrophonePermissions()
68+
}
69+
70+
public func start() throws {
71+
72+
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
73+
try AVAudioSession.sharedInstance().setActive(true)
74+
engine.prepare()
75+
try engine.start()
76+
}
77+
78+
public func stop() {
79+
engine.inputNode.removeTap(onBus: inputBus)
80+
engine.stop()
81+
}
82+
83+
private func requestMicrophonePermissions(
84+
) {
85+
switch AVAudioSession.sharedInstance().recordPermission {
86+
case .undetermined:
87+
AVAudioSession.sharedInstance().requestRecordPermission { isAllowed in
88+
89+
}
90+
@unknown default: break
91+
92+
}
93+
}
94+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// ContentView.swift
3+
// KWSExample
4+
//
5+
// Created by Marat Zainullin on 12/06/2025.
6+
//
7+
8+
import SwiftUI
9+
import OtosakuKWS
10+
11+
struct ContentView: View {
12+
@ObservedObject private var observer: Observer = Observer()
13+
14+
var body: some View {
15+
VStack(spacing: 24) {
16+
Text("🎙️ Otosaku KWS Demo")
17+
.font(.largeTitle.bold())
18+
19+
if let keyword = observer.detectedKeyword, let score = observer.confidence {
20+
Text("✅ Detected: **\(keyword)**")
21+
.font(.title2)
22+
.foregroundColor(.green)
23+
24+
Text("Confidence: \(String(format: "%.2f", score))")
25+
.font(.subheadline)
26+
.foregroundColor(.secondary)
27+
} else {
28+
Text("Listening...")
29+
.font(.title2)
30+
.foregroundColor(.gray)
31+
}
32+
33+
if observer.recordWasStarted {
34+
ProgressView()
35+
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
36+
.scaleEffect(1.5)
37+
}
38+
39+
Spacer()
40+
}
41+
.padding()
42+
.onAppear {
43+
observer.startRecording()
44+
// setupKWS()
45+
}
46+
.onDisappear {
47+
observer.stop()
48+
}
49+
}
50+
51+
// private func setupKWS() {
52+
// Task {
53+
// do {
54+
// let modelRoot = Bundle.main.resourceURL!
55+
// let featurizerRoot = Bundle.main.resourceURL!
56+
//
57+
// let kws = try OtosakuKWS(
58+
// modelRootURL: modelRoot,
59+
// featureExtractorRootURL: featurizerRoot,
60+
// configuration: .init()
61+
// )
62+
//
63+
// kws.setProbabilityThreshold(0.9)
64+
//
65+
// kws.onKeywordDetected = { keyword, score in
66+
// detectedKeyword = keyword
67+
// confidence = score
68+
//
69+
// DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
70+
// detectedKeyword = nil
71+
// confidence = nil
72+
// }
73+
// }
74+
//
75+
// let audioInput = AudioStreamer()
76+
//
77+
// audioInput.onBuffer = { buffer in
78+
// Task {
79+
// await kws.handleAudioBuffer(buffer)
80+
// }
81+
// }
82+
//
83+
// try audioInput.start()
84+
// } catch {
85+
// print("🚨 Error setting up KWS: \(error)")
86+
// }
87+
// }
88+
// }
89+
}
62.9 KB
Binary file not shown.
1.69 KB
Binary file not shown.

0 commit comments

Comments
 (0)