Graphics and Audio
Sigil provides cross-platform graphics, audio, and windowing for building games and creative applications. It's built on Sokol, a collection of lightweight C libraries.
Modules
(sigil app)— Application lifecycle, windowing, input events(sigil graphics)— 2D rendering, textures, transforms(sigil graphics font)— Text rendering with TrueType fonts(sigil graphics image)— Image loading(sigil audio)— Sound effects and music playback
Quick Start
A minimal graphical application:
(import (sigil app)
(sigil graphics))
(define (init) #f)
(define (frame dt)
(with-frame
(clear-screen 0.1 0.1 0.2)
(set-color 1.0 0.5 0.0)
(fill-rect 100 100 64 64)))
(define (cleanup) #f)
(app-run init frame cleanup)Application Lifecycle
app-run
The main entry point for graphical applications:
(app-run init-proc frame-proc cleanup-proc
[title: "Sigil App"]
[width: 800]
[height: 600])init-proc— Called once at startupframe-proc— Called every frame with delta time (seconds)cleanup-proc— Called before exit
Input
;; Keyboard
(key-down? 'space) ; Currently held
(key-pressed? 'return) ; Just pressed this frame
(key-released? 'escape) ; Just released this frame
;; Mouse
(mouse-x) ; X position in pixels
(mouse-y) ; Y position in pixels
(mouse-down? 'left) ; Button held
(mouse-pressed? 'right) ; Button just clicked
;; Lifecycle
(quit-requested?) ; User tried to close window
(request-quit) ; Trigger application exitKey symbols: 'a through 'z, '0 through '9, 'space, 'return, 'escape, 'left, 'right, 'up, 'down, 'tab, 'backspace, etc.
Frame Info
(frame-time) ; Seconds since last frame (delta time)
(time-elapsed) ; Seconds since app started
(frame-width) ; Frame buffer width in pixels
(frame-height) ; Frame buffer height in pixelsGraphics
Frame Structure
All drawing happens between begin-frame and end-frame:
(with-frame
(clear-screen 0 0 0)
;; drawing commands here
)The with-frame macro handles begin-frame and end-frame automatically.
Viewport and Coordinates
Set up a virtual coordinate system with automatic letterboxing:
(set-viewport 320 240) ; Virtual 320x240 resolution
(set-letterbox-color 0 0 0) ; Black barsThis maintains aspect ratio regardless of window size.
Colors
(set-color 1.0 0.0 0.0) ; Red (RGB, 0.0-1.0)
(set-color 1.0 1.0 1.0 0.5) ; White, 50% transparent (RGBA)Shape Primitives
;; Rectangles
(fill-rect x y width height)
(draw-rect x y width height)
;; Lines and points
(draw-line x1 y1 x2 y2)
(draw-point x y)
;; Triangles
(fill-triangle x1 y1 x2 y2 x3 y3)
(draw-triangle x1 y1 x2 y2 x3 y3)Textures
Load and draw images:
(import (sigil graphics image))
;; Load image from file
(define img (load-image "sprite.png"))
;; Create GPU texture
(define tex (load-texture img))
;; Draw at position
(draw-texture tex 100 100)
;; Draw scaled
(draw-texture tex 100 100 width: 64 height: 64)
;; Draw sprite sheet region
(draw-texture-region tex x y sx sy sw sh)Supported formats: PNG, JPEG, BMP, TGA, GIF.
Transforms
Apply transformations to subsequent drawing:
(push-transform)
(translate 100 100)
(rotate 0.5) ; radians
(scale 2.0 2.0)
(fill-rect -16 -16 32 32) ; Drawn transformed
(pop-transform)
;; Rotate around a point
(rotate-at angle center-x center-y)
(scale-at sx sy center-x center-y)
;; Reset to identity
(reset-transform)Text Rendering
(import (sigil graphics font))
;; Load TrueType font at pixel size
(define font (load-font "DejaVuSans.ttf" 24))
;; Draw text
(draw-text font "Hello, World!" 100 100)
;; Measure text width
(text-width font "Hello") ; => pixels
;; Draw centered
(draw-text-centered font "Centered" 400 300)Audio
Setup
Audio is initialized automatically when using app-run. For manual control:
(import (sigil audio))
(audio-setup)
;; ... use audio ...
(audio-shutdown)Sound Effects
Short sounds loaded entirely into memory:
(define explosion (load-sound "explosion.ogg"))
;; Play sound
(play-sound explosion)
(play-sound explosion volume: 0.5) ; 50% volume
;; Stop all sounds
(stop-all-sounds)Music
Streamed from disk for longer audio:
(define bgm (load-music "background.ogg"))
;; Playback control
(play-music bgm)
(play-music bgm loop: #t) ; Loop forever
(pause-music)
(resume-music)
(stop-music)
;; Check state
(music-playing?) ; => #t or #f
;; Volume
(set-music-volume 0.8) ; 0.0 to 1.0Supported format: OGG Vorbis.
Global Audio Control
(set-master-volume 0.5) ; Affects all audio
(mute-audio)
(unmute-audio)
(audio-muted?) ; => #t or #fStreaming Audio on Every Target
A game that makes its own sound (a synth, a tracker) pushes PCM into a stream with (sigil audio sink). The same source runs natively, where miniaudio drains the stream, and in the browser, where an AudioWorklet does (sigil-audio plus sigil-wasm-audio on the web config).
(import (sigil audio sink))
(define ctx (audio-context-open)) ; resume it from a tap on the web
(define rate (audio-context-sample-rate ctx))
(define music (open-audio-stream channels: 2 buffer-frames: 16384))
;; each frame: top the stream up toward the depth you want kept ahead
(push-audio-samples music pcm n-frames) ; interleaved float32
(audio-stream-depth music) ; frames queued, not yet heardThe depth a stream keeps ahead is how long a frame may stall before the output runs dry. It is also how late anything pushed now is heard: at 48 kHz a 12288-frame depth is 256 ms.
Cues: sounds that must be heard at once
Do not push a tap or a hit into a stream: it would wait behind everything queued. Load it once as a cue and play it by handle:
(define tap (load-audio-cue (render-tap rate) channels: 1)) ; float32 PCM at `rate`
(play-audio-cue tap volume: 0.8 pan: -0.25)
(set-audio-cue-volume! 0.5) ; the SFX volume (in the worklet, cues already playing too)
(stop-audio-cues!)In the browser a cue is mixed in the AudioWorklet in the next 128-frame render quantum (2.7 ms at 48 kHz), whatever any stream holds, cross-origin isolated or not; a page with no worklet plays it from a buffer made once, on the cue's first play. (audio-cue-mode) says which, and (audio-cue-lag) reports the frames the last play waited for its quantum. Natively a cue plays through the device callback, like play-sound.
Rendering below the device's rate
A producer can render at a lower rate than the device's and let the stream upsample (a 4-tap cubic, in the audio thread):
(define music (open-audio-stream rate: 24000))
(set-audio-stream-rate! music 22050) ; from the frames pushed nextA rate change applies from the stream's write position when you call it: frames already queued play at the rate they were rendered at, so a producer can step down without reopening the stream. Depth and room count the stream's own frames.
Knowing what the player heard
(audio-stream-underruns s) counts output frames the stream could not fill. Not all of them are audible: a stream with nothing to say that runs dry is silent either way. (audio-stream-underruns-sounding s) counts the frames that cut sound off (the stream ran dry within 128 frames of a sample above -80 dB), and (audio-stream-underruns-idle s) the rest. Judge a fix by the sounding count.
The rate ladder
On a weak device a synth can cost more per frame than a 60 fps frame has. (sigil audio ladder) measures what the producer costs and steps its render rate down a rung when the steady cost is over budget:
(import (sigil audio ladder))
(define ladder (make-rate-ladder rate
rungs: '(32000 24000 22050)
budget-ms: 4.0
key: "ring 16384")) ; the buffering it measures under
;; each pull of the producer, timed
(rate-ladder-measure ladder n (lambda () (render! buf n)))
;; each cue: the next 150 ms of pulls share a frame with the tap
(rate-ladder-quiet! ladder)
;; once a frame
(let ((new-rate (rate-ladder-step! ladder)))
(when new-rate
(reopen-synth-at! new-rate)
(set-audio-stream-rate! music new-rate)))
(rate-ladder-report ladder) ; => "48000/24000 BY LADDER@20.2S"The median of 60 measured pulls decides, never a single pull. Pulls within 150 ms of a cue and the 30 after an open or a step are not measured. The rate only goes down, and never below floor: (default: the lowest rung); rate-ladder-pin! and rate-ladder-unpin! are the only ways to set it higher. To keep a device's rung across runs, pass remember: a procedure that stores (rate-ladder-remembered ladder) and call rate-ladder-restore! with it at the next boot. A stored rate is ignored under a different key:, because the rung a device lands on depends on the buffering around the measurement: one phone settled a rung higher with a 320 ms ring than with a 256 ms ring.
Example: Simple Game
(import (sigil app)
(sigil graphics)
(sigil graphics font)
(sigil audio))
(define player-x 400)
(define player-y 300)
(define speed 200)
(define font #f)
(define jump-sound #f)
(define (init)
(set-viewport 800 600)
(set! font (load-font "game-font.ttf" 16))
(set! jump-sound (load-sound "jump.ogg")))
(define (frame dt)
;; Input
(when (key-down? 'left)
(set! player-x (- player-x (* speed dt))))
(when (key-down? 'right)
(set! player-x (+ player-x (* speed dt))))
(when (key-pressed? 'space)
(play-sound jump-sound))
(when (key-pressed? 'escape)
(request-quit))
;; Render
(with-frame
(clear-screen 0.2 0.3 0.4)
;; Player
(set-color 1.0 0.8 0.0)
(fill-rect (- player-x 16) (- player-y 16) 32 32)
;; UI
(set-color 1.0 1.0 1.0)
(draw-text font "Arrow keys to move, Space to jump" 10 10)))
(define (cleanup) #f)
(app-run init frame cleanup
title: "My Game"
width: 800
height: 600)Building a Game
Add the graphics and audio packages to your dependencies:
;; package.sgl
(package
name: "my-game"
version: "0.1.0"
dependencies:
(list
(from-workspace name: "sigil-app")
(from-workspace name: "sigil-graphics")
(from-workspace name: "sigil-audio"))
entry: '(my-game main))Build and run:
sigil runCreate a standalone executable:
sigil build --config releaseCross-Platform Notes
The graphics and audio libraries work on:
- Linux (OpenGL)
- macOS (Metal)
- Windows (D3D11)
- WebAssembly (WebGL)
The same code runs on all platforms — Sokol handles the graphics backend abstraction.