Skip to content

Commit fa87646

Browse files
committed
release: add Stable Audio 3 local workflow
1 parent 4319383 commit fa87646

39 files changed

Lines changed: 4404 additions & 169 deletions

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ ORAM_DEFAULT_ENGINE=
2323
# engine router
2424
ORAM_ENGINE_ROUTER_MODE=auto
2525
ORAM_PREFERRED_PROVIDER=
26+
ORAM_STABLE_AUDIO_SERVICE_URL=http://127.0.0.1:8765
27+
ORAM_STABLE_AUDIO_LOCAL_PROVIDER=stable_audio_mlx
28+
ORAM_STABLE_AUDIO_LOCAL_MODEL=sm-music
29+
ORAM_STABLE_AUDIO_DECODER=same-s
30+
ORAM_STABLE_AUDIO_API_URL=
2631

2732
# dashboard security
2833
ORAM_DASHBOARD_TOKEN=

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ recorder → looper → sampler → engine router → local archive
111111
normalize, trim, fades, and spatial transforms.
112112
- Listens back — spectral analysis of pitch, BPM, key, harmonics, and frequency
113113
character through local FFT, LLM-based interpretation, or hybrid routes.
114-
- Generates sound through Local Mock by default, or summons textures through
115-
BYOK providers (ElevenLabs, Stability AI, fal Stable Audio).
114+
- Generates sound through Local SA3 by default when the local service is
115+
available, or summons textures through BYOK providers (ElevenLabs, Stability
116+
AI, fal Stable Audio).
116117
- Writes generated sounds into `~/Music/ORAM Library`.
117118
- Archives sessions as traces of a state: mix/stem WAVs, command logs, metadata,
118119
waveform text, and listening reports.
@@ -270,7 +271,8 @@ workstation." That is the intended boundary:
270271
- No ORAM cloud account is required.
271272
- The app talks to a localhost daemon.
272273
- Provider keys are stored in macOS Keychain for the packaged app.
273-
- Local Mock remains available without cloud credentials.
274+
- Local SA3 is the default local generation path; Local Mock remains available
275+
only as an explicit fallback.
274276
- Telemetry is off by default.
275277
- Generated sounds and archives stay in the local ORAM Library.
276278
- Daemon mutation routes use a local bearer token when auth is enabled.
@@ -420,7 +422,7 @@ Current plugin features:
420422
- four native layers with record, overdub, mute, solo, clear, volume, and pan
421423
- loop regions and host-input monitoring
422424
- typed ORAM command parsing through the daemon
423-
- generation through Local Mock, ElevenLabs, or Stability routing, then WAV
425+
- generation through Local SA3, ElevenLabs, or Stability routing, then WAV
424426
import into native plugin layers
425427
- native state serialization for parameters and layer audio
426428
- basic plugin-side DSP actions including reverse, speed/pitch-ratio, filters,

apps/macos/Sources/ORAMApp/Models/Models.swift

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,48 @@ struct ProvidersResponse: Decodable {
240240
let available: Int
241241
}
242242

243+
struct AudioDevice: Decodable, Identifiable {
244+
let id: Int
245+
let name: String
246+
let maxInputChannels: Int
247+
let maxOutputChannels: Int
248+
let defaultSamplerate: Double
249+
let isInput: Bool
250+
let isOutput: Bool
251+
252+
enum CodingKeys: String, CodingKey {
253+
case id
254+
case name
255+
case maxInputChannels = "max_input_channels"
256+
case maxOutputChannels = "max_output_channels"
257+
case defaultSamplerate = "default_samplerate"
258+
case isInput = "is_input"
259+
case isOutput = "is_output"
260+
}
261+
}
262+
263+
struct DevicesResponse: Decodable {
264+
let devices: [AudioDevice]
265+
let defaultInput: Int
266+
let defaultOutput: Int
267+
let currentInput: Int?
268+
let currentOutput: Int?
269+
let currentSampleRate: Int
270+
let currentFormat: String
271+
let currentBitDepth: Int
272+
273+
enum CodingKeys: String, CodingKey {
274+
case devices
275+
case defaultInput = "default_input"
276+
case defaultOutput = "default_output"
277+
case currentInput = "current_input"
278+
case currentOutput = "current_output"
279+
case currentSampleRate = "current_sample_rate"
280+
case currentFormat = "current_format"
281+
case currentBitDepth = "current_bit_depth"
282+
}
283+
}
284+
243285
struct SoundRecord: Decodable, Identifiable {
244286
let id: String
245287
let createdAt: String
@@ -294,6 +336,77 @@ struct GeneratePayload: Encodable {
294336
}
295337
}
296338

