From d2b885b3fc0afed6d308b578e32e826f8913e51c Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Wed, 15 Jul 2026 19:17:01 +0000 Subject: [PATCH] menuband: build take DMG in temp then move to Desktop + ready chime The Desktop no longer shows a half-written / cover-less file mid-build: the staging dir AND the .dmg are assembled in one temp work dir, the cover is stamped there (waiting for the async resource-fork write to land), then the finished artifact is moved onto the Desktop in one step. A short crystalline ReadyChime (self-contained bell arpeggio, like CountInWhoosh) rings when the DMG lands. --- slab/menuband/Sources/MenuBand/MenuBandController.swift | 4 +++- slab/menuband/Sources/MenuBand/ReadyChime.swift | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ slab/menuband/Sources/MenuBand/TakeDMG.swift | 46 ++++++++++++++++++++++++++++++++++++---------- 3 file(s) changed, 107 insertion(s)(+), 11 deletion(s)(-) diff --git a/slab/menuband/Sources/MenuBand/MenuBandController.swift b/slab/menuband/Sources/MenuBand/MenuBandController.swift --- a/slab/menuband/Sources/MenuBand/MenuBandController.swift +++ b/slab/menuband/Sources/MenuBand/MenuBandController.swift @@ -1453,7 +1453,9 @@ // The sidecar .mid (editable notes for Ableton) rides inside the DMG. let extras = [take.midi].compactMap { $0 } // DMG only — the .mbtape (zip) alternative is dropped. `buildMbtape` // stays in TakeDMG if we ever want to flag it back on. - return TakeDMG.build(wav: src, extras: extras, name: name, coverIcon: cover) + let dmg = TakeDMG.build(wav: src, extras: extras, name: name, coverIcon: cover) + if dmg != nil { ReadyChime.shared.play() } // cool "release" chime when the artifact lands + return dmg } /// State-change observer. Drops the pins whenever the tape goes diff --git a/slab/menuband/Sources/MenuBand/ReadyChime.swift b/slab/menuband/Sources/MenuBand/ReadyChime.swift new file mode 100644 --- /dev/null +++ b/slab/menuband/Sources/MenuBand/ReadyChime.swift @@ -0,0 +1,68 @@ +import AVFoundation + +/// A short crystalline "release" chime — a rising bell arpeggio that rings when +/// a finished take DMG lands on the Desktop. Runs on its own tiny engine so it +/// never touches the instrument's audio graph (mirrors `CountInWhoosh`). +final class ReadyChime { + static let shared = ReadyChime() + + private let engine = AVAudioEngine() + private let player = AVAudioPlayerNode() + private let sampleRate = 44_100.0 + private var started = false + + private func ensureStarted() { + guard !started else { return } + engine.attach(player) + guard let fmt = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 2) else { return } + engine.connect(player, to: engine.mainMixerNode, format: fmt) + do { try engine.start(); started = true } + catch { NSLog("ReadyChime: engine start failed — \(error)") } + } + + /// Ring it: a staggered bell arpeggio spelling a bright D major add-shimmer, + /// each note a decaying sine + a glassy 2nd partial, panned to fan out. + func play() { + ensureStarted() + guard started, + let fmt = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 2) + else { return } + + let dur = 1.4 + let n = Int(sampleRate * dur) + guard let buf = AVAudioPCMBuffer(pcmFormat: fmt, frameCapacity: AVAudioFrameCount(n)), + let left = buf.floatChannelData?[0], + let right = buf.floatChannelData?[1] + else { return } + buf.frameLength = AVAudioFrameCount(n) + + struct Bell { let freq: Double; let start: Double; let pan: Double } + let bells: [Bell] = [ + Bell(freq: 587.33, start: 0.00, pan: -0.35), // D5 + Bell(freq: 739.99, start: 0.08, pan: -0.10), // F#5 + Bell(freq: 880.00, start: 0.16, pan: 0.15), // A5 + Bell(freq: 1174.66, start: 0.26, pan: 0.35), // D6 shimmer + ] + let twoPi = 2.0 * Double.pi + for i in 0.. Int in + guard let path = path else { return 0 } + return max(0, getxattr(path, key, nil, 0, 0, 0)) + } + if size >= minBytes { return } + usleep(20_000) // 20 ms + } while Date() < deadline } @discardableResult -- tangled.sh