You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
10 KiB
273 lines
10 KiB
//
|
|
// Sound.swift
|
|
// LeCountdown
|
|
//
|
|
// Created by Laurent Morvillier on 30/01/2023.
|
|
//
|
|
|
|
import Foundation
|
|
import AVFoundation
|
|
|
|
protocol Playable: StringRepresentable, Equatable, Hashable {
|
|
var soundList: Set<Sound> { get }
|
|
}
|
|
|
|
extension Playlist: Playable {
|
|
var stringValue: String { self.rawValue }
|
|
var soundList: Set<Sound> {
|
|
return Set(SoundCatalog.main.sounds(for: self))
|
|
}
|
|
}
|
|
extension Sound: Playable {
|
|
var stringValue: String { self.rawValue.formatted() }
|
|
var soundList: Set<Sound> {
|
|
return [self]
|
|
}
|
|
}
|
|
|
|
protocol Localized {
|
|
var localizedString: String { get }
|
|
}
|
|
|
|
class SoundCatalog {
|
|
|
|
static let main: SoundCatalog = SoundCatalog()
|
|
|
|
fileprivate var _soundsByPlaylist: [Playlist : [Sound]] = [:]
|
|
|
|
init() {
|
|
self._soundsByPlaylist = Dictionary(grouping: Sound.allCases) { $0.playlist }
|
|
}
|
|
|
|
func sounds(for playlist: Playlist) -> [Sound] {
|
|
switch playlist {
|
|
case .shorts:
|
|
return [.FF_SH_bowl_drone_tap_hold_E, .FF_SH_bowl_drone_tapping_C, .EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab, .ESM_Ambient_Game_Menu_Soft_Wood]
|
|
default:
|
|
return self._soundsByPlaylist[playlist] ?? []
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
enum Catalog {
|
|
case ring
|
|
case confirmation
|
|
|
|
var playlists: [Playlist] {
|
|
switch self {
|
|
case .ring: return [.stephanBodzin, .nature, .relax]
|
|
case .confirmation: return [.shorts]
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Playlist: String, CaseIterable, Identifiable, Localized {
|
|
|
|
var id: String { return self.rawValue }
|
|
|
|
case custom
|
|
case nature
|
|
case stephanBodzin
|
|
case relax
|
|
case shorts
|
|
|
|
var localizedString: String {
|
|
switch self {
|
|
case .nature:
|
|
return NSLocalizedString("Nature", comment: "")
|
|
case .stephanBodzin:
|
|
return "Stephan Bodzin - Boavista"
|
|
case .custom:
|
|
return NSLocalizedString("Custom", comment: "")
|
|
case .relax:
|
|
return NSLocalizedString("Relax", comment: "")
|
|
case .shorts:
|
|
return NSLocalizedString("Confirmation", comment: "")
|
|
}
|
|
}
|
|
|
|
var shortName: String {
|
|
switch self {
|
|
case .stephanBodzin:
|
|
return "Boavista"
|
|
default:
|
|
return self.localizedString
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Sound id are stored thus case order should not be changed
|
|
enum Sound: Int, CaseIterable, Identifiable, Localized {
|
|
|
|
var id: Int { return self.rawValue }
|
|
|
|
// StephanBodzin
|
|
case sbSEM_Synths_Loop4_Nothing_Like_You
|
|
case sbLoop_ToneSD_Boavista
|
|
case sbArpeggio_Loop_River
|
|
case sbSquareArp_Loop_River
|
|
case sbHighChords_Loop_River
|
|
case sbMatriarchFxs_Loop2_Collider
|
|
// Relax
|
|
case FF_SH_bowl_drone_tapping_C
|
|
case FF_SH_bowl_drone_tap_hold_E
|
|
case EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab
|
|
case EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm
|
|
case EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am
|
|
case EX_ATSM_Bell_Binaural_Flam_Eb
|
|
case EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am
|
|
case EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm
|
|
// Nature
|
|
case rain_soft
|
|
case stream1
|
|
case stream2
|
|
case surf1
|
|
case crickets
|
|
case tropicalForestMorning
|
|
case desertMorning
|
|
case wetland
|
|
case riparianZone
|
|
// Shorts
|
|
case ESM_Ambient_Game_Menu_Soft_Wood
|
|
case sbRose
|
|
case trancosoBowl
|
|
case natureOceanShore
|
|
|
|
static var `default`: Sound { .sbSEM_Synths_Loop4_Nothing_Like_You }
|
|
|
|
var localizedString: String {
|
|
switch self {
|
|
case .sbSEM_Synths_Loop4_Nothing_Like_You: return "Nothing Like You"
|
|
case .sbLoop_ToneSD_Boavista: return "Boavista"
|
|
case .sbArpeggio_Loop_River: return "River 1"
|
|
case .sbSquareArp_Loop_River: return "River 2"
|
|
case .sbHighChords_Loop_River: return "River 3"
|
|
case .sbMatriarchFxs_Loop2_Collider: return "Collider"
|
|
case .FF_SH_bowl_drone_tapping_C: return "Bowl 1"
|
|
case .FF_SH_bowl_drone_tap_hold_E: return "Bowl 2"
|
|
case .EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm: return "Koshi Chimes 1"
|
|
case .EX_ATSM_Bell_Binaural_Flam_Eb: return "Bell Binaural"
|
|
case .EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am: return "Sansula"
|
|
case .EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm: return "Chimey percussion"
|
|
case .EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am: return "Koshi Chimes 2"
|
|
case .EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab: return "Bowl 3"
|
|
case .rain_soft: return "Rain"
|
|
case .stream1: return "Stream 1"
|
|
case .stream2: return "Stream 2"
|
|
case .surf1: return "Surf 1"
|
|
case .crickets: return "Crickets"
|
|
case .tropicalForestMorning: return "Forest morning 1"
|
|
case .desertMorning: return "Desert morning 2"
|
|
case .wetland: return "Wetland"
|
|
case .riparianZone: return "Riparian Zone"
|
|
case .ESM_Ambient_Game_Menu_Soft_Wood: return "Wood percussion"
|
|
case .sbRose: return "Rose"
|
|
case .trancosoBowl: return "Bowl 4"
|
|
case .natureOceanShore: return "Ocean Shore"
|
|
}
|
|
}
|
|
|
|
var fileName: String {
|
|
switch self {
|
|
case .sbSEM_Synths_Loop4_Nothing_Like_You: return "SEM_Synths_Loop4_Nothing_Like_You.wav"
|
|
case .sbLoop_ToneSD_Boavista: return "Loop_ToneSD_Boavista.wav"
|
|
case .sbArpeggio_Loop_River: return "Arpeggio_Loop_River.wav"
|
|
case .sbSquareArp_Loop_River: return "SquareArp_Loop_River.wav"
|
|
case .sbHighChords_Loop_River: return "HighChords_Loop_River.wav"
|
|
case .sbMatriarchFxs_Loop2_Collider: return "MatriarchFxs_Loop2_Collider.wav"
|
|
case .FF_SH_bowl_drone_tapping_C: return "FF_SH_bowl_drone_tapping_C.wav"
|
|
case .FF_SH_bowl_drone_tap_hold_E: return "FF_SH_bowl_drone_tap_hold_E.wav"
|
|
case .EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm: return "EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm.wav"
|
|
case .EX_ATSM_Bell_Binaural_Flam_Eb: return "EX_ATSM_Bell_Binaural_Flam_Eb.wav"
|
|
case .EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am: return "EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am.wav"
|
|
case .EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm: return "EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm.wav"
|
|
case .EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am: return "EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am.wav"
|
|
case .EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab: return "EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab.wav"
|
|
case .rain_soft: return "QP01 0011 Rain soft.wav"
|
|
case .stream1: return "QP01 0017 Stream sparkling.wav"
|
|
case .stream2: return "QP01 0018 Stream moderate.wav"
|
|
case .surf1: return "QP01 0023 Surf moderate sandy.wav"
|
|
case .crickets: return "QP01 0028 Insect crickets isolated.wav"
|
|
case .tropicalForestMorning: return "QP01 0037 Tropical forest morning.wav"
|
|
case .desertMorning: return "QP01 0130 Desert morning bird chorus.wav"
|
|
case .wetland: return "QP01 0096 Wetland lake early morning.wav"
|
|
case .riparianZone: return "QP01 0118 Riparian Zone thrush.wav"
|
|
case .ESM_Ambient_Game_Menu_Soft_Wood: return "ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav"
|
|
case .sbRose: return "rose1.mp3"
|
|
case .trancosoBowl: return "trancoso_bowl1.mp3"
|
|
case .natureOceanShore: return "QP01 0075 Ocean shore waves delicate birds.wav"
|
|
}
|
|
}
|
|
|
|
var playlist: Playlist {
|
|
switch self {
|
|
case .sbSEM_Synths_Loop4_Nothing_Like_You, .sbLoop_ToneSD_Boavista, .sbArpeggio_Loop_River, .sbSquareArp_Loop_River, .sbHighChords_Loop_River, .sbMatriarchFxs_Loop2_Collider, .sbRose:
|
|
return .stephanBodzin
|
|
case .FF_SH_bowl_drone_tapping_C, .FF_SH_bowl_drone_tap_hold_E, .EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm, .EX_ATSM_Bell_Binaural_Flam_Eb, .EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am, .EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm, .EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am, .EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab, .trancosoBowl:
|
|
return .relax
|
|
case .rain_soft, .stream1, .stream2, .surf1, .crickets, .tropicalForestMorning, .desertMorning, .wetland, .riparianZone, .natureOceanShore:
|
|
return .nature
|
|
case .ESM_Ambient_Game_Menu_Soft_Wood:
|
|
return .shorts
|
|
}
|
|
}
|
|
|
|
var isRestricted: Bool {
|
|
switch self {
|
|
case .sbSEM_Synths_Loop4_Nothing_Like_You, .sbLoop_ToneSD_Boavista, .FF_SH_bowl_drone_tapping_C, .EX_ATSM_Bell_Binaural_Flam_Eb, .tropicalForestMorning, .rain_soft, .ESM_Ambient_Game_Menu_Soft_Wood:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
var url: URL? {
|
|
let components = self.fileName.components(separatedBy: ".")
|
|
if components.count == 2 {
|
|
return Bundle.main.url(forResource: components[0],
|
|
withExtension: components[1])
|
|
} else {
|
|
print("bad sound file name for \(self)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func soundFile() throws -> SoundFile {
|
|
return try SoundFile(fullName: self.fileName)
|
|
}
|
|
|
|
func duration() async throws -> TimeInterval {
|
|
|
|
guard let url = self.url else {
|
|
print("sound \(self) has no url")
|
|
return -1.0
|
|
}
|
|
|
|
let audioAsset = AVURLAsset.init(url: url, options: nil)
|
|
let duration = try await audioAsset.load(.duration)
|
|
return CMTimeGetSeconds(duration)
|
|
}
|
|
|
|
static func computeSoundDurationsIfNecessary() {
|
|
Task {
|
|
for sound in Sound.allCases {
|
|
if Preferences.soundDurations[sound.fileName] == nil {
|
|
if let duration = try? await sound.duration() {
|
|
Preferences.soundDurations[sound.fileName] = duration
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var formattedDuration: String {
|
|
if let duration = Preferences.soundDurations[self.fileName] {
|
|
return duration.minuteSecond
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
}
|
|
|