diff --git a/AudioPlaybackKit/Package.swift b/AudioPlaybackKit/Package.swift deleted file mode 100644 index a3e2aee..0000000 --- a/AudioPlaybackKit/Package.swift +++ /dev/null @@ -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"]), - ] -) diff --git a/AudioPlaybackKit/README.md b/AudioPlaybackKit/README.md deleted file mode 100644 index 71bcdd3..0000000 --- a/AudioPlaybackKit/README.md +++ /dev/null @@ -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. diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift deleted file mode 100644 index d687a55..0000000 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift +++ /dev/null @@ -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 - } -} diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift deleted file mode 100644 index 9568ca2..0000000 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift +++ /dev/null @@ -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) - } - -} diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift deleted file mode 100644 index 71759f4..0000000 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift +++ /dev/null @@ -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 - } - -} diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift deleted file mode 100644 index 9ab4820..0000000 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift +++ /dev/null @@ -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)") - } - } -} diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift deleted file mode 100644 index 0e45408..0000000 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift +++ /dev/null @@ -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 - } -} diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift deleted file mode 100644 index 1c1528e..0000000 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift +++ /dev/null @@ -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 - } - } -} diff --git a/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift b/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift deleted file mode 100644 index 8411a81..0000000 --- a/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift +++ /dev/null @@ -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) - } -} diff --git a/TheNoiseClock.code-workspace b/TheNoiseClock.code-workspace new file mode 100644 index 0000000..b53107f --- /dev/null +++ b/TheNoiseClock.code-workspace @@ -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 + } +} diff --git a/TheNoiseClock.xcodeproj/project.pbxproj b/TheNoiseClock.xcodeproj/project.pbxproj index 79973b6..6ae4cb3 100644 --- a/TheNoiseClock.xcodeproj/project.pbxproj +++ b/TheNoiseClock.xcodeproj/project.pbxproj @@ -7,7 +7,8 @@ objects = { /* 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 */; }; EAF1C0DE2F3A4B5C0011223E /* TheNoiseClockWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EAF1C0DE2F3A4B5C00112234 /* TheNoiseClockWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -113,7 +114,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */, + EA756C5C2F465C3E006196BB /* AudioPlaybackKit in Frameworks */, + EA756C592F465C07006196BB /* Bedrock in Frameworks */, EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -196,8 +198,9 @@ ); name = TheNoiseClock; packageProductDependencies = ( - EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */, EAC051B02F2E64AB007F87EA /* Bedrock */, + EA756C582F465C07006196BB /* Bedrock */, + EA756C5B2F465C3E006196BB /* AudioPlaybackKit */, ); productName = TheNoiseClock; productReference = EA384AFB2E6E6B6000CA7D50 /* The Noise Clock.app */; @@ -305,8 +308,8 @@ mainGroup = EA384AF22E6E6B6000CA7D50; minimizedProjectReferenceProxies = 1; packageReferences = ( - EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */, - EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */, + EA756C572F465C07006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */, + EA756C5A2F465C3E006196BB /* XCLocalSwiftPackageReference "../_Packages/AudioPlaybackKit" */, ); preferredProjectObjectVersion = 77; productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */; @@ -438,7 +441,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -504,7 +507,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -772,20 +775,23 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */ = { + EA756C572F465C07006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */ = { isa = XCLocalSwiftPackageReference; - relativePath = AudioPlaybackKit; + relativePath = ../_Packages/Bedrock; }; - EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */ = { + EA756C5A2F465C3E006196BB /* XCLocalSwiftPackageReference "../_Packages/AudioPlaybackKit" */ = { isa = XCLocalSwiftPackageReference; - relativePath = ../Bedrock; + relativePath = ../_Packages/AudioPlaybackKit; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */ = { + EA756C582F465C07006196BB /* Bedrock */ = { + isa = XCSwiftPackageProductDependency; + productName = Bedrock; + }; + EA756C5B2F465C3E006196BB /* AudioPlaybackKit */ = { isa = XCSwiftPackageProductDependency; - package = EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */; productName = AudioPlaybackKit; }; EAC051B02F2E64AB007F87EA /* Bedrock */ = { diff --git a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index 528733b..1bbd017 100644 --- a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ TheNoiseClock.xcscheme_^#shared#^_ orderHint - 2 + 0 TheNoiseClockWidget.xcscheme_^#shared#^_ orderHint - 3 + 1 diff --git a/TheNoiseClock/Localizable.xcstrings b/TheNoiseClock/Localizable.xcstrings index 279fd32..4b6103f 100644 --- a/TheNoiseClock/Localizable.xcstrings +++ b/TheNoiseClock/Localizable.xcstrings @@ -3454,7 +3454,6 @@ } }, "settings.debug.branding_preview.subtitle" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3477,7 +3476,6 @@ } }, "settings.debug.branding_preview.title" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3500,7 +3498,6 @@ } }, "settings.debug.icon_generator.subtitle" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3523,7 +3520,6 @@ } }, "settings.debug.icon_generator.title" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3546,7 +3542,6 @@ } }, "settings.debug.reset_onboarding.subtitle" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3569,7 +3564,6 @@ } }, "settings.debug.reset_onboarding.title" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3592,7 +3586,6 @@ } }, "settings.debug.section_title" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : {