Compare commits

..

1 Commits

Author SHA1 Message Date
0dde5bd66c removed package
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2026-02-18 14:48:08 -06:00
13 changed files with 41 additions and 1006 deletions

View File

@ -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"]),
]
)

View File

@ -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.

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)")
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View 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
}
}

View File

@ -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 */ = {

View File

@ -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>

View File

@ -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" : {