Skip to content
53 changes: 53 additions & 0 deletions ACKNOWLEDGEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Acknowledgements

Third-party material bundled with Loop, and the terms it is used under.

## Alarm and alert sounds

The themed glucose alarm sets (`acoustic_*.caf`, `electronic_*.caf`) and the
one-off tones (`glass_bell.caf`, `harp.caf`, `meow.caf`, `saw_wave.caf`,
`hard_saw_wave.caf`, `sine_wave.caf`, `open_can.caf`, `open_wrapper.caf`) were
created for the Trio project by **an anonymous diabetes dad and Trio user**.

- Licence: **Creative Commons Zero 1.0 Universal (CC0 1.0)** —
<https://creativecommons.org/publicdomain/zero/1.0/>
- The author states that they are the author of these files, that they hold
the right to license them, and that they contain no third-party material
they cannot license. The CC0 dedication is what allows them to be carried
here as well as in Trio.
- CC0 requires no attribution. The credit above is given at the author's
request; they have asked to remain anonymous.
- Changes made: the one-off tones were looped by whole repeats to at least
five seconds, so that every alarm sounds for a comparable time; the themed
sets were left as delivered, since their length and level carry the urgency
along with the timbre. All were then encoded to IMA4 `.caf`.

The spoken alert sounds are generated from third-party voice models and are
*not* covered by that dedication — see below.

## Voice alerts

The spoken alert sounds were generated with
[Piper](https://github.com/rhasspy/piper) using voices from the
[rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices) collection.

### Amy (`voice_*_amy.caf`)

- Model: `en/en_US/amy/medium`
- Source dataset: [MycroftAI/mimic3-voices](https://github.com/MycroftAI/mimic3-voices)
- Licence: **Creative Commons Attribution-ShareAlike 4.0 International
(CC BY-SA 4.0)** — <https://creativecommons.org/licenses/by-sa/4.0/>
- Changes made: the generated phrases were looped to a minimum duration of
five seconds so a spoken alert lasts long enough to be heard as an alarm,
then encoded to IMA4 `.caf`. No other processing was applied.
- These audio files are made available under CC BY-SA 4.0. This does not
change the licence of Loop itself.

### Bryce (`voice_*_bryce.caf`)

- Model: `en/en_US/bryce/medium`
- Source: <https://brycebeattie.com> (training data released into the public
domain by the speaker)
- Licence: **Public domain.** Credited here as a courtesy; no attribution is
required.
- Changes made: looped and encoded as above.
3 changes: 2 additions & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Loop is released under the MIT license (MIT)
With exceptions for frameworks and graphics, noted below.
With exceptions for frameworks and graphics, noted below, and for the bundled
alarm sounds and voices, listed in [ACKNOWLEDGEMENTS.md](ACKNOWLEDGEMENTS.md).

Copyright (c) 2015 Nathan Racklyeft
Copyright (c) 2016 LoopKit Authors
Expand Down
164 changes: 164 additions & 0 deletions Loop.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

46 changes: 32 additions & 14 deletions Loop/Managers/Alerts/CriticalAlertAudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,29 @@ final class CriticalAlertAudioPlayer {
/// `SystemVolumeBooster`.
private let volumeBooster = SystemVolumeBooster()

/// How long the alarm plays before stopping on its own. A short burst
/// (a few loops of the ~0.84s tone) is attention-grabbing without being
/// a runaway siren — the user-visible UNNotification persists, and the
/// alert re-fires on the next critical-alert trigger if the condition
/// holds.
private let maxDuration: TimeInterval = 6
/// Shortest the alarm plays before stopping on its own. A short burst is
/// attention-grabbing without being a runaway siren — the user-visible
/// UNNotification persists, and the alert re-fires on the next
/// critical-alert trigger if the condition holds.
///
/// It is a floor, not a cap: the burst runs whole passes of the sound and
/// keeps going until it has reached this, so a 0.84s tone repeats about
/// eight times while a 10.8s alarm plays through once. Cutting a long
/// alarm off mid-phrase would throw away the part of it that carries the
/// urgency, which is why the length can't simply be fixed here.
static let minBurstDuration: TimeInterval = 6

/// Hard ceiling on a burst, so an unexpectedly long sound can't turn into
/// a siren that runs for minutes. Also the longest sound AlarmKit and
/// `UNNotificationSound` will play, so nothing in the catalog reaches it.
static let maxBurstDuration: TimeInterval = 30

/// Whole passes of `duration`, enough to reach `minBurstDuration`, capped.
nonisolated static func burstDuration(forSoundOfDuration duration: TimeInterval) -> TimeInterval {
guard duration > 0 else { return minBurstDuration }
let passes = max(1, Int((minBurstDuration / duration).rounded(.up)))
return min(maxBurstDuration, duration * Double(passes))
}

/// True if we're currently looping audio for a critical alert.
var isPlaying: Bool { player?.isPlaying ?? false }
Expand All @@ -78,7 +95,9 @@ final class CriticalAlertAudioPlayer {
// everything (audio + vibration) to maxDuration, so the alarm still
// buzzes even if audio setup below fails.
startVibration()
stopTimer = Timer.scheduledTimer(withTimeInterval: maxDuration, repeats: false) { [weak self] _ in
// Bounds vibration if the audio setup below fails. Re-armed to the
// sound's own burst length once we know how long it is.
stopTimer = Timer.scheduledTimer(withTimeInterval: Self.minBurstDuration, repeats: false) { [weak self] _ in
Task { @MainActor in self?.stop() }
}

Expand Down Expand Up @@ -128,14 +147,13 @@ final class CriticalAlertAudioPlayer {
return
}
player = p
if !boostVolume {
stopTimer?.invalidate()
stopTimer = Timer.scheduledTimer(withTimeInterval: p.duration, repeats: false) { [weak self] _ in
Task { @MainActor in self?.stop() }
}
let burst = boostVolume ? Self.burstDuration(forSoundOfDuration: p.duration) : p.duration
stopTimer?.invalidate()
stopTimer = Timer.scheduledTimer(withTimeInterval: burst, repeats: false) { [weak self] _ in
Task { @MainActor in self?.stop() }
}
os_log("Started critical-alert audio playback (duration=%{public}.2fs playing=%{public}@)",
log: log, type: .info, p.duration, p.isPlaying ? "yes" : "no")
os_log("Started critical-alert audio playback (duration=%{public}.2fs burst=%{public}.2fs playing=%{public}@)",
log: log, type: .info, p.duration, burst, p.isPlaying ? "yes" : "no")
} catch {
os_log("Failed to start audio playback: %{public}@", log: log, type: .error, String(describing: error))
}
Expand Down
Loading