From f0a13293bb7565e03b6040901ca088d80b40a178 Mon Sep 17 00:00:00 2001 From: swipelab Date: Fri, 5 Jun 2026 18:19:33 +0300 Subject: [PATCH] P8.1: minimal match/clear SFX via iOS System Sound Services (sx FFI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feasibility spike outcome: iOS audio from sx is feasible with no sx-library change. System Sound Services is plain C, reached with the same `#foreign` FFI uikit.sx already uses (UIApplicationMain / dlsym / CACurrentMediaTime); AudioToolbox + CoreFoundation are linked per-target in build.sx. Smallest viable SFX: one short CC0 clip (Kenney Interface Sounds, CC0 1.0) played when a swap clears a match. Purely additive — audio.sx reads/writes no score/board/move state; the wiring in board_view only adds a call. - audio.sx: load clear.wav once, AudioServicesPlaySystemSound on clear - board_view.sx: trigger sfx_clear() on a legal swap that clears (>=1 round) - main.sx: allocate + init g_audio at boot - build.sx: link AudioToolbox + CoreFoundation on iOS - assets/audio/clear.wav (+ one-line CC0 credit in LICENSE.txt) Verified: ios-sim build links; 18/18 tests pass; sim boot log shows "[sx] audio: clear cue loaded" (AudioServicesCreateSystemSoundID succeeded, asset shipped in the bundle and decoded). --- assets/audio/LICENSE.txt | 9 ++++ assets/audio/clear.wav | Bin 0 -> 29660 bytes audio.sx | 88 +++++++++++++++++++++++++++++++++++++++ board_view.sx | 5 +++ build.sx | 4 ++ main.sx | 9 ++++ 6 files changed, 115 insertions(+) create mode 100644 assets/audio/LICENSE.txt create mode 100644 assets/audio/clear.wav create mode 100644 audio.sx diff --git a/assets/audio/LICENSE.txt b/assets/audio/LICENSE.txt new file mode 100644 index 0000000..783d8a6 --- /dev/null +++ b/assets/audio/LICENSE.txt @@ -0,0 +1,9 @@ +m3te sound effects +================== + +clear.wav — "confirmation_001" from Kenney's Interface Sounds pack +(www.kenney.nl), licensed CC0 1.0 (public domain): +https://creativecommons.org/publicdomain/zero/1.0/ + +Converted to mono 44.1 kHz signed-16-bit PCM WAV (afconvert) — the format +iOS System Sound Services loads directly via audio.sx. diff --git a/assets/audio/clear.wav b/assets/audio/clear.wav new file mode 100644 index 0000000000000000000000000000000000000000..1ddd5e5c2e4245140bed07ac2be5ab0936f683e7 GIT binary patch literal 29660 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 G^#1{;&s;wM literal 0 HcmV?d00001 diff --git a/audio.sx b/audio.sx new file mode 100644 index 0000000..9254ef5 --- /dev/null +++ b/audio.sx @@ -0,0 +1,88 @@ +// iOS sound effect via AudioToolbox System Sound Services (P8.1). +// +// A purely additive layer: it never reads or mutates score / board / move +// state — board_view tells it a match cleared and it plays one short clip. +// +// System Sound Services is plain C, so it is reached with sx's `#foreign` FFI +// exactly as uikit.sx reaches UIApplicationMain / dlsym / CACurrentMediaTime — +// no sx-library change. The AudioToolbox + CoreFoundation frameworks are linked +// per-target in build.sx. Every call is guarded by `inline if OS == .ios`, so +// other targets never reference these symbols nor need the frameworks. + +#import "modules/std.sx"; +#import "modules/std/objc.sx"; +#import "modules/compiler.sx"; + +// AudioToolbox — System Sound Services. SystemSoundID is a UInt32; OSStatus a +// SInt32 (0 == noErr); the clip's file is passed as a CFURLRef (opaque ptr). +AudioServicesCreateSystemSoundID :: (url: *void, out_id: *u32) -> s32 #foreign; +AudioServicesPlaySystemSound :: (sound_id: u32) #foreign; + +// CoreFoundation — build a file CFURL from an absolute path. `len` is a CFIndex +// (long); `is_dir` a Boolean (unsigned char); a NULL allocator = default. +CFURLCreateFromFileSystemRepresentation :: (allocator: *void, buffer: *u8, len: s64, is_dir: s8) -> *void #foreign; +CFRelease :: (cf: *void) #foreign; + +// libc — getcwd to absolutize the bundle-relative asset path. The platform +// chdir's to the bundle's resource dir at boot, so CWD is the .app and the +// game's other relative `assets/...` loads already resolve against it. +getcwd :: (buf: *u8, size: usize) -> *u8 #foreign; +c_strlen :: (s: *u8) -> usize #foreign "strlen"; + +// Loaded once at startup; `play` is then a single C call per cleared match. +GameAudio :: struct { + clear_id: u32; + loaded: bool; + + init :: (self: *GameAudio) { + self.clear_id = 0; + self.loaded = false; + inline if OS != .ios { return; } + + self.clear_id = load_system_sound("clear.wav"); + self.loaded = self.clear_id != 0; + if self.loaded { NSLog(xx "[sx] audio: clear cue loaded"); } + else { NSLog(xx "[sx] audio: load failed"); } + } + + play_clear :: (self: *GameAudio) { + inline if OS != .ios { return; } + if !self.loaded { return; } + AudioServicesPlaySystemSound(self.clear_id); + NSLog(xx "[sx] sfx clear"); + } +} + +// Create a SystemSoundID for `assets/audio/` (relative to the bundle). +// Returns 0 on any failure, which `play_clear` treats as "skip". +load_system_sound :: (name: string) -> u32 { + inline if OS != .ios { return 0; } + + cwd_buf : [1024]u8 = ---; + if getcwd(@cwd_buf[0], 1024) == null { return 0; } + cwd : string = ---; + cwd.ptr = @cwd_buf[0]; + cwd.len = cast(s64) c_strlen(@cwd_buf[0]); + + // CFURLCreateFromFileSystemRepresentation takes an explicit byte length, so + // the formatted path needs no NUL terminator. + path := format("{}/assets/audio/{}", cwd, name); + + url := CFURLCreateFromFileSystemRepresentation(null, path.ptr, path.len, 0); + if url == null { return 0; } + + sound_id : u32 = 0; + status := AudioServicesCreateSystemSoundID(url, @sound_id); + CFRelease(url); + if status != 0 { return 0; } + sound_id +} + +// The process-wide instance. main() allocates + inits it; board_view triggers +// the cue through `sfx_clear`. Null until init, so `sfx_clear` is a safe no-op +// before then. +g_audio : *GameAudio = null; + +sfx_clear :: () { + if g_audio != null { g_audio.play_clear(); } +} diff --git a/board_view.sx b/board_view.sx index 0c27434..a05e925 100644 --- a/board_view.sx +++ b/board_view.sx @@ -21,6 +21,7 @@ #import "board_fx.sx"; #import "gem_anim.sx"; #import "swipe.sx"; +#import "audio.sx"; // Fraction of a cell each gem occupies; the remainder is margin so a gem sits // inside its cell tile rather than touching the tile's edges. @@ -622,6 +623,10 @@ impl View for BoardView { mv := plan_and_commit(self.board, intent.a, intent.b); if self.anim != null { self.anim.begin(mv); } if self.fx != null { self.fx.begin(@mv); } + // SFX (P8.1). Additive only — plays a short cue when a swap + // actually clears a match; reads no score/board state and + // writes none. A legal move has >=1 cascade round. + if mv.legal and mv.rounds.len > 0 { sfx_clear(); } self.sel.clear(); } else { if hit := self.layout.point_to_cell(start) { diff --git a/build.sx b/build.sx index d7ce4fc..b5ccb9d 100644 --- a/build.sx +++ b/build.sx @@ -29,6 +29,10 @@ configure_build :: () { opts.add_framework("OpenGLES"); opts.add_framework("QuartzCore"); opts.add_framework("Metal"); + // System Sound Services SFX (audio.sx) — a short clip played when a + // swap clears a match. CoreFoundation supplies the file URL. + opts.add_framework("AudioToolbox"); + opts.add_framework("CoreFoundation"); opts.add_asset_dir("assets", "assets"); } } diff --git a/main.sx b/main.sx index 953bfea..fcd91ba 100644 --- a/main.sx +++ b/main.sx @@ -18,6 +18,7 @@ #import "board_anim.sx"; #import "board_fx.sx"; #import "gem_anim.sx"; +#import "audio.sx"; #run configure_build(); @@ -284,6 +285,14 @@ main :: () -> void { g_motion = xx context.allocator.alloc(size_of(GemMotion)); g_motion.init(); + // SFX (P8.1). Loads the one System Sound Services cue once; board_view + // plays it when a swap clears a match. Purely additive — never touches + // score/board/move state. On iOS the platform has already chdir'd to the + // bundle, so the cue's relative path resolves. No-op off iOS. + g_audio = xx context.allocator.alloc(size_of(GameAudio)); + memset(xx g_audio, 0, size_of(GameAudio)); + g_audio.init(); + // Deterministic-capture hooks: pin the animation clock and/or preselect a // cell so the always-on idle (and the select reaction) screenshot the same // way every time. No env set → fully live.