├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── CMiniAudio │ ├── include │ │ ├── miniaudio.h │ │ └── module.modulemap │ └── miniaudio.c ├── Lullaby │ ├── Audio │ │ ├── Basic.swift │ │ ├── Buffer.swift │ │ ├── Engine │ │ │ ├── DummyEngine.swift │ │ │ └── Engine.swift │ │ └── Signal.swift │ ├── Envelope │ │ ├── ADSR.swift │ │ ├── Envelope.swift │ │ └── Gate.swift │ ├── Oscillator │ │ ├── Oscillator+Basic.swift │ │ └── Oscillator.swift │ ├── Synth │ │ └── Synth.swift │ ├── Utility │ │ └── Print.swift │ └── Wave │ │ ├── LinearInterpolatedWavetable.swift │ │ ├── Wave+Basic.swift │ │ ├── Wave.swift │ │ └── Wavetable.swift ├── LullabyMiniAudioEngine │ └── MiniAudioEngine.swift ├── LullabyMusic │ ├── Extensions │ │ └── Extensions.swift │ ├── Harmony │ │ └── Interval.swift │ └── Tuning │ │ └── Tuning.swift └── LullabySoundIOEngine │ └── SoundIOEngine.swift └── Tests └── LullabyTests └── LullabyTests.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | Sources/CMiniAudio/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-macos: 11 | runs-on: macos-latest 12 | steps: 13 | - name: Install libsoundio 14 | run: brew install libsoundio 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Build 18 | run: swift build 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | 9 | *.xcscheme 10 | .swiftpm/xcode/package.xcworkspace/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jae Kong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-collections", 6 | "repositoryURL": "https://github.com/apple/swift-collections.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", 10 | "version": "0.0.7" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Lullaby", 8 | platforms: [ 9 | .macOS(.v12) 10 | ], 11 | products: [ 12 | .library( 13 | name: "Lullaby", 14 | targets: ["Lullaby"]), 15 | .library( 16 | name: "LullabyMusic", 17 | targets: ["LullabyMusic"]), 18 | // .library( 19 | // name: "LullabySoundIOEngine", 20 | // targets: ["LullabySoundIOEngine"]), 21 | .library( 22 | name: "LullabyMiniAudioEngine", 23 | targets: ["LullabyMiniAudioEngine"]) 24 | ], 25 | dependencies: [ 26 | // .package(url: "https://github.com/thara/SoundIO.git", from: "0.3.2"), 27 | .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "0.0.5")) 28 | // .package(url: "https://github.com/apple/swift-numerics.git", .upToNextMajor(from: "0.1.0")) 29 | ], 30 | targets: [ 31 | .target( 32 | name: "Lullaby", 33 | dependencies: [ 34 | .product(name: "Collections", package: "swift-collections"), 35 | .target(name: "LullabyMusic") 36 | ] 37 | ), 38 | .target( 39 | name: "LullabyMusic" 40 | ), 41 | // .target( 42 | // name: "LullabySoundIOEngine", 43 | // dependencies: [ 44 | // .product(name: "SoundIO", package: "SoundIO"), 45 | // .product(name: "Collections", package: "swift-collections"), 46 | // .target(name: "Lullaby") 47 | // ], 48 | // linkerSettings: [.unsafeFlags(["-L/usr/local/lib"])] 49 | // ), 50 | .target( 51 | name: "CMiniAudio", 52 | dependencies: [], 53 | path: "Sources/CMiniAudio", 54 | cSettings: [ 55 | .define("MINIAUDIO_IMPLEMENTATION") 56 | ] 57 | ), 58 | .target( 59 | name: "LullabyMiniAudioEngine", 60 | dependencies: [ 61 | .target(name: "CMiniAudio"), 62 | .target(name: "Lullaby") 63 | ] 64 | ), 65 | .testTarget( 66 | name: "LullabyTests", 67 | dependencies: [ 68 | .target(name: "Lullaby"), 69 | // .target(name: "LullabySoundIOEngine"), 70 | .target(name: "LullabyMiniAudioEngine") 71 | ]) 72 | ] 73 | ) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lullaby 2 | 3 | ![For Swift 5.5+](https://img.shields.io/badge/swift-5.5%2B-orange?style=flat-square) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fjaekong%2FLullaby%2Fbadge%3Ftype%3Dswift-versions&style=flat-square)](https://swiftpackageindex.com/jaekong/Lullaby) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fjaekong%2FLullaby%2Fbadge%3Ftype%3Dplatforms&style=flat-square)](https://swiftpackageindex.com/jaekong/Lullaby) 6 | 7 | Lullaby is an audio synthesis framework for Swift that supports both macOS and Linux! It was inspired by other audio environments like FAUST, SuperCollider, Max and an article "Functional Signal Processing in Swift". 8 | 9 | Currently, it is not production-ready, but I would like to know what you think! 10 | 11 | ## What Can I do with it? 12 | 13 | - Audio Synthesis 14 | - Computer Music Composition 15 | - Real-time Reactive Audio Effects for Games and Apps 16 | - Data Sonification 17 | 18 | ## Supported Platforms 19 | 20 | - Tested on macOS / Linux[^1] 21 | - Swift 5.5 22 | 23 | ## Usage 24 | 25 | ```swift 26 | //... 27 | dependencies: [ 28 | // ... 29 | .package(url: "https://github.com/jtodaone/Lullaby.git", from: "0.2.0") 30 | ], 31 | targets: [ 32 | .executableTarget( 33 | // ... 34 | dependencies: [ 35 | .product(name: "Lullaby", package: "Lullaby"), 36 | .product(name: "LullabyMusic", package: "Lullaby"), 37 | .product(name: "LullabyMiniAudioEngine", package: "Lullaby") 38 | ] 39 | ) 40 | ] 41 | //... 42 | ``` 43 | 44 | ## Examples 45 | 46 | ```swift 47 | import Lullaby 48 | import LullabyMusic 49 | import LullabyMiniAudioEngine 50 | 51 | func sineTest() async throws { 52 | let value = Value(value: 440) 53 | 54 | let carrier = await sine(frequency: value.output) 55 | 56 | let task = Task { 57 | for i in twelveToneEqualTemperamentTuning.pitches { 58 | await value.setValue(Sample(i * 440)) 59 | await Task.sleep(seconds: 0.5) 60 | } 61 | 62 | return 63 | } 64 | 65 | let engine = try await MiniAudioEngine() 66 | 67 | engine.setOutput(to: carrier) 68 | try engine.prepare() 69 | try engine.start() 70 | 71 | await task.value 72 | 73 | try engine.stop() 74 | } 75 | 76 | let task = Task { 77 | try await sineTest() 78 | task.cancel() 79 | } 80 | 81 | while !task.isCancelled {} 82 | 83 | ``` 84 | 85 | ## Future Plans 86 | 87 | - Triggers subscribing to external events - key presses, MIDI, etcs. 88 | - Musical Composition DSL system 89 | - Writing musical pieces through custom DSL. 90 | - Phrases, Loops and more. 91 | - Test on other platforms (iOS, Windows, etcs.) 92 | - API Documentation 93 | 94 | ## Attribution 95 | 96 | - [SoundIO](https://github.com/thara/SoundIO), a Swift binding of [libsoundio](https://github.com/andrewrk/libsoundio) is used in the source code, but it will be deprecated in favour of miniaudio. 97 | - [miniaudio](https://miniaud.io) is used internally to handle audio I/O. 98 | 99 | - Lullaby is licensed under [MIT License](LICENSE). You don't have to, but it would be nice if you let me know if you used Lullaby! 100 | 101 | [^1]: Tested on Raspberry Pi 4B running Raspberry Pi OS Bullseye and Ubuntu 22.04 LTS. Theoretically, it should work on other platforms too. I couldn't test it though. If it doesn't work, please open an issue. 102 | -------------------------------------------------------------------------------- /Sources/CMiniAudio/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CMiniAudio { 2 | header "miniaudio.h" 3 | } 4 | -------------------------------------------------------------------------------- /Sources/CMiniAudio/miniaudio.c: -------------------------------------------------------------------------------- 1 | #include "miniaudio.h" 2 | 3 | -------------------------------------------------------------------------------- /Sources/Lullaby/Audio/Basic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias Precision = Float32 4 | 5 | public typealias Sample = Precision 6 | public typealias Amplitude = Sample 7 | 8 | public extension Amplitude { 9 | var decibel: Self { 10 | return 20 * log10f(self) 11 | } 12 | 13 | init(from decibel: Float) { 14 | self = powf(10.0, (decibel / 20)) 15 | } 16 | } 17 | 18 | public typealias Time = Precision 19 | 20 | public extension Task where Success == Never, Failure == Never{ 21 | static func sleep(seconds: T) async { 22 | let nanoseconds = UInt64(seconds * 1_000_000_000) 23 | try? await Self.sleep(nanoseconds: nanoseconds) 24 | } 25 | } 26 | 27 | public actor Value { 28 | public init(value: Sample) { 29 | self.value = value 30 | } 31 | 32 | public func setValue(_ value: Sample) { 33 | self.value = value 34 | } 35 | 36 | public var value: Sample 37 | public var output: Signal { 38 | return Signal { _ in 39 | return self.value 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Lullaby/Audio/Buffer.swift: -------------------------------------------------------------------------------- 1 | import Collections 2 | 3 | public actor LBBuffer { 4 | private var internalBuffer: Deque = [] 5 | 6 | init() { 7 | 8 | } 9 | 10 | func append(values: [Float]) { 11 | internalBuffer.append(contentsOf: values) 12 | } 13 | 14 | func pop(count: Int) -> [Float] { 15 | Array(internalBuffer.dropFirst(count)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Lullaby/Audio/Engine/DummyEngine.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final public class DummyEngine: LBEngine { 4 | private var output: Signal = 0 5 | private var audioTask: Task? 6 | 7 | public var buffer: Int = 512 8 | public var sampleRate: Int = 44100 9 | 10 | public var printEnabled = true 11 | 12 | public var latency: Time { 13 | return Time(buffer) / Time(sampleRate) 14 | } 15 | 16 | public init() async throws { 17 | 18 | } 19 | 20 | public func setOutput(to signal: Signal) { 21 | self.output = signal 22 | } 23 | 24 | public func prepare() throws { 25 | 26 | } 27 | 28 | public func start() throws { 29 | audioTask = Task { 30 | let secondsPerFrame = 1.0 / Float(sampleRate) 31 | var secondsOffset: Time = 0 32 | 33 | while true { 34 | for _ in 0.. (Sample) 4 | 5 | public struct Signal { 6 | public var function: DSPFunction 7 | fileprivate let uuid = UUID() 8 | 9 | public init(_ function: @escaping DSPFunction) { 10 | self.function = function 11 | } 12 | 13 | @inlinable 14 | public func callAsFunction(_ time: Time) -> Sample { 15 | function(time) 16 | } 17 | } 18 | 19 | extension Signal: Equatable { 20 | public static func == (lhs: Signal, rhs: Signal) -> Bool { 21 | withUnsafePointer(to: lhs.function) { lhsPointer in 22 | withUnsafePointer(to: rhs.function) { rhsPointer in 23 | return lhsPointer == rhsPointer 24 | } 25 | } 26 | } 27 | } 28 | 29 | extension Signal: Hashable { 30 | public func hash(into hasher: inout Hasher) { 31 | uuid.hash(into: &hasher) 32 | } 33 | } 34 | 35 | extension Collection where Element == Signal { 36 | public func callAsFunction(_ time: Time) -> Sample { 37 | reduce(0) { 38 | $0 + $1.callAsFunction(time) 39 | } 40 | } 41 | 42 | public var output: Signal { 43 | return Signal { 44 | self.callAsFunction($0) 45 | } 46 | } 47 | } 48 | 49 | extension Signal: ExpressibleByFloatLiteral { 50 | public init(floatLiteral value: FloatLiteralType) { 51 | self.function = { _ in return Sample(value) } 52 | } 53 | } 54 | 55 | extension Signal: ExpressibleByIntegerLiteral { 56 | public init(integerLiteral value: IntegerLiteralType) { 57 | self.function = { _ in return Sample(value) } 58 | } 59 | } 60 | 61 | public extension Signal { 62 | static func +(lhs: Signal, rhs: Signal) -> Signal { 63 | return Signal { time in lhs(time) + rhs(time) } 64 | } 65 | 66 | static func -(lhs: Signal, rhs: Signal) -> Signal { 67 | return Signal { time in lhs(time) - rhs(time) } 68 | } 69 | 70 | static func *(lhs: Signal, rhs: Signal) -> Signal { 71 | return Signal { time in lhs(time) * rhs(time) } 72 | } 73 | 74 | static func /(lhs: Signal, rhs: Signal) -> Signal { 75 | return Signal { time in 76 | guard rhs(time) != 0 else { return 0 } 77 | return lhs(time) / rhs(time) 78 | } 79 | } 80 | } 81 | 82 | public extension Signal { 83 | static func constant(_ value: Sample) -> Signal { 84 | return Signal { _ in value } 85 | } 86 | 87 | static func constant(_ value: T) -> Signal { 88 | return Signal { _ in Sample(value) } 89 | } 90 | 91 | static func constant(_ value: T) -> Signal { 92 | return Signal { _ in Sample(value) } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/Lullaby/Envelope/ADSR.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func adsr(trigger: Signal, attack: Time, decay: Time, sustain: Amplitude, release: Time) -> Signal { 4 | var internalTriggeredTime: Time? = nil 5 | var internalReleasedTime: Time? = nil 6 | 7 | precondition(attack >= 0, "Attack value should be positive.") 8 | precondition(decay >= 0, "Decay value should be positive.") 9 | precondition(sustain >= 0, "Sustain value should be positive.") 10 | precondition(release >= 0, "Release value should be positive.") 11 | 12 | return Signal({ time -> Sample in 13 | let triggerSample = trigger(time) 14 | if internalTriggeredTime == nil && triggerSample >= 0.5 { 15 | internalTriggeredTime = time 16 | return 0 17 | } 18 | 19 | guard let triggeredTime = internalTriggeredTime else { 20 | return 0 21 | } 22 | 23 | if internalReleasedTime == nil && triggerSample < 0.5 { 24 | internalReleasedTime = time 25 | return sustain 26 | } 27 | 28 | let elapsedTime = time - triggeredTime 29 | 30 | switch elapsedTime { 31 | case ..= 0, "Attack value should be positive.") 18 | precondition(release >= 0, "Release value should be positive.") 19 | 20 | guard 21 | let decay = decay, 22 | let sustain = sustain, 23 | let decayShape = decayShape, 24 | let sustainShape = sustainShape 25 | else { 26 | self.attackShape = attackShape 27 | self.releaseShape = releaseShape 28 | self.attack = attack 29 | self.release = release 30 | return 31 | } 32 | 33 | precondition(decay >= 0, "Decay value should be positive.") 34 | precondition(sustain >= 0, "Sustain value should be positive.") 35 | 36 | self.attackShape = attackShape 37 | self.decayShape = decayShape 38 | self.sustainShape = sustainShape 39 | self.releaseShape = releaseShape 40 | self.attack = attack 41 | self.decay = decay 42 | self.sustain = sustain 43 | self.release = release 44 | } 45 | } 46 | 47 | /// ADSR / AR Envelope Generator with custom curve shapes. 48 | public class EnvelopeGenerator { 49 | public let envelope: Envelope 50 | 51 | private var attackShape: Wave { return envelope.attackShape } 52 | private var decayShape: Wave? { return envelope.decayShape } 53 | private var sustainShape: Wave? { return envelope.sustainShape } 54 | private var releaseShape: Wave { return envelope.releaseShape } 55 | 56 | private var attack: Time { return envelope.attack } 57 | private var decay: Time? { return envelope.decay } 58 | private var sustain: Amplitude? { return envelope.sustain } 59 | private var release: Time { return envelope.release } 60 | 61 | private var activated: Bool = false 62 | 63 | private var internalTriggeredTime: Time? = nil 64 | private var internalReleasedTime: Time? = nil 65 | 66 | private var lastValue: Amplitude? = nil 67 | 68 | public init(attack: Time, decay: Time? = nil, sustain: Amplitude? = nil, release: Time, attackShape: @escaping Wave = BasicWaves.rampUp, decayShape: Wave? = BasicWaves.rampDown, sustainShape: Wave? = BasicWaves.constant, releaseShape: @escaping Wave = BasicWaves.rampDown) { 69 | self.envelope = Envelope(attack: attack, decay: decay, sustain: sustain, release: release, attackShape: attackShape, decayShape: decayShape, sustainShape: sustainShape, releaseShape: releaseShape) 70 | } 71 | 72 | public init(envelope: Envelope) { 73 | self.envelope = envelope 74 | } 75 | 76 | public func impulse(sustain: Time? = nil) async { 77 | activate() 78 | await Task.sleep(seconds: sustain ?? self.attack + self.release) 79 | deactivate() 80 | } 81 | 82 | public func activate() { 83 | activated = true 84 | self.internalReleasedTime = nil 85 | self.internalTriggeredTime = nil 86 | self.lastValue = nil 87 | } 88 | 89 | public func deactivate() { 90 | activated = false 91 | } 92 | 93 | public var output: Signal { 94 | guard 95 | let decay = decay, 96 | let sustain = sustain, 97 | let decayShape = decayShape, 98 | let sustainShape = sustainShape 99 | else { 100 | return Signal { time -> Sample in 101 | if self.internalTriggeredTime == nil && self.activated { 102 | self.internalTriggeredTime = time 103 | return 0 104 | } 105 | 106 | guard let triggeredTime = self.internalTriggeredTime else { 107 | return 0 108 | } 109 | 110 | if self.internalReleasedTime == nil && !self.activated { 111 | self.internalReleasedTime = time 112 | } 113 | 114 | let elapsedTime = time - triggeredTime 115 | 116 | if let releasedTime = self.internalReleasedTime { 117 | if releasedTime + self.release <= time { 118 | self.internalTriggeredTime = nil 119 | self.internalReleasedTime = nil 120 | self.lastValue = nil 121 | 122 | return 0 123 | } 124 | 125 | if elapsedTime >= self.attack { 126 | return self.releaseShape(min((elapsedTime - self.attack) / self.release, 1)) 127 | } 128 | 129 | return self.releaseShape(min((time - releasedTime) / self.release, 1)) * (self.lastValue ?? 1) 130 | } 131 | 132 | switch elapsedTime { 133 | case .. Sample in 153 | if self.internalTriggeredTime == nil && self.activated { 154 | self.internalTriggeredTime = time 155 | return 0 156 | } 157 | 158 | guard let triggeredTime = self.internalTriggeredTime else { 159 | return 0 160 | } 161 | 162 | if self.internalReleasedTime == nil && !self.activated { 163 | self.internalReleasedTime = time 164 | return self.releaseShape(0) * (self.lastValue ?? sustain) 165 | } 166 | 167 | let elapsedTime = time - triggeredTime 168 | 169 | if let releasedTime = self.internalReleasedTime { 170 | if releasedTime + self.release <= time { 171 | self.internalTriggeredTime = nil 172 | self.internalReleasedTime = nil 173 | self.lastValue = nil 174 | 175 | return 0 176 | } 177 | 178 | return self.releaseShape((time - releasedTime) / self.release) * (self.lastValue ?? sustain) 179 | } 180 | 181 | switch elapsedTime { 182 | case .. Signal { 4 | return Oscillator(wave: BasicWaves.sine, frequency: frequency, phase: phase).output 5 | } 6 | 7 | public func triangle(frequency: Signal, phase: Phase = 0) -> Signal { 8 | return Oscillator(wave: BasicWaves.triangle, frequency: frequency, phase: phase).output 9 | } 10 | 11 | public func square(frequency: Signal, phase: Phase = 0) -> Signal { 12 | return Oscillator(wave: BasicWaves.square, frequency: frequency, phase: phase).output 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Lullaby/Oscillator/Oscillator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A class that converts a wave function to a signal. 4 | public class Oscillator { 5 | public init(wave: @escaping Wave, frequency: Signal, phase: Phase) { 6 | self.wave = wave 7 | self.frequency = frequency 8 | self.phase = phase 9 | } 10 | 11 | public var wave: Wave 12 | public var frequency: Signal 13 | public var phase: Phase 14 | 15 | public var output: Signal { 16 | var lastTime: Time = 0 17 | 18 | return Signal { time in 19 | let deltaTime = max(time - lastTime, 0) 20 | self.phase += Phase(self.frequency(time) * (deltaTime)) 21 | self.phase = self.phase.truncatingRemainder(dividingBy: 1) 22 | 23 | defer { lastTime = time } 24 | 25 | return self.wave(self.phase) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Lullaby/Synth/Synth.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Collections 3 | 4 | public struct SynthEvent { 5 | public let frequency: Signal 6 | public let duration: Time 7 | } 8 | 9 | public actor MonophonicSynth { 10 | private var oscillator: Oscillator 11 | private var envelope: EnvelopeGenerator 12 | 13 | public var frequency: Signal = 0 14 | 15 | public var output: Signal { 16 | return oscillator.output * envelope.output 17 | } 18 | 19 | public init(wave: @escaping Wave, envelope: Envelope) { 20 | self.oscillator = Oscillator(wave: wave, frequency: frequency, phase: 0) 21 | self.envelope = EnvelopeGenerator(envelope: envelope) 22 | } 23 | 24 | public func play(event: SynthEvent) async { 25 | self.oscillator.frequency = event.frequency 26 | await self.envelope.impulse(sustain: event.duration) 27 | await Task.sleep(seconds: self.envelope.envelope.release) 28 | } 29 | 30 | public func play(event: SynthEvent) { 31 | Task { 32 | await self.play(event: event) 33 | } 34 | } 35 | } 36 | 37 | public actor Synth { 38 | // private var synths: Deque = [] 39 | 40 | private var wave: Wave 41 | private var envelope: Envelope 42 | 43 | public var signals: [Signal] = [] 44 | public var output: Signal { 45 | return Signal { 46 | self.mixedSignal($0) 47 | } 48 | } 49 | 50 | public var currentPolyphonyCount: Signal = 0 51 | 52 | public init(wave: @escaping Wave, envelope: Envelope) { 53 | self.wave = wave 54 | self.envelope = envelope 55 | } 56 | 57 | public func play(event: SynthEvent) async { 58 | let synth = MonophonicSynth(wave: wave, envelope: envelope) 59 | let signal = await synth.output 60 | 61 | signals.append(signal) 62 | currentPolyphonyCount = currentPolyphonyCount + 1 63 | 64 | await synth.play(event: event) 65 | 66 | signals.removeAll { $0 == signal } 67 | currentPolyphonyCount = currentPolyphonyCount - 1 68 | } 69 | 70 | public func play(events: [SynthEvent]) async { 71 | await withTaskGroup(of: Void.self, returning: Void.self) { group in 72 | for event in events { 73 | group.addTask { 74 | await self.play(event: event) 75 | } 76 | } 77 | } 78 | } 79 | 80 | private func mixedSignal(_ time: Time) -> Sample { 81 | return signals(time) 82 | } 83 | 84 | nonisolated public func play(event: SynthEvent) { 85 | Task { 86 | await self.play(event: event) 87 | } 88 | } 89 | 90 | nonisolated public func play(events: [SynthEvent]) { 91 | Task { 92 | await self.play(events: events) 93 | } 94 | } 95 | } 96 | 97 | //public actor Synth { 98 | // private var oscillators: Deque = [] 99 | // private var envelopes: Deque = [] 100 | // 101 | // private var envelopeData: Envelope 102 | // private var wave: Wave 103 | // 104 | // public var output: Signal { 105 | // zip(oscillators, envelopes).map { 106 | // $0.0.output * $0.1.output 107 | // }.reduce(0) { $0 + $1 } 108 | // } 109 | // 110 | // public init(wave: @escaping Wave, envelope: Envelope) { 111 | // self.wave = wave 112 | // self.envelopeData = envelope 113 | // } 114 | // 115 | // public func play(event: SynthEvent) async { 116 | // let oscillator = Oscillator(wave: self.wave, frequency: event.frequency, phase: event.phase) 117 | // let envelope = EnvelopeGenerator(envelope: self.envelopeData) 118 | // 119 | // oscillators.append(oscillator) 120 | // envelopes.append(envelope) 121 | // 122 | // Task { 123 | // 124 | // } 125 | // } 126 | //} 127 | -------------------------------------------------------------------------------- /Sources/Lullaby/Utility/Print.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func print(_ signal: Signal, skippingBy: Int = 440) -> Signal { 4 | var count = 0 5 | return Signal { time in 6 | let sample = signal(time) 7 | if count % skippingBy == 0 { 8 | Task.detached { 9 | print(time, sample) 10 | } 11 | } 12 | count += 1 13 | return sample 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Lullaby/Wave/LinearInterpolatedWavetable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Generates linear-interpolated Wave function from given sample points. 4 | /// 5 | /// To make a triangle wave function, you can provide five sample points like below. 6 | /// ``` 7 | /// let triangle = linearInterpolatedWavetable(samplePoints: [0, 1, 0, -1, 0]) 8 | /// ``` 9 | public func linearInterpolatedWavetable(samplePoints: [Sample]) -> Wave { 10 | let sampleCount = samplePoints.count - 1 11 | let sampleRange = Phase(sampleCount) 12 | 13 | guard sampleCount != -1 else { 14 | return { _ in 0 } 15 | } 16 | 17 | guard sampleCount != 0 else { 18 | return { _ in samplePoints[0] } 19 | } 20 | 21 | return { phase in 22 | let lerpIndex = (phase * sampleRange).truncatingRemainder(dividingBy: sampleRange) 23 | let leftIndex = Int(lerpIndex.rounded(.down)) 24 | let rightIndex = Int(lerpIndex.rounded(.up).truncatingRemainder(dividingBy: sampleRange + 1)) 25 | 26 | return samplePoints[leftIndex] + (samplePoints[rightIndex] - samplePoints[leftIndex]) * (Sample(lerpIndex) - Sample(leftIndex)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Lullaby/Wave/Wave+Basic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | internal enum LookupTables { 4 | static let sineTable: [Float] = (0..<44100).map { 5 | return sin(2 * .pi * (Float($0) / 44100)) 6 | } 7 | 8 | static let squareTable: [Float] = [1, -1] 9 | } 10 | 11 | public enum BasicWaves { 12 | public static let rampUp: Wave = { phase in max(min(phase, 1), 0) } 13 | public static let rampDown: Wave = { phase in max(min(1 - phase, 1), 0) } 14 | public static let constant = wavetable(from: [1]) 15 | 16 | /// A pre-defined wavetable based sine wave function. 17 | public static let sine = wavetable(from: LookupTables.sineTable) 18 | 19 | /// A pre-defined linear-interpolated wavetable based triangle wave function. 20 | public static let triangle = linearInterpolatedWavetable(samplePoints: [0, 1, 0, -1, 0]) 21 | 22 | /// A pre-defined wavetable based square wave function. 23 | public static let square = wavetable(from: LookupTables.squareTable) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Lullaby/Wave/Wave.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A number in 0...1 range that represents a phase value. 4 | public typealias Phase = Precision 5 | 6 | /// A type of function that recieves value in 0...1 and returns sample value. 7 | public typealias Wave = (Phase) -> Sample 8 | -------------------------------------------------------------------------------- /Sources/Lullaby/Wave/Wavetable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Generates wavetable that only interpolates the phase value. 4 | /// 5 | /// To make a square wave function, you can provide five sample points like below. 6 | /// ``` 7 | /// let square = wavetable(from: [1, -1]) 8 | /// ``` 9 | public func wavetable(from table: [Sample]) -> Wave { 10 | let sampleCount = Phase(table.count) 11 | return { phase in 12 | let currentIndex = Int((sampleCount * (phase).truncatingRemainder(dividingBy: 1)).rounded(.down)) 13 | 14 | return table[currentIndex] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/LullabyMiniAudioEngine/MiniAudioEngine.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CMiniAudio 3 | import Lullaby 4 | 5 | final public class MiniAudioEngine: LBEngine { 6 | 7 | private var device: ma_device! 8 | private var deviceConfig: ma_device_config! 9 | 10 | fileprivate var output: Signal = .constant(0) 11 | 12 | fileprivate var secondsPerFrame: Float = 0 13 | fileprivate var secondsOffset: Float = 0 14 | 15 | public init() async throws { 16 | } 17 | 18 | public func setOutput(to signal: Signal) { 19 | self.output = signal 20 | } 21 | 22 | public func prepare() throws { 23 | device = ma_device() 24 | deviceConfig = ma_device_config_init(ma_device_type_playback) 25 | deviceConfig.playback.format = ma_format_f32 26 | deviceConfig.playback.channels = 1 27 | deviceConfig.sampleRate = 44100 28 | 29 | secondsPerFrame = 1.0 / Float(deviceConfig.sampleRate) 30 | secondsOffset = 0 31 | 32 | deviceConfig.dataCallback = writeCallback 33 | deviceConfig.pUserData = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) 34 | 35 | guard ma_device_init(nil, &deviceConfig, &device) == MA_SUCCESS else { throw MiniAudioError.miniAudioDeviceInitFailure } 36 | } 37 | 38 | public func start() throws { 39 | guard ma_device_init(nil, &deviceConfig, &device) == MA_SUCCESS else { 40 | throw MiniAudioError.miniAudioDeviceInitFailure 41 | } 42 | 43 | guard ma_device_start(&device) == MA_SUCCESS else { 44 | ma_device_uninit(&device) 45 | throw MiniAudioError.miniAudioDeviceStartFailure 46 | } 47 | } 48 | 49 | public func stop() throws { 50 | guard ma_device_stop(&device) == MA_SUCCESS else { 51 | throw MiniAudioError.miniAudioDeviceStopFailure 52 | } 53 | } 54 | 55 | public static func playTest(of signal: Signal = sine(frequency: 440.0), for seconds: Double = 1) async throws { 56 | let engine = try await Self() 57 | engine.setOutput(to: signal) 58 | try engine.prepare() 59 | try engine.start() 60 | await Task.sleep(seconds: seconds) 61 | try engine.stop() 62 | } 63 | 64 | deinit { 65 | ma_device_uninit(&device) 66 | } 67 | 68 | enum MiniAudioError: Error { 69 | case miniAudioInitFailure 70 | case miniAudioDeviceInitFailure 71 | case miniAudioDeviceStartFailure 72 | case miniAudioDeviceStopFailure 73 | } 74 | } 75 | 76 | fileprivate func writeCallback(_ device: UnsafeMutablePointer?, _ pOutput: UnsafeMutableRawPointer?, _ pInput: UnsafeRawPointer?, _ frameCount: ma_uint32) -> Void { 77 | guard (device?.pointee) != nil else { return } 78 | 79 | guard var cursor = pOutput else { return } 80 | 81 | let selfPointer = Unmanaged.fromOpaque((device?.pointee.pUserData)!).takeUnretainedValue() 82 | 83 | for frame in 0...size) 88 | } 89 | 90 | selfPointer.secondsOffset = (selfPointer.secondsOffset + selfPointer.secondsPerFrame * Float(frameCount)) 91 | } 92 | -------------------------------------------------------------------------------- /Sources/LullabyMusic/Extensions/Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Collection where Index == Int { 4 | public func split(into size: Int) -> [Self.SubSequence] { 5 | return stride(from: 0, to: count, by: size).map { 6 | self[$0 ..< Swift.min($0 + size, count)] 7 | } 8 | } 9 | } 10 | 11 | extension Data { 12 | public func padded(chunkSize: Int) -> Data { 13 | if count % chunkSize != 0 { 14 | let paddingSize = chunkSize - count % chunkSize 15 | let padding = Data(count: paddingSize) 16 | var newData = self 17 | newData.append(padding) 18 | return newData 19 | } else { 20 | return self 21 | } 22 | } 23 | } 24 | 25 | extension UInt8 { 26 | public func interlaceBits() -> UInt8 { 27 | let filtered1 = 0b01010101 & self 28 | let filtered2 = 0b10101010 & self 29 | return filtered1 << 1 + filtered2 >> 1 30 | } 31 | 32 | public func binaryString() -> String { 33 | let number = String(self, radix: 2) 34 | let padding = String(repeating: "0", count: (8 - number.count)) 35 | return String(padding + number) 36 | } 37 | } 38 | 39 | extension String { 40 | public func split(into size: Int) -> [Substring] { 41 | return stride(from: 0, to: count, by: size).map { 42 | self[(self.index(startIndex, offsetBy: $0)) ..< (self.index(startIndex, offsetBy: $0 + size, limitedBy: endIndex) ?? endIndex)] 43 | } 44 | } 45 | } 46 | 47 | extension Int { 48 | public var isPrime: Bool { 49 | get { 50 | if self <= 1 { 51 | return false 52 | } 53 | 54 | if self == 2 || self == 3 { 55 | return true 56 | } 57 | 58 | for i in 2...(self / 2) { 59 | if self % i == 0 { 60 | return false 61 | } 62 | } 63 | 64 | return true 65 | } 66 | } 67 | 68 | public var primeFactors: [(prime: Int, exponent: Int)] { 69 | get { 70 | let primes = Int.findPrimes(under: self) 71 | var factoringPrimes: [(Int, Int)] = [] 72 | 73 | var currentExponent = 0 74 | 75 | for prime in primes { 76 | guard self % prime == 0 else { continue } 77 | 78 | currentExponent = 1 79 | 80 | while self % Int(pow(Double(prime), Double(currentExponent))) == 0 { 81 | currentExponent += 1 82 | } 83 | 84 | factoringPrimes.append((prime, currentExponent - 1)) 85 | } 86 | 87 | return factoringPrimes 88 | } 89 | } 90 | 91 | public func isSmooth(n: Int) -> Bool { 92 | guard n >= 2 else { return false } 93 | return self.primeFactors.filter { (prime, _) -> Bool in prime > n }.count == 0 94 | } 95 | 96 | public static func findPrimes(under max: Int) -> [Int] { 97 | let preCalculated = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997] 98 | 99 | var primes: [Int] = [] 100 | let preCalculatedMax = 1000 101 | 102 | if max <= preCalculatedMax { 103 | primes = preCalculated.filter { $0 <= max } 104 | } else { 105 | primes = preCalculated 106 | 107 | for number in (preCalculatedMax + 1)...max { 108 | if number.isPrime { 109 | primes.append(Int(number)) 110 | } 111 | } 112 | } 113 | 114 | return primes 115 | } 116 | 117 | public static func findSmoothNumbers(n: Int, under max: Int) -> [Int] { 118 | guard n >= 2 else { return [] } 119 | 120 | var result: [Int] = [] 121 | 122 | for number in 2...max { 123 | if number.isSmooth(n: n) { 124 | result.append(number) 125 | } 126 | } 127 | 128 | return result 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/LullabyMusic/Harmony/Interval.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Interval Classes in 12TET based western music theory. 4 | public enum Interval: Equatable, Comparable { 5 | case major(Int) 6 | case minor(Int) 7 | case perfect(Int) 8 | case diminished(Int) 9 | case augmented(Int) 10 | 11 | init?(semitones: Int) { 12 | guard semitones >= 0 else { return nil } 13 | 14 | let octave = semitones / 12 15 | let convertedSemitones = semitones % 12 16 | 17 | switch convertedSemitones { 18 | case 0: 19 | self = .perfect(1 + octave * 7) 20 | case 1: 21 | self = .minor(2 + octave * 7) 22 | case 2: 23 | self = .major(2 + octave * 7) 24 | case 3: 25 | self = .minor(3 + octave * 7) 26 | case 4: 27 | self = .major(3 + octave * 7) 28 | case 5: 29 | self = .perfect(4 + octave * 7) 30 | case 6: 31 | self = .diminished(5 + octave * 7) 32 | case 7: 33 | self = .perfect(5 + octave * 7) 34 | case 8: 35 | self = .minor(6 + octave * 7) 36 | case 9: 37 | self = .major(6 + octave * 7) 38 | case 10: 39 | self = .minor(7 + octave * 7) 40 | case 11: 41 | self = .major(7 + octave * 7) 42 | default: 43 | return nil 44 | } 45 | } 46 | 47 | var semitones: Int { 48 | switch self { 49 | case .major(let interval): 50 | let convertedInterval = (interval - 1) % 7 + 1 51 | let octave = (interval - 1) / 7 52 | 53 | switch convertedInterval { 54 | case 2...3: 55 | return ((convertedInterval - 1) * 2) + octave * 12 56 | case 6...7: 57 | return ((convertedInterval - 1) * 2 - 1) + octave * 12 58 | default: 59 | fatalError("") 60 | } 61 | case .minor(let interval): 62 | let convertedInterval = (interval - 1) % 7 + 1 63 | let octave = (interval - 1) / 7 64 | 65 | switch convertedInterval { 66 | case 2...3: 67 | return ((convertedInterval - 1) * 2 - 1) + octave * 12 68 | case 6...7: 69 | return ((convertedInterval - 1) * 2 - 2) + octave * 12 70 | default: 71 | return 0 72 | } 73 | case .perfect(let interval): 74 | let convertedInterval = (interval - 1) % 7 + 1 75 | let octave = (interval - 1) / 7 76 | 77 | switch convertedInterval { 78 | case 1: 79 | return octave * 12 80 | case 4: 81 | return 5 + octave * 12 82 | case 5: 83 | return 7 + octave * 12 84 | default: 85 | return 0 86 | } 87 | case .diminished(let interval): 88 | let convertedInterval = (interval - 1) % 7 + 1 89 | 90 | switch convertedInterval { 91 | case 1, 4, 5: 92 | return Interval.perfect(interval).semitones - 1 93 | case 2, 3, 6, 7: 94 | return Interval.minor(interval).semitones - 1 95 | default: 96 | fatalError("") 97 | } 98 | case .augmented(let interval): 99 | let convertedInterval = (interval - 1) % 7 + 1 100 | 101 | switch convertedInterval { 102 | case 1, 4, 5: 103 | return Interval.perfect(interval).semitones + 1 104 | case 2, 3, 6, 7: 105 | return Interval.major(interval).semitones + 1 106 | default: 107 | fatalError("") 108 | } 109 | } 110 | } 111 | 112 | public static func ==(lhs: Self, rhs: Self) -> Bool { 113 | return lhs.semitones == rhs.semitones 114 | } 115 | 116 | public static func <(lhs: Self, rhs: Self) -> Bool { 117 | return lhs.semitones < rhs.semitones 118 | } 119 | } 120 | 121 | /// Common Intervals in 12TET based western music theory. 122 | public enum CommonIntervals: Int, Hashable { 123 | case unison = 0 124 | case minor2 = 1 125 | case major2 = 2 126 | case minor3 = 3 127 | case major3 = 4 128 | case perfect4 = 5 129 | case tritone = 6 130 | case perfect5 = 7 131 | case minor6 = 8 132 | case major6 = 9 133 | case minor7 = 10 134 | case major7 = 11 135 | case octave = 12 136 | 137 | // one more octave for 9th 11th and stuff 138 | case minor9 = 13 139 | case major9 = 14 140 | case minor10 = 15 141 | case major10 = 16 142 | case perfect11 = 17 143 | case aug11 = 18 144 | case perfect12 = 19 145 | case minor13 = 20 // i mean at this point do we even need them? 146 | case major13 = 21 147 | case minor14 = 22 148 | case major14 = 23 149 | case perfect15 = 24 150 | 151 | public static let dim2: CommonIntervals = .unison 152 | public static let dim3: CommonIntervals = .major2 153 | public static let dim4: CommonIntervals = .major3 154 | public static let dim5: CommonIntervals = .tritone 155 | public static let dim6: CommonIntervals = .perfect5 156 | public static let dim7: CommonIntervals = .major6 157 | public static let dim8: CommonIntervals = .major7 158 | 159 | // not gonna bother putting all the diminished and augmented 160 | public static let dim12: CommonIntervals = .aug11 161 | 162 | public static let aug1: CommonIntervals = .minor2 163 | public static let aug2: CommonIntervals = .minor3 164 | public static let aug3: CommonIntervals = .perfect4 165 | public static let aug4: CommonIntervals = .tritone 166 | public static let aug5: CommonIntervals = .minor6 167 | public static let aug6: CommonIntervals = .minor7 168 | public static let aug7: CommonIntervals = .octave 169 | 170 | public init(semitones: Int) { 171 | if semitones > 12 { 172 | self = Self.init(rawValue: semitones % 24)! 173 | } else if semitones < 0 { 174 | self = Self.init(rawValue: (semitones % 24) + 24)! 175 | } else { 176 | self = Self.init(rawValue: semitones)! 177 | } 178 | } 179 | 180 | public func diminished() -> Self { 181 | return Self.init(semitones: self.rawValue - 1) 182 | } 183 | 184 | public func augmented() -> Self { 185 | return Self.init(semitones: self.rawValue - 1) 186 | } 187 | } 188 | 189 | public enum ChordClass { 190 | public static let major: Set = [.unison, .major3, .perfect5] 191 | public static let minor: Set = [.unison, .minor3, .perfect5] 192 | public static let diminished: Set = [.unison, .minor3, .dim5] 193 | public static let augmented: Set = [.unison, .major3, .aug5] 194 | } 195 | -------------------------------------------------------------------------------- /Sources/LullabyMusic/Tuning/Tuning.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Frequency in Hz. 4 | public typealias Frequency = Double 5 | 6 | /// Represents pitch in terms of an exponent of 2. For example, value of 0 equals to standard pitch, 1 equals to an octave higher than standard pitch. 7 | public typealias Pitch = Double 8 | 9 | /// Double-precision real number ratio. Used to represent interval. 10 | public struct Ratio: Equatable, Codable, Hashable { 11 | fileprivate var numerator: Int = 1 12 | fileprivate var denominator: Int = 1 13 | 14 | public init(_ numerator: Int, _ denominator: Int) { 15 | self.value = (numerator, denominator) 16 | } 17 | 18 | public var value: (numerator: Int, denominator: Int) { 19 | get { 20 | return (numerator, denominator) 21 | } 22 | set { 23 | let numeratorFactors = newValue.numerator.primeFactors 24 | let denominatorFactors = newValue.denominator.primeFactors 25 | 26 | let maxNFactor = numeratorFactors.max(by: { $0.prime < $1.prime }) ?? (2, 0) 27 | let maxDFactor = denominatorFactors.max(by: { $0.prime < $1.prime }) ?? (2, 0) 28 | 29 | guard let maxPrime = [maxNFactor, maxDFactor].max(by: { $0.prime < $1.prime })?.prime else { return } 30 | 31 | let primes = Int.findPrimes(under: maxPrime) 32 | 33 | var newNumerator: Int = 1 34 | var newDenominator: Int = 1 35 | 36 | for prime in primes { 37 | let numeratorFactor = numeratorFactors.first { $0.prime == prime } ?? (prime, 0) 38 | let denominatorFactor = denominatorFactors.first { $0.prime == prime } ?? (prime, 0) 39 | 40 | let newExponent = numeratorFactor.exponent - denominatorFactor.exponent 41 | 42 | switch newExponent { 43 | case ..<0: 44 | newDenominator *= Int(pow(Double(prime), Double(-newExponent))) 45 | case 1...: 46 | newNumerator *= Int(pow(Double(prime), Double(newExponent))) 47 | default: 48 | break 49 | } 50 | } 51 | 52 | self.numerator = newNumerator 53 | self.denominator = newDenominator 54 | } 55 | } 56 | 57 | public var decimalValue: Double { 58 | return Double(numerator) / Double(denominator) 59 | } 60 | } 61 | 62 | extension Ratio: CustomStringConvertible { 63 | public var description: String { 64 | return "\(numerator)/\(denominator)" 65 | } 66 | } 67 | 68 | extension Ratio: Comparable { 69 | public static func < (lhs: Ratio, rhs: Ratio) -> Bool { 70 | return lhs.decimalValue < rhs.decimalValue 71 | } 72 | } 73 | 74 | extension Ratio: AdditiveArithmetic { 75 | public static var zero: Ratio = Ratio(0, 1) 76 | 77 | public static func - (lhs: Ratio, rhs: Ratio) -> Ratio { 78 | let commonDenominator = lhs.denominator * rhs.denominator 79 | let lhsNumerator = lhs.numerator * rhs.denominator 80 | let rhsNumerator = rhs.numerator * lhs.denominator 81 | 82 | return Ratio(lhsNumerator - rhsNumerator, commonDenominator) 83 | } 84 | 85 | public static func + (lhs: Ratio, rhs: Ratio) -> Ratio { 86 | let commonDenominator = lhs.denominator * rhs.denominator 87 | let lhsNumerator = lhs.numerator * rhs.denominator 88 | let rhsNumerator = rhs.numerator * lhs.denominator 89 | 90 | return Ratio(lhsNumerator + rhsNumerator, commonDenominator) 91 | } 92 | } 93 | 94 | public func primeLimitedIntervals(n: Int, max: Int) -> Set { 95 | let a = Int.findSmoothNumbers(n: n, under: max) 96 | let b = a 97 | 98 | var result: Set = [] 99 | 100 | for x in a { 101 | for y in b { 102 | if (x <= y || x >= y * 2) { continue } 103 | let interval = Ratio(x, y) 104 | result.insert(interval) 105 | } 106 | } 107 | 108 | return result 109 | } 110 | 111 | /// A struct representing a tuning system. 112 | public struct Tuning: Codable { 113 | public var pitches: [Pitch] 114 | public var intervals: [Ratio]? 115 | 116 | public var n: Int = 5 { 117 | didSet { 118 | switch mode { 119 | case .primeLimitedTuning: 120 | pitches = [1] + primeLimitedIntervals(n: n, max: max).sorted().map { $0.decimalValue } 121 | default: 122 | break 123 | } 124 | } 125 | } 126 | 127 | public var max: Int = 40 { 128 | didSet { 129 | switch mode { 130 | case .primeLimitedTuning: 131 | pitches = [1] + primeLimitedIntervals(n: n, max: max).sorted().map { $0.decimalValue } 132 | default: 133 | break 134 | } 135 | } 136 | } 137 | 138 | public var toneCount: Int = 12 { 139 | didSet { 140 | switch mode { 141 | case .equalTemperamentTuning: 142 | pitches = (0.. Frequency { 226 | let octave = floor(note) 227 | let pitchClass = note - octave 228 | 229 | guard let closestNote = pitches.sorted(by: { a, b in 230 | abs(Double(a) - pow(2, pitchClass)) < abs(Double(b) - pow(2, pitchClass)) 231 | }).first else { 232 | return standardFrequency * octave 233 | } 234 | 235 | return (standardFrequency * Double(closestNote)) * pow(2, octave) 236 | } 237 | 238 | public func closestNoteInTune(note: Pitch) -> Pitch { 239 | let octave = floor(note) 240 | let pitchClass = note - octave 241 | 242 | guard let closestNote = pitches.sorted(by: { a, b in 243 | abs(Double(a) - pow(2, pitchClass)) < abs(Double(b) - pow(2, pitchClass)) 244 | }).first else { 245 | return note 246 | } 247 | 248 | return octave + log2(Double(closestNote)) 249 | } 250 | } 251 | 252 | /// Standard 12TET at 440Hz tuning. 253 | public let twelveToneEqualTemperamentTuning = Tuning(toneCount: 12, standardFrequency: 440) 254 | 255 | extension Double { 256 | public init(_ other: Ratio) { 257 | self.init(other.decimalValue) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /Sources/LullabySoundIOEngine/SoundIOEngine.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Lullaby 3 | import SoundIO 4 | import CSoundIO 5 | 6 | @available(*, deprecated, message: "Use MiniAudioEngine instead.") 7 | final public class SoundIOEngine: LBEngine { 8 | private let io: SoundIO 9 | private var eventLoop: Task! 10 | private var audioTask: Task<(), Error>? 11 | private var _inputDeviceCount: Int32? 12 | private var _outputDeviceCount: Int32? 13 | 14 | fileprivate var _output: Signal = .constant(0) 15 | 16 | private var device: Device! 17 | private var out: OutStream! 18 | 19 | public func setOutput(to signal: Signal) { 20 | self._output = signal 21 | } 22 | 23 | public init() async throws { 24 | do { 25 | io = try SoundIO() 26 | 27 | #if os(macOS) 28 | try io.connect(to: Backend.coreAudio) 29 | #else 30 | do { 31 | try io.connect(to: Backend.jack) 32 | } catch { 33 | print("Cannot establish JACK connection") 34 | do { 35 | try io.connect() 36 | } catch { 37 | print("Cannot establish Audio Backend connection") 38 | } 39 | } 40 | #endif 41 | 42 | io.flushEvents() 43 | 44 | io.onDevicesChange { 45 | self._inputDeviceCount = try? $0.inputDeviceCount() 46 | self._outputDeviceCount = try? $0.outputDeviceCount() 47 | } 48 | 49 | eventLoop = Task.detached { 50 | while !Task.isCancelled { 51 | self.io.waitEvents() 52 | } 53 | } 54 | } catch { 55 | print("Error occured: ", error.localizedDescription) 56 | fatalError() 57 | } 58 | } 59 | 60 | deinit { 61 | eventLoop?.cancel() 62 | try! io.withInternalPointer { pointer in 63 | soundio_disconnect(pointer) 64 | } 65 | } 66 | 67 | public func prepare() throws { 68 | print("Device Count:", try self.io.outputDeviceCount()) 69 | device = try self.io.getOutputDevice(at: io.defaultOutputDeviceIndex()) 70 | 71 | print("Current Device:", device.name) 72 | 73 | out = try OutStream(to: device) 74 | out.format = .signed16bitLittleEndian 75 | out.softwareLatency = 0 76 | } 77 | 78 | public func start() throws { 79 | audioTask = Task(priority: .high) { 80 | var secondsOffset: Float = 0 81 | 82 | out.underflowCallback { outstream in 83 | print("Underflow Occured") 84 | } 85 | 86 | out.writeCallback { (outstream, frameCountMin, frameCountMax) in 87 | let layout = outstream.layout 88 | let secondsPerFrame = 1.0 / Float(outstream.sampleRate) 89 | 90 | var framesLeft = frameCountMax 91 | 92 | while 0 < framesLeft { 93 | var frameCount = framesLeft 94 | let areas = try! outstream.beginWriting(theNumberOf: &frameCount) 95 | 96 | if frameCount == 0 { 97 | break 98 | } 99 | 100 | for frame in 0..