Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0dde5bd66c |
@ -1,33 +0,0 @@
|
|||||||
// swift-tools-version: 5.9
|
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
||||||
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "AudioPlaybackKit",
|
|
||||||
platforms: [
|
|
||||||
.iOS(.v17),
|
|
||||||
.tvOS(.v17)
|
|
||||||
],
|
|
||||||
products: [
|
|
||||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
|
||||||
.library(
|
|
||||||
name: "AudioPlaybackKit",
|
|
||||||
targets: ["AudioPlaybackKit"]),
|
|
||||||
],
|
|
||||||
dependencies: [
|
|
||||||
// Dependencies declare other packages that this package depends on.
|
|
||||||
// .package(url: /* package url */, from: "1.0.0"),
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
|
||||||
.target(
|
|
||||||
name: "AudioPlaybackKit",
|
|
||||||
dependencies: []
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "AudioPlaybackKitTests",
|
|
||||||
dependencies: ["AudioPlaybackKit"]),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
# AudioPlaybackKit
|
|
||||||
|
|
||||||
A Swift package for audio playback functionality, specifically designed for white noise and ambient sound applications.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Audio Playback**: High-quality audio playback with background support
|
|
||||||
- **Sound Management**: Load and manage audio files from bundles
|
|
||||||
- **Wake Lock**: Prevent device from sleeping during audio playback
|
|
||||||
- **Interruption Handling**: Automatic handling of audio interruptions (calls, etc.)
|
|
||||||
- **Configuration**: JSON-based sound configuration system
|
|
||||||
- **Preview Support**: Built-in audio preview functionality
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- iOS 16.0+ / macOS 13.0+
|
|
||||||
- Swift 5.9+
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Swift Package Manager
|
|
||||||
|
|
||||||
Add the following to your `Package.swift` file:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
dependencies: [
|
|
||||||
.package(url: "https://github.com/yourusername/AudioPlaybackKit.git", from: "1.0.0")
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Or add it through Xcode:
|
|
||||||
1. File → Add Package Dependencies
|
|
||||||
2. Enter the repository URL
|
|
||||||
3. Select the version and add to your target
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Audio Playback
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import AudioPlaybackKit
|
|
||||||
|
|
||||||
// Get the shared player instance
|
|
||||||
let player = NoisePlayer.shared
|
|
||||||
|
|
||||||
// Create a sound
|
|
||||||
let sound = Sound(
|
|
||||||
name: "White Noise",
|
|
||||||
fileName: "white-noise.mp3",
|
|
||||||
category: "ambient",
|
|
||||||
description: "Classic white noise"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Play the sound
|
|
||||||
player.playSound(sound)
|
|
||||||
|
|
||||||
// Stop the sound
|
|
||||||
player.stopSound()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the ViewModel
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import AudioPlaybackKit
|
|
||||||
|
|
||||||
// Create a view model
|
|
||||||
let viewModel = NoiseViewModel()
|
|
||||||
|
|
||||||
// Get available sounds
|
|
||||||
let sounds = viewModel.availableSounds
|
|
||||||
|
|
||||||
// Play a sound
|
|
||||||
viewModel.playSound(sounds.first!)
|
|
||||||
|
|
||||||
// Preview a sound (3 seconds)
|
|
||||||
viewModel.previewSound(sounds.first!)
|
|
||||||
|
|
||||||
// Stop playback
|
|
||||||
viewModel.stopSound()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sound Configuration
|
|
||||||
|
|
||||||
Create a `sounds.json` file in your app bundle:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sounds": [
|
|
||||||
{
|
|
||||||
"id": "white-noise",
|
|
||||||
"name": "White Noise",
|
|
||||||
"fileName": "white-noise.mp3",
|
|
||||||
"category": "ambient",
|
|
||||||
"description": "Classic white noise for focus and relaxation",
|
|
||||||
"bundleName": "Ambient"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"id": "ambient",
|
|
||||||
"name": "Ambient",
|
|
||||||
"description": "General ambient sounds",
|
|
||||||
"bundleName": "Ambient"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"defaultVolume": 0.8,
|
|
||||||
"defaultLoopCount": -1,
|
|
||||||
"preloadSounds": true,
|
|
||||||
"preloadStrategy": "category",
|
|
||||||
"audioSessionCategory": "playback",
|
|
||||||
"audioSessionMode": "default",
|
|
||||||
"audioSessionOptions": ["mixWithOthers"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Wake Lock Service
|
|
||||||
|
|
||||||
```swift
|
|
||||||
import AudioPlaybackKit
|
|
||||||
|
|
||||||
let wakeLock = WakeLockService.shared
|
|
||||||
|
|
||||||
// Enable wake lock (prevents device from sleeping)
|
|
||||||
wakeLock.enableWakeLock()
|
|
||||||
|
|
||||||
// Disable wake lock
|
|
||||||
wakeLock.disableWakeLock()
|
|
||||||
|
|
||||||
// Check if active
|
|
||||||
if wakeLock.isActive {
|
|
||||||
print("Wake lock is active")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
- **NoisePlayer**: Main audio playback service
|
|
||||||
- **SoundConfigurationService**: Manages sound configuration and loading
|
|
||||||
- **WakeLockService**: Prevents device from sleeping during playback
|
|
||||||
- **NoiseViewModel**: SwiftUI-friendly view model for audio playback
|
|
||||||
|
|
||||||
### Models
|
|
||||||
|
|
||||||
- **Sound**: Represents an audio file with metadata
|
|
||||||
- **SoundConfiguration**: JSON configuration structure
|
|
||||||
- **AudioSettings**: Audio session and playback settings
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This package is available under the MIT license. See the LICENSE file for more info.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
//
|
|
||||||
// AudioConstants.swift
|
|
||||||
// AudioPlaybackKit
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 9/8/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AVFAudio
|
|
||||||
|
|
||||||
/// Audio-related constants and configuration
|
|
||||||
public enum AudioConstants {
|
|
||||||
|
|
||||||
// MARK: - Audio Session Configuration
|
|
||||||
public enum AudioSession {
|
|
||||||
public static let category = AVAudioSession.Category.playback
|
|
||||||
public static let mode = AVAudioSession.Mode.default
|
|
||||||
public static let options: AVAudioSession.CategoryOptions = [.mixWithOthers]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Playback Settings
|
|
||||||
public enum Playback {
|
|
||||||
public static let numberOfLoops = -1 // Infinite loop
|
|
||||||
public static let prepareToPlay = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Volume
|
|
||||||
public enum Volume {
|
|
||||||
public static let min: Float = 0.0
|
|
||||||
public static let max: Float = 1.0
|
|
||||||
public static let `default`: Float = 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
//
|
|
||||||
// Sound.swift
|
|
||||||
// AudioPlaybackKit
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 9/8/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// Sound data model for audio files
|
|
||||||
public struct Sound: Identifiable, Hashable, Codable {
|
|
||||||
public let id: String
|
|
||||||
public let name: String
|
|
||||||
public let fileName: String
|
|
||||||
public let category: String
|
|
||||||
public let description: String
|
|
||||||
public let bundleName: String? // Optional bundle name for organization
|
|
||||||
public let isDefault: Bool? // Optional - used for alarm sounds to mark default
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
public init(id: String? = nil, name: String, fileName: String, category: String, description: String, bundleName: String? = nil, isDefault: Bool? = nil) {
|
|
||||||
self.id = id ?? UUID().uuidString // Use provided id or generate GUID
|
|
||||||
self.name = name
|
|
||||||
self.fileName = fileName
|
|
||||||
self.category = category
|
|
||||||
self.description = description
|
|
||||||
self.bundleName = bundleName
|
|
||||||
self.isDefault = isDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Codable Custom Implementation
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
// Generate a new GUID for each sound loaded from JSON
|
|
||||||
self.id = UUID().uuidString
|
|
||||||
|
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
|
||||||
self.fileName = try container.decode(String.self, forKey: .fileName)
|
|
||||||
self.category = try container.decode(String.self, forKey: .category)
|
|
||||||
self.description = try container.decode(String.self, forKey: .description)
|
|
||||||
self.bundleName = try container.decodeIfPresent(String.self, forKey: .bundleName)
|
|
||||||
self.isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
try container.encode(id, forKey: .id)
|
|
||||||
try container.encode(name, forKey: .name)
|
|
||||||
try container.encode(fileName, forKey: .fileName)
|
|
||||||
try container.encode(category, forKey: .category)
|
|
||||||
try container.encode(description, forKey: .description)
|
|
||||||
try container.encodeIfPresent(bundleName, forKey: .bundleName)
|
|
||||||
try container.encodeIfPresent(isDefault, forKey: .isDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case id, name, fileName, category, description, bundleName, isDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Hashable
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
//
|
|
||||||
// SoundConfiguration.swift
|
|
||||||
// AudioPlaybackKit
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 9/8/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// Configuration model for sound system loaded from JSON
|
|
||||||
public struct SoundConfiguration: Codable {
|
|
||||||
public let sounds: [Sound]
|
|
||||||
public let settings: AudioSettings
|
|
||||||
|
|
||||||
public init(sounds: [Sound], settings: AudioSettings) {
|
|
||||||
self.sounds = sounds
|
|
||||||
self.settings = settings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simple struct for loading just the sounds array from category JSON files
|
|
||||||
public struct SoundsOnly: Codable {
|
|
||||||
public let sounds: [Sound]
|
|
||||||
|
|
||||||
public init(sounds: [Sound]) {
|
|
||||||
self.sounds = sounds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Audio settings configuration
|
|
||||||
public struct AudioSettings: Codable {
|
|
||||||
public let defaultVolume: Float
|
|
||||||
public let defaultLoopCount: Int
|
|
||||||
public let preloadSounds: Bool
|
|
||||||
public let preloadStrategy: String // "all", "category", "none"
|
|
||||||
public let audioSessionCategory: String
|
|
||||||
public let audioSessionMode: String
|
|
||||||
public let audioSessionOptions: [String]
|
|
||||||
|
|
||||||
public init(defaultVolume: Float, defaultLoopCount: Int, preloadSounds: Bool, preloadStrategy: String, audioSessionCategory: String, audioSessionMode: String, audioSessionOptions: [String]) {
|
|
||||||
self.defaultVolume = defaultVolume
|
|
||||||
self.defaultLoopCount = defaultLoopCount
|
|
||||||
self.preloadSounds = preloadSounds
|
|
||||||
self.preloadStrategy = preloadStrategy
|
|
||||||
self.audioSessionCategory = audioSessionCategory
|
|
||||||
self.audioSessionMode = audioSessionMode
|
|
||||||
self.audioSessionOptions = audioSessionOptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Service for loading and managing sound configuration
|
|
||||||
public class SoundConfigurationService {
|
|
||||||
public static let shared = SoundConfigurationService()
|
|
||||||
|
|
||||||
private var configuration: SoundConfiguration?
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
|
|
||||||
/// Load audio settings from SoundsSettings.json
|
|
||||||
private func loadAudioSettings(from bundle: Bundle = .main) -> AudioSettings {
|
|
||||||
guard let url = bundle.url(forResource: "SoundsSettings", withExtension: "json") else {
|
|
||||||
print("⚠️ Warning: SoundsSettings.json not found, using default settings")
|
|
||||||
return AudioSettings(
|
|
||||||
defaultVolume: 0.8,
|
|
||||||
defaultLoopCount: -1,
|
|
||||||
preloadSounds: true,
|
|
||||||
preloadStrategy: "category",
|
|
||||||
audioSessionCategory: "playback",
|
|
||||||
audioSessionMode: "default",
|
|
||||||
audioSessionOptions: ["mixWithOthers"]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: url)
|
|
||||||
let settings = try JSONDecoder().decode(AudioSettings.self, from: data)
|
|
||||||
print("✅ Loaded audio settings from SoundsSettings.json")
|
|
||||||
return settings
|
|
||||||
} catch {
|
|
||||||
print("⚠️ Warning: Error loading audio settings, using defaults: \(error)")
|
|
||||||
return AudioSettings(
|
|
||||||
defaultVolume: 0.8,
|
|
||||||
defaultLoopCount: -1,
|
|
||||||
preloadSounds: true,
|
|
||||||
preloadStrategy: "category",
|
|
||||||
audioSessionCategory: "playback",
|
|
||||||
audioSessionMode: "default",
|
|
||||||
audioSessionOptions: ["mixWithOthers"]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load sound configuration from multiple category-specific JSON files
|
|
||||||
public func loadConfigurationFromBundles(from bundle: Bundle = .main) -> SoundConfiguration {
|
|
||||||
// Include AlarmSounds bundle for alarm sound preview functionality
|
|
||||||
let bundleNames = ["Colored", "Nature", "Mechanical", "Ambient", "AlarmSounds"]
|
|
||||||
var allSounds: [Sound] = []
|
|
||||||
|
|
||||||
for bundleName in bundleNames {
|
|
||||||
guard let bundleURL = bundle.url(forResource: bundleName, withExtension: "bundle"),
|
|
||||||
let categoryBundle = Bundle(url: bundleURL),
|
|
||||||
let url = categoryBundle.url(forResource: "sounds", withExtension: "json") else {
|
|
||||||
print("⚠️ Warning: Could not find sounds.json in \(bundleName).bundle")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: url)
|
|
||||||
let soundsOnly = try JSONDecoder().decode(SoundsOnly.self, from: data)
|
|
||||||
allSounds.append(contentsOf: soundsOnly.sounds)
|
|
||||||
|
|
||||||
print("✅ Loaded \(soundsOnly.sounds.count) sounds from \(bundleName).bundle")
|
|
||||||
} catch {
|
|
||||||
print("⚠️ Warning: Error loading sounds from \(bundleName).bundle: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load settings from separate file
|
|
||||||
let settings = loadAudioSettings(from: bundle)
|
|
||||||
|
|
||||||
let config = SoundConfiguration(sounds: allSounds, settings: settings)
|
|
||||||
self.configuration = config
|
|
||||||
print("✅ Loaded combined sound configuration with \(allSounds.count) sounds from \(bundleNames.count) bundles")
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current configuration
|
|
||||||
public func getConfiguration() -> SoundConfiguration {
|
|
||||||
if configuration == nil {
|
|
||||||
return loadConfigurationFromBundles()
|
|
||||||
}
|
|
||||||
return configuration!
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all available sounds
|
|
||||||
public func getAvailableSounds() -> [Sound] {
|
|
||||||
return getConfiguration().sounds
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get sounds by category
|
|
||||||
public func getSoundsByCategory(_ categoryId: String) -> [Sound] {
|
|
||||||
return getConfiguration().sounds
|
|
||||||
.filter { $0.category == categoryId }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Get audio settings
|
|
||||||
public func getAudioSettings() -> AudioSettings {
|
|
||||||
return getConfiguration().settings
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,321 +0,0 @@
|
|||||||
//
|
|
||||||
// SoundPlayer.swift
|
|
||||||
// AudioPlaybackKit
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 9/8/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AVFoundation
|
|
||||||
import Observation
|
|
||||||
|
|
||||||
/// Audio playback service for sounds and ambient audio
|
|
||||||
@available(iOS 17.0, tvOS 17.0, *)
|
|
||||||
@Observable
|
|
||||||
public class SoundPlayer {
|
|
||||||
|
|
||||||
// MARK: - Singleton
|
|
||||||
public static let shared = SoundPlayer()
|
|
||||||
|
|
||||||
// MARK: - Properties
|
|
||||||
private var players: [String: AVAudioPlayer] = [:]
|
|
||||||
private let playersLock = NSLock()
|
|
||||||
private var currentPlayer: AVAudioPlayer?
|
|
||||||
public private(set) var currentSound: Sound?
|
|
||||||
private var shouldResumeAfterInterruption = false
|
|
||||||
private let wakeLockService = WakeLockService.shared
|
|
||||||
private let soundConfigurationService = SoundConfigurationService.shared
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
private init() {
|
|
||||||
setupAudioSession()
|
|
||||||
setupAudioInterruptionHandling()
|
|
||||||
// Preload sounds off the main thread to avoid blocking UI during app launch
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
||||||
self?.preloadSounds()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
stopAllSounds()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public Interface
|
|
||||||
public var isPlaying: Bool {
|
|
||||||
return currentPlayer?.isPlaying ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
public func playSound(_ sound: Sound) {
|
|
||||||
playSound(sound, volumeOverride: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func playSound(_ sound: Sound, volume: Float) {
|
|
||||||
playSound(sound, volumeOverride: volume)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func playSound(_ sound: Sound, volumeOverride: Float?) {
|
|
||||||
print("🎵 Attempting to play: \(sound.name)")
|
|
||||||
|
|
||||||
// Stop current sound if playing
|
|
||||||
stopSound()
|
|
||||||
|
|
||||||
// Store current sound for interruption handling
|
|
||||||
currentSound = sound
|
|
||||||
|
|
||||||
// Get or create player for this sound
|
|
||||||
playersLock.lock()
|
|
||||||
let player = players[sound.fileName]
|
|
||||||
playersLock.unlock()
|
|
||||||
|
|
||||||
guard let player else {
|
|
||||||
playersLock.lock()
|
|
||||||
let availableKeys = Array(players.keys)
|
|
||||||
playersLock.unlock()
|
|
||||||
print("❌ Sound not preloaded: \(sound.fileName)")
|
|
||||||
print("📁 Available sounds: \(availableKeys)")
|
|
||||||
|
|
||||||
// Try to load the sound dynamically as fallback
|
|
||||||
guard let fileUrl = getURL(for: sound) else {
|
|
||||||
print("❌ Sound file not found: \(sound.fileName)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let newPlayer = try AVAudioPlayer(contentsOf: fileUrl)
|
|
||||||
newPlayer.numberOfLoops = AudioConstants.Playback.numberOfLoops
|
|
||||||
newPlayer.volume = volumeOverride ?? AudioConstants.Volume.default
|
|
||||||
newPlayer.prepareToPlay()
|
|
||||||
playersLock.lock()
|
|
||||||
players[sound.fileName] = newPlayer
|
|
||||||
playersLock.unlock()
|
|
||||||
currentPlayer = newPlayer
|
|
||||||
let success = newPlayer.play()
|
|
||||||
print("🎵 Fallback play result: \(success ? "SUCCESS" : "FAILED")")
|
|
||||||
return
|
|
||||||
} catch {
|
|
||||||
print("❌ Error creating fallback player: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPlayer = player
|
|
||||||
if let volumeOverride {
|
|
||||||
player.volume = volumeOverride
|
|
||||||
}
|
|
||||||
let success = player.play()
|
|
||||||
print("🎵 Play result: \(success ? "SUCCESS" : "FAILED")")
|
|
||||||
print("🔊 Player isPlaying: \(player.isPlaying)")
|
|
||||||
print("🔊 Player volume: \(player.volume)")
|
|
||||||
|
|
||||||
// Enable wake lock when playing audio to prevent device sleep
|
|
||||||
if success {
|
|
||||||
wakeLockService.enableWakeLock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func stopSound() {
|
|
||||||
currentPlayer?.stop()
|
|
||||||
currentPlayer = nil
|
|
||||||
shouldResumeAfterInterruption = false
|
|
||||||
|
|
||||||
// Disable wake lock when stopping audio
|
|
||||||
wakeLockService.disableWakeLock()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func clearCurrentSound() {
|
|
||||||
stopSound()
|
|
||||||
currentSound = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private Methods
|
|
||||||
|
|
||||||
/// Helper method to get URL for sound file, handling bundles and direct paths
|
|
||||||
private func getURL(for sound: Sound) -> URL? {
|
|
||||||
// If sound has a bundle name, look in that bundle first
|
|
||||||
if let bundleName = sound.bundleName {
|
|
||||||
if let bundleURL = Bundle.main.url(forResource: bundleName, withExtension: "bundle"),
|
|
||||||
let bundle = Bundle(url: bundleURL) {
|
|
||||||
return bundle.url(forResource: sound.fileName, withExtension: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to direct file path
|
|
||||||
if sound.fileName.contains("/") {
|
|
||||||
// Path includes subfolder (e.g., "Sounds/white-noise.mp3")
|
|
||||||
let components = sound.fileName.components(separatedBy: "/")
|
|
||||||
let fileName = components.last!
|
|
||||||
let subfolder = components.dropLast().joined(separator: "/")
|
|
||||||
return Bundle.main.url(forResource: fileName, withExtension: nil, subdirectory: subfolder)
|
|
||||||
} else {
|
|
||||||
// Direct file path (fallback)
|
|
||||||
if let url = Bundle.main.url(forResource: sound.fileName, withExtension: nil) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
// Alarm sounds live in a subdirectory; try that next
|
|
||||||
return Bundle.main.url(forResource: sound.fileName, withExtension: nil, subdirectory: "AlarmSounds")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupAudioSession() {
|
|
||||||
do {
|
|
||||||
let settings = soundConfigurationService.getAudioSettings()
|
|
||||||
|
|
||||||
// Use configuration settings or fall back to constants
|
|
||||||
let category = settings.audioSessionCategory == "playback" ?
|
|
||||||
AVAudioSession.Category.playback : AudioConstants.AudioSession.category
|
|
||||||
let mode = settings.audioSessionMode == "default" ?
|
|
||||||
AVAudioSession.Mode.default : AudioConstants.AudioSession.mode
|
|
||||||
let options: AVAudioSession.CategoryOptions = settings.audioSessionOptions.contains("mixWithOthers") == true ?
|
|
||||||
[.mixWithOthers] : AudioConstants.AudioSession.options
|
|
||||||
|
|
||||||
try AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: options)
|
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
|
|
||||||
// Configure for background audio playback
|
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
|
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
|
|
||||||
print("🔊 Audio session configured for background playback")
|
|
||||||
} catch {
|
|
||||||
print("Error setting up audio session: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func preloadSounds() {
|
|
||||||
print("📁 Preloading audio files...")
|
|
||||||
|
|
||||||
// Get sound configuration
|
|
||||||
let sounds = soundConfigurationService.getAvailableSounds()
|
|
||||||
let settings = soundConfigurationService.getAudioSettings()
|
|
||||||
|
|
||||||
for sound in sounds {
|
|
||||||
guard let fileUrl = getURL(for: sound) else {
|
|
||||||
print("❌ Sound file not found: \(sound.fileName)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let player = try AVAudioPlayer(contentsOf: fileUrl)
|
|
||||||
player.numberOfLoops = settings.defaultLoopCount
|
|
||||||
player.volume = settings.defaultVolume
|
|
||||||
if settings.preloadSounds {
|
|
||||||
player.prepareToPlay()
|
|
||||||
}
|
|
||||||
playersLock.lock()
|
|
||||||
players[sound.fileName] = player
|
|
||||||
playersLock.unlock()
|
|
||||||
print("✅ Loaded: \(sound.name) (\(sound.fileName))")
|
|
||||||
} catch {
|
|
||||||
print("❌ Error preloading sound \(sound.fileName): \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playersLock.lock()
|
|
||||||
let count = players.count
|
|
||||||
playersLock.unlock()
|
|
||||||
print("📁 Preloading complete. Loaded \(count) sounds.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func stopAllSounds() {
|
|
||||||
playersLock.lock()
|
|
||||||
for player in players.values {
|
|
||||||
player.stop()
|
|
||||||
}
|
|
||||||
players.removeAll()
|
|
||||||
playersLock.unlock()
|
|
||||||
currentPlayer = nil
|
|
||||||
currentSound = nil
|
|
||||||
shouldResumeAfterInterruption = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set up audio interruption handling to maintain playback
|
|
||||||
private func setupAudioInterruptionHandling() {
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(handleAudioInterruption),
|
|
||||||
name: AVAudioSession.interruptionNotification,
|
|
||||||
object: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(handleRouteChange),
|
|
||||||
name: AVAudioSession.routeChangeNotification,
|
|
||||||
object: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func handleAudioInterruption(notification: Notification) {
|
|
||||||
guard let userInfo = notification.userInfo,
|
|
||||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
||||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch type {
|
|
||||||
case .began:
|
|
||||||
// Audio was interrupted (e.g., phone call)
|
|
||||||
shouldResumeAfterInterruption = isPlaying
|
|
||||||
if isPlaying {
|
|
||||||
currentPlayer?.pause()
|
|
||||||
print("🔇 Audio interrupted - will resume after interruption ends")
|
|
||||||
}
|
|
||||||
|
|
||||||
case .ended:
|
|
||||||
// Audio interruption ended
|
|
||||||
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
||||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
||||||
if options.contains(.shouldResume) && shouldResumeAfterInterruption {
|
|
||||||
// Resume playback
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
self.resumePlayback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@unknown default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func handleRouteChange(notification: Notification) {
|
|
||||||
guard let userInfo = notification.userInfo,
|
|
||||||
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
||||||
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch reason {
|
|
||||||
case .oldDeviceUnavailable:
|
|
||||||
// Headphones were unplugged, etc.
|
|
||||||
if isPlaying {
|
|
||||||
shouldResumeAfterInterruption = true
|
|
||||||
currentPlayer?.pause()
|
|
||||||
print("🔇 Audio route changed - will resume when new route available")
|
|
||||||
}
|
|
||||||
|
|
||||||
case .newDeviceAvailable:
|
|
||||||
// New audio device available
|
|
||||||
if shouldResumeAfterInterruption {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
self.resumePlayback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resume playback after interruption
|
|
||||||
private func resumePlayback() {
|
|
||||||
guard shouldResumeAfterInterruption, let sound = currentSound else { return }
|
|
||||||
|
|
||||||
do {
|
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
playSound(sound)
|
|
||||||
shouldResumeAfterInterruption = false
|
|
||||||
print("🔊 Audio playback resumed after interruption")
|
|
||||||
} catch {
|
|
||||||
print("❌ Error resuming audio playback: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
//
|
|
||||||
// WakeLockService.swift
|
|
||||||
// AudioPlaybackKit
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 9/8/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
#if canImport(UIKit)
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
import Observation
|
|
||||||
|
|
||||||
/// Service to manage screen wake lock and prevent device from sleeping
|
|
||||||
@available(iOS 17.0, tvOS 17.0, *)
|
|
||||||
@Observable
|
|
||||||
public class WakeLockService {
|
|
||||||
|
|
||||||
// MARK: - Singleton
|
|
||||||
public static let shared = WakeLockService()
|
|
||||||
|
|
||||||
// MARK: - Properties
|
|
||||||
public private(set) var isWakeLockActive = false
|
|
||||||
private var wakeLockTimer: Timer?
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
disableWakeLock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public Interface
|
|
||||||
|
|
||||||
/// Enable wake lock to prevent device from sleeping
|
|
||||||
public func enableWakeLock() {
|
|
||||||
guard !isWakeLockActive else { return }
|
|
||||||
|
|
||||||
#if canImport(UIKit)
|
|
||||||
// Prevent device from sleeping
|
|
||||||
UIApplication.shared.isIdleTimerDisabled = true
|
|
||||||
|
|
||||||
// Set up periodic timer to maintain wake lock
|
|
||||||
wakeLockTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { _ in
|
|
||||||
// Keep the app active by briefly enabling/disabling idle timer
|
|
||||||
UIApplication.shared.isIdleTimerDisabled = false
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
UIApplication.shared.isIdleTimerDisabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
isWakeLockActive = true
|
|
||||||
print("🔒 Wake lock enabled - device will not sleep")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disable wake lock and allow device to sleep normally
|
|
||||||
public func disableWakeLock() {
|
|
||||||
guard isWakeLockActive else { return }
|
|
||||||
|
|
||||||
#if canImport(UIKit)
|
|
||||||
// Allow device to sleep normally
|
|
||||||
UIApplication.shared.isIdleTimerDisabled = false
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Stop the maintenance timer
|
|
||||||
wakeLockTimer?.invalidate()
|
|
||||||
wakeLockTimer = nil
|
|
||||||
|
|
||||||
isWakeLockActive = false
|
|
||||||
print("🔓 Wake lock disabled - device can sleep normally")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle wake lock state
|
|
||||||
public func toggleWakeLock() {
|
|
||||||
if isWakeLockActive {
|
|
||||||
disableWakeLock()
|
|
||||||
} else {
|
|
||||||
enableWakeLock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if wake lock is currently active
|
|
||||||
public var isActive: Bool {
|
|
||||||
return isWakeLockActive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
//
|
|
||||||
// SoundViewModel.swift
|
|
||||||
// AudioPlaybackKit
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 9/8/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Observation
|
|
||||||
|
|
||||||
/// ViewModel for sound/audio playback
|
|
||||||
@available(iOS 17.0, tvOS 17.0, *)
|
|
||||||
@Observable
|
|
||||||
public class SoundViewModel {
|
|
||||||
|
|
||||||
// MARK: - Properties
|
|
||||||
private let soundPlayer: SoundPlayer
|
|
||||||
private let soundConfigurationService: SoundConfigurationService
|
|
||||||
public var isPreviewing: Bool = false
|
|
||||||
public var previewSound: Sound?
|
|
||||||
|
|
||||||
public var selectedSound: Sound?
|
|
||||||
|
|
||||||
public var isPlaying: Bool {
|
|
||||||
soundPlayer.isPlaying
|
|
||||||
}
|
|
||||||
|
|
||||||
public var availableSounds: [Sound] {
|
|
||||||
return soundConfigurationService.getAvailableSounds()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
public init(soundPlayer: SoundPlayer = SoundPlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) {
|
|
||||||
self.soundPlayer = soundPlayer
|
|
||||||
self.soundConfigurationService = soundConfigurationService
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public Interface
|
|
||||||
public func playSound(_ sound: Sound) {
|
|
||||||
soundPlayer.playSound(sound)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func stopSound() {
|
|
||||||
soundPlayer.stopSound()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func selectSound(_ sound: Sound) {
|
|
||||||
// Stop any current playback when selecting a new sound
|
|
||||||
if isPlaying {
|
|
||||||
stopSound()
|
|
||||||
}
|
|
||||||
// Stop any preview
|
|
||||||
stopPreview()
|
|
||||||
|
|
||||||
selectedSound = sound
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Preview Functionality
|
|
||||||
public func previewSound(_ sound: Sound) {
|
|
||||||
// Stop any current preview
|
|
||||||
stopPreview()
|
|
||||||
|
|
||||||
// Set preview state
|
|
||||||
previewSound = sound
|
|
||||||
isPreviewing = true
|
|
||||||
|
|
||||||
// Play preview (3 seconds)
|
|
||||||
soundPlayer.playSound(sound)
|
|
||||||
|
|
||||||
// Auto-stop preview after 3 seconds
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
|
||||||
self.stopPreview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func stopPreview() {
|
|
||||||
if isPreviewing {
|
|
||||||
soundPlayer.stopSound()
|
|
||||||
isPreviewing = false
|
|
||||||
previewSound = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
//
|
|
||||||
// AudioPlaybackKitTests.swift
|
|
||||||
// AudioPlaybackKitTests
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 9/8/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import XCTest
|
|
||||||
@testable import AudioPlaybackKit
|
|
||||||
|
|
||||||
final class AudioPlaybackKitTests: XCTestCase {
|
|
||||||
|
|
||||||
func testSoundModel() throws {
|
|
||||||
let sound = Sound(
|
|
||||||
name: "Test Sound",
|
|
||||||
fileName: "test.mp3",
|
|
||||||
category: "test",
|
|
||||||
description: "Test description"
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertEqual(sound.name, "Test Sound")
|
|
||||||
XCTAssertEqual(sound.fileName, "test.mp3")
|
|
||||||
XCTAssertEqual(sound.category, "test")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSoundConfigurationService() throws {
|
|
||||||
let service = SoundConfigurationService.shared
|
|
||||||
let sounds = service.getAvailableSounds()
|
|
||||||
|
|
||||||
// Should have fallback sounds if no configuration is loaded
|
|
||||||
XCTAssertFalse(sounds.isEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testWakeLockService() throws {
|
|
||||||
let service = WakeLockService.shared
|
|
||||||
|
|
||||||
// Initially should not be active
|
|
||||||
XCTAssertFalse(service.isActive)
|
|
||||||
|
|
||||||
// Enable wake lock
|
|
||||||
service.enableWakeLock()
|
|
||||||
XCTAssertTrue(service.isActive)
|
|
||||||
|
|
||||||
// Disable wake lock
|
|
||||||
service.disableWakeLock()
|
|
||||||
XCTAssertFalse(service.isActive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
TheNoiseClock.code-workspace
Normal file
20
TheNoiseClock.code-workspace
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../_Packages/Bedrock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../_Packages/AudioPlaybackKit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.enablePersistentSessions": true,
|
||||||
|
"terminal.integrated.persistentSessionReviveProcess": "onExitAndWindowClose",
|
||||||
|
"task.allowAutomaticTasks": "off",
|
||||||
|
"swift.disableAutoResolve": true,
|
||||||
|
"swift.disableSwiftPackageManagerIntegration": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,8 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */; };
|
EA756C592F465C07006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA756C582F465C07006196BB /* Bedrock */; };
|
||||||
|
EA756C5C2F465C3E006196BB /* AudioPlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA756C5B2F465C3E006196BB /* AudioPlaybackKit */; };
|
||||||
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC051B02F2E64AB007F87EA /* Bedrock */; };
|
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC051B02F2E64AB007F87EA /* Bedrock */; };
|
||||||
EAF1C0DE2F3A4B5C0011223E /* TheNoiseClockWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EAF1C0DE2F3A4B5C00112234 /* TheNoiseClockWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
EAF1C0DE2F3A4B5C0011223E /* TheNoiseClockWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EAF1C0DE2F3A4B5C00112234 /* TheNoiseClockWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@ -113,7 +114,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */,
|
EA756C5C2F465C3E006196BB /* AudioPlaybackKit in Frameworks */,
|
||||||
|
EA756C592F465C07006196BB /* Bedrock in Frameworks */,
|
||||||
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */,
|
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -196,8 +198,9 @@
|
|||||||
);
|
);
|
||||||
name = TheNoiseClock;
|
name = TheNoiseClock;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */,
|
|
||||||
EAC051B02F2E64AB007F87EA /* Bedrock */,
|
EAC051B02F2E64AB007F87EA /* Bedrock */,
|
||||||
|
EA756C582F465C07006196BB /* Bedrock */,
|
||||||
|
EA756C5B2F465C3E006196BB /* AudioPlaybackKit */,
|
||||||
);
|
);
|
||||||
productName = TheNoiseClock;
|
productName = TheNoiseClock;
|
||||||
productReference = EA384AFB2E6E6B6000CA7D50 /* The Noise Clock.app */;
|
productReference = EA384AFB2E6E6B6000CA7D50 /* The Noise Clock.app */;
|
||||||
@ -305,8 +308,8 @@
|
|||||||
mainGroup = EA384AF22E6E6B6000CA7D50;
|
mainGroup = EA384AF22E6E6B6000CA7D50;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */,
|
EA756C572F465C07006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */,
|
||||||
EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */,
|
EA756C5A2F465C3E006196BB /* XCLocalSwiftPackageReference "../_Packages/AudioPlaybackKit" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */;
|
productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */;
|
||||||
@ -438,7 +441,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@ -504,7 +507,7 @@
|
|||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@ -772,20 +775,23 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */ = {
|
EA756C572F465C07006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = AudioPlaybackKit;
|
relativePath = ../_Packages/Bedrock;
|
||||||
};
|
};
|
||||||
EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */ = {
|
EA756C5A2F465C3E006196BB /* XCLocalSwiftPackageReference "../_Packages/AudioPlaybackKit" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = ../Bedrock;
|
relativePath = ../_Packages/AudioPlaybackKit;
|
||||||
};
|
};
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */ = {
|
EA756C582F465C07006196BB /* Bedrock */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Bedrock;
|
||||||
|
};
|
||||||
|
EA756C5B2F465C3E006196BB /* AudioPlaybackKit */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */;
|
|
||||||
productName = AudioPlaybackKit;
|
productName = AudioPlaybackKit;
|
||||||
};
|
};
|
||||||
EAC051B02F2E64AB007F87EA /* Bedrock */ = {
|
EAC051B02F2E64AB007F87EA /* Bedrock */ = {
|
||||||
|
|||||||
@ -7,12 +7,12 @@
|
|||||||
<key>TheNoiseClock.xcscheme_^#shared#^_</key>
|
<key>TheNoiseClock.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>TheNoiseClockWidget.xcscheme_^#shared#^_</key>
|
<key>TheNoiseClockWidget.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>3</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@ -3454,7 +3454,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.debug.branding_preview.subtitle" : {
|
"settings.debug.branding_preview.subtitle" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3477,7 +3476,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.debug.branding_preview.title" : {
|
"settings.debug.branding_preview.title" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3500,7 +3498,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.debug.icon_generator.subtitle" : {
|
"settings.debug.icon_generator.subtitle" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3523,7 +3520,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.debug.icon_generator.title" : {
|
"settings.debug.icon_generator.title" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3546,7 +3542,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.debug.reset_onboarding.subtitle" : {
|
"settings.debug.reset_onboarding.subtitle" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3569,7 +3564,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.debug.reset_onboarding.title" : {
|
"settings.debug.reset_onboarding.title" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@ -3592,7 +3586,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.debug.section_title" : {
|
"settings.debug.section_title" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user