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