parent
66fb024b51
commit
3f788a2f81
@ -0,0 +1,83 @@ |
||||
import AVFAudio |
||||
import Observation |
||||
import ShazamKit |
||||
|
||||
@Observable |
||||
final class ShazamService: NSObject, SHSessionDelegate { |
||||
var isListening = false |
||||
var matchedTitle: String? |
||||
var matchedArtist: String? |
||||
var errorMessage: String? |
||||
|
||||
private var session: SHSession? |
||||
private let audioEngine = AVAudioEngine() |
||||
private var timeoutTask: Task<Void, Never>? |
||||
|
||||
func startListening() { |
||||
guard !isListening else { |
||||
stopListening() |
||||
return |
||||
} |
||||
|
||||
matchedTitle = nil |
||||
matchedArtist = nil |
||||
errorMessage = nil |
||||
|
||||
let session = SHSession() |
||||
session.delegate = self |
||||
self.session = session |
||||
|
||||
let inputNode = audioEngine.inputNode |
||||
let format = inputNode.outputFormat(forBus: 0) |
||||
|
||||
inputNode.installTap(onBus: 0, bufferSize: 2048, format: format) { buffer, time in |
||||
session.matchStreamingBuffer(buffer, at: time) |
||||
} |
||||
|
||||
do { |
||||
try audioEngine.start() |
||||
isListening = true |
||||
|
||||
timeoutTask = Task { [weak self] in |
||||
try? await Task.sleep(for: .seconds(15)) |
||||
guard !Task.isCancelled else { return } |
||||
self?.stopListening() |
||||
if self?.matchedTitle == nil { |
||||
self?.errorMessage = "No match found. Try again in a quieter environment." |
||||
} |
||||
} |
||||
} catch { |
||||
errorMessage = "Could not access microphone." |
||||
} |
||||
} |
||||
|
||||
func stopListening() { |
||||
timeoutTask?.cancel() |
||||
timeoutTask = nil |
||||
if audioEngine.isRunning { |
||||
audioEngine.inputNode.removeTap(onBus: 0) |
||||
audioEngine.stop() |
||||
} |
||||
session = nil |
||||
isListening = false |
||||
} |
||||
|
||||
func clearResult() { |
||||
matchedTitle = nil |
||||
matchedArtist = nil |
||||
errorMessage = nil |
||||
} |
||||
|
||||
nonisolated func session(_ session: SHSession, didFind match: SHMatch) { |
||||
Task { @MainActor [weak self] in |
||||
guard let item = match.mediaItems.first else { return } |
||||
self?.matchedTitle = item.title |
||||
self?.matchedArtist = item.artist |
||||
self?.stopListening() |
||||
} |
||||
} |
||||
|
||||
nonisolated func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature) { |
||||
// Session continues listening; the 15s timeout handles the "no match" case. |
||||
} |
||||
} |
||||
Loading…
Reference in new issue