mirror of
https://github.com/carey314/mio-plugin-worker.git
synced 2026-08-10 07:04:32 +00:00
feat(sit): 15s Morse alert tone on sit threshold
The UN notification ding is one short beep — easy to miss in a meeting. For sit alerts, repeat the system Morse tone (0.7s staccato "S O S") at 1Hz for 15 ticks, layered on top of the notification + dock bounce. - new: SoundPlayer.swift — NSSound-based repeating tone with stop() - WorkerStore.tickFire: trigger SoundPlayer.playMorseSitAlert(count: 15) alongside the existing notify() call - WorkerStore.sitStop: silence in-flight alert when user stops monitoring Uses /System/Library/Sounds/Morse.aiff — shipped on every macOS, no resource bundling needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3fb2c5c2a8
commit
19c5be91fa
80
Sources/engine/SoundPlayer.swift
Normal file
80
Sources/engine/SoundPlayer.swift
Normal file
@ -0,0 +1,80 @@
|
||||
//
|
||||
// SoundPlayer.swift
|
||||
// 摸鱼侠 plugin v0.2.1
|
||||
//
|
||||
// Sit-reminder alert tone. The bundled UN notification sound is a
|
||||
// ~1s ding that's easy to miss in a meeting — for sit alerts we want
|
||||
// a 15s nag that makes you actually stand up.
|
||||
//
|
||||
// Implementation: repeat the macOS system "Morse" sound (0.7s) once
|
||||
// per second for 15 ticks. Morse is the most distinctive notification
|
||||
// sound shipped with macOS — staccato 3-tone "S O S" pattern that
|
||||
// pierces background noise better than Glass/Hero/Ping/Funk/etc.
|
||||
//
|
||||
// Plays via NSSound (not UNNotificationContent.sound) because UN
|
||||
// sounds are limited to ~30s but get truncated to ~5s on macOS, and
|
||||
// attach a single tone to a single notification — no way to fire a
|
||||
// repeated sequence from UN.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
final class SoundPlayer {
|
||||
static let shared = SoundPlayer()
|
||||
private init() {}
|
||||
|
||||
/// System Morse sound — 0.7s staccato 3-tone "S".
|
||||
/// Shipped on every macOS since at least 10.0; safe to hard-code path.
|
||||
private let morseURL = URL(fileURLWithPath: "/System/Library/Sounds/Morse.aiff")
|
||||
|
||||
private var timer: Timer?
|
||||
private var remaining: Int = 0
|
||||
private var currentSound: NSSound?
|
||||
|
||||
/// Repeated Morse alert: play `count` times at 1Hz. Each play is a
|
||||
/// fresh NSSound so previous-in-flight playback doesn't get stomped
|
||||
/// (NSSound.play() is non-blocking and the 0.7s tone overlaps with
|
||||
/// the next-second silence — acceptable acoustic gap).
|
||||
///
|
||||
/// Calling while a previous alert is still running cancels the old
|
||||
/// one and restarts from `count`. This keeps "user sat back down +
|
||||
/// trigger fired again 45min later" clean.
|
||||
func playMorseSitAlert(count: Int = 15) {
|
||||
stop()
|
||||
remaining = count
|
||||
playOnce() // fire immediately, then 1Hz repeat
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
Task { @MainActor in self?.playOnce() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel any in-flight alert. Called by WorkerStore.sitStop() so
|
||||
/// "user stopped sit monitoring" silences the nag immediately.
|
||||
func stop() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
remaining = 0
|
||||
currentSound?.stop()
|
||||
currentSound = nil
|
||||
}
|
||||
|
||||
private func playOnce() {
|
||||
guard remaining > 0 else {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
remaining -= 1
|
||||
// byReference: true — load lazily from the system path each call.
|
||||
// The cost is negligible (< 1ms) and avoids keeping a 200KB AIFF
|
||||
// resident across the 45min idle stretch between alerts.
|
||||
if let sound = NSSound(contentsOf: morseURL, byReference: true) {
|
||||
sound.play()
|
||||
currentSound = sound
|
||||
} else {
|
||||
WorkerDebugLog.write("SoundPlayer: failed to load \(morseURL.path)")
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,7 +256,11 @@ final class WorkerStore: ObservableObject {
|
||||
title: "该起来动一下了",
|
||||
body: "你已连续坐了 \(sitTriggerMin) 分钟,起身喝口水吧。"
|
||||
)
|
||||
WorkerDebugLog.write("sit threshold notification fired (\(sitElapsedSec)s)")
|
||||
// 1Hz × 15 ticks of system Morse tone — the UN
|
||||
// notification ding is too easy to miss in a meeting,
|
||||
// so the sit alert gets a louder, longer signal.
|
||||
SoundPlayer.shared.playMorseSitAlert(count: 15)
|
||||
WorkerDebugLog.write("sit threshold notification + morse alert fired (\(sitElapsedSec)s)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -463,6 +467,9 @@ final class WorkerStore: ObservableObject {
|
||||
defaults.set(false, forKey: K.sitEnabled)
|
||||
defaults.set(0, forKey: K.sitAccumActive)
|
||||
defaults.set(0.0, forKey: K.sitLastNotified)
|
||||
// Silence any in-flight Morse alert from a prior trigger so the
|
||||
// user stopping monitoring also stops the nag immediately.
|
||||
SoundPlayer.shared.stop()
|
||||
}
|
||||
|
||||
func sitSetTrigger(_ min: Int) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user