// 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(); } }