339+
struct StableAudioLoraPayload: Encodable {
340+
let name: String
341+
let path: String
342+
let strength: Double
343+
let interval: [Double]?
344+
}
345+
346+
struct StableAudioRenderPayload: Encodable {
347+
let prompt: String
348+
let mode: String
349+
let duration: Double
350+
let provider: String
351+
let model: String
352+
let decoder: String
353+
let localProvider: String
354+
let localModel: String
355+
let serviceURL: String
356+
let chunkedDecode: Bool
357+
let sourceLayer: Int?
358+
let targetLayer: String?
359+
let assignLayer: Bool
360+
let tags: [String]
361+
let negativePrompt: String
362+
let seed: Int?
363+
let steps: Int
364+
let cfgScale: Double
365+
let noiseDepth: Double?
366+
let inpaintStart: Double?
367+
let inpaintEnd: Double?
368+
let variationCount: Int
369+
let loraStack: [StableAudioLoraPayload]
370+
let loraAPath: String
371+
let loraAStrength: Double
372+
let loraBPath: String
373+
let loraBStrength: Double
374+
let loraIntervalMin: Double
375+
let loraIntervalMax: Double
376+
377+
enum CodingKeys: String, CodingKey {
378+
case prompt
379+
case mode
380+
case duration
381+
case provider
382+
case model
383+
case decoder
384+
case localProvider = "local_provider"
385+
case localModel = "local_model"
386+
case serviceURL = "service_url"
387+
case chunkedDecode = "chunked_decode"
388+
case sourceLayer = "source_layer"
389+
case targetLayer = "target_layer"
390+
case assignLayer = "assign_layer"
391+
case tags
392+
case negativePrompt = "negative_prompt"
393+
case seed
394+
case steps
395+
case cfgScale = "cfg_scale"
396+
case noiseDepth = "noise_depth"
397+
case inpaintStart = "inpaint_start"
398+
case inpaintEnd = "inpaint_end"
399+
case variationCount = "variation_count"
400+
case loraStack = "lora_stack"
401+
case loraAPath = "lora_a_path"
402+
case loraAStrength = "lora_a_strength"
403+
case loraBPath = "lora_b_path"
404+
case loraBStrength = "lora_b_strength"
405+
case loraIntervalMin = "lora_interval_min"
406+
case loraIntervalMax = "lora_interval_max"
407+
}
408+
}
409+
297410
struct GenerateResponse: Decodable {
298411
let status: String
299412
let sound: SoundRecord?

apps/macos/Sources/ORAMApp/Services/DaemonClient.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ final class DaemonClient {
3434
try await get("/providers")
3535
}
3636

37+
func devices() async throws -> DevicesResponse {
38+
try await get("/devices")
39+
}
40+
3741
func credentialStatus() async throws -> [String: CredentialStatus] {
3842
try await get("/credentials/status")
3943
}
@@ -59,6 +63,10 @@ final class DaemonClient {
5963
try await post("/generate", payload: payload)
6064
}
6165

66+
func stableAudioRender(_ payload: StableAudioRenderPayload) async throws -> GenerateResponse {
67+
try await post("/stable-audio/render", payload: payload)
68+
}
69+
6270
func recordStart() async throws {
6371
let payload = ["target": "selected"]
6472
let _: EmptyResponse = try await post("/record/start", payload: payload)

apps/macos/Sources/ORAMApp/Stores/AppStore.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ final class AppStore: ObservableObject {
66
@Published var health: Health?
77
@Published var state: EngineState?
88
@Published var providers: [ProviderEngine] = []
9+
@Published var devices: DevicesResponse?
910
@Published var credentials: [String: CredentialStatus] = [:]
1011
@Published var sounds: [SoundRecord] = []
1112
@Published var waveforms: [Int: WaveformPeaks] = [:]
@@ -43,6 +44,7 @@ final class AppStore: ObservableObject {
4344
let nextState = try await client.state()
4445
state = nextState
4546
providers = try await client.providers().engines
47+
devices = try await client.devices()
4648
credentials = try await client.credentialStatus()
4749
sounds = try await client.sounds().sounds
4850
await refreshWaveforms(for: nextState)
@@ -86,6 +88,21 @@ final class AppStore: ObservableObject {
8688
}
8789
}
8890

91+
func stableAudioRender(_ payload: StableAudioRenderPayload) async {
92+
guard !payload.prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
93+
isGenerating = true
94+
defer { isGenerating = false }
95+
do {
96+
let response = try await client.stableAudioRender(payload)
97+
if let sound = response.sound {
98+
selectedSoundID = sound.id
99+
}
100+
await refreshAll()
101+
} catch {
102+
errorMessage = error.localizedDescription
103+
}
104+
}
105+
89106
func startRecording() async {
90107
do {
91108
try await client.recordStart()
@@ -207,9 +224,14 @@ final class AppStore: ObservableObject {
207224
}
208225
}
209226

210-
func updateAudioSettings(sampleRate: Int?, blockSize: Int?) async {
227+
func updateAudioSettings(sampleRate: Int?, blockSize: Int?, inputDevice: Int? = nil, outputDevice: Int? = nil) async {
211228
do {
212-
try await client.updateSettings(sampleRate: sampleRate, blockSize: blockSize)
229+
try await client.updateSettings(
230+
sampleRate: sampleRate,
231+
blockSize: blockSize,
232+
inputDevice: inputDevice,
233+
outputDevice: outputDevice
234+
)
213235
await refreshAll()
214236
} catch {
215237
errorMessage = error.localizedDescription

0 commit comments

Comments
 (0)