dragonruby-audio
This skill covers advanced DragonRuby audio patterns. For basic sound playback see the main dragonruby skill.
Audio API Overview
Audio is controlled via args.audio (a persistent hash) and args.outputs.sounds (fire-and-forget).
# One-shot (auto-removed on completion)
args.outputs.sounds << 'sounds/coin.wav'
args.outputs.sounds << { path: 'sounds/hit.wav', gain: 0.5 }
# Managed (persists, looping or controlled)
args.audio[:music] = {
input: 'sounds/theme.ogg',
looping: true,
gain: 1.0,
pitch: 1.0, # 1.0 = normal speed
paused: false,
}
args.audio.delete(:music) # stop
# After loading:
args.audio[:music].playtime # seconds elapsed
args.audio[:music].playlength # total duration
args.audio.volume = 0.8 # global volume multiplier
Supported formats: .wav, .ogg. Resample to 44.1kHz max via ffmpeg.
Spatial Audio
Map screen coordinates to the normalised -1..1 range the engine expects:
def screen_to_audio_x(screen_x, screen_w = 1280)
(screen_x / screen_w.to_f * 2.0) - 1.0
end
def screen_to_audio_y(screen_y, screen_h = 720)
(screen_y / screen_h.to_f * 2.0) - 1.0
end
# Attach audio position to a moving entity:
args.audio[:explosion] = {
input: 'sounds/boom.wav',
x: screen_to_audio_x(entity.x),
y: screen_to_audio_y(entity.y),
z: 0.0,
gain: 1.0
}
# Update position each tick for a moving source:
if args.audio[:engine]
args.audio[:engine].x = screen_to_audio_x(ship.x)
args.audio[:engine].y = screen_to_audio_y(ship.y)
end
You can hang arbitrary metadata on the audio hash for convenience — extra keys are ignored by the engine:
args.audio[:sfx] = {
input: 'sounds/laser.wav',
gain: 1.0,
# custom metadata:
source_entity_id: enemy.entity_id,
started_at: Kernel.tick_count
}
Crossfading Between Tracks
Smoothly transition between two loops without clicks:
def tick_music(args)
args.state.main_track ||= :track_a
args.state.other_track ||= :track_b
args.audio[:track_a] ||= { input: 'sounds/music_a.ogg', looping: true, gain: 1.0 }
args.audio[:track_b] ||= { input: 'sounds/music_b.ogg', looping: true, gain: 0.0 }
# Trigger crossfade every 600 ticks (10 seconds):
if Kernel.tick_count.zmod?(600)
args.state.main_track, args.state.other_track =
args.state.other_track, args.state.main_track
end
# Fade toward targets:
target_a = args.state.main_track == :track_a ? 1.0 : 0.0
target_b = args.state.main_track == :track_b ? 1.0 : 0.0
args.audio[:track_a].gain = args.audio[:track_a].gain.lerp(target_a, 0.02)
args.audio[:track_b].gain = args.audio[:track_b].gain.lerp(target_b, 0.02)
end
Audio Queue with Timed Playback
Decouple scheduling from playback — queue audio for future ticks:
args.state.audio_queue ||= []
# Schedule a sound:
args.state.audio_queue << {
id: :"sfx_#{Kernel.tick_count}",
input: 'sounds/beep.wav',
gain: 0.8,
queue_at: Kernel.tick_count + 30 # play in 0.5 seconds
}
# Process queue each tick:
def process_audio_queue(args)
ready, pending = args.state.audio_queue.partition { |e| e[:queue_at] <= Kernel.tick_count }
args.state.audio_queue = pending
ready.each { |e| args.audio[e[:id]] = e }
end
Gain Decay (Natural Fade-out)
Apply a per-tick decay rate instead of manual envelope tracking:
args.audio.each do |id, track|
next unless track.is_a?(Hash) && track[:decay_rate]
track[:gain] -= track[:decay_rate]
args.audio.delete(id) if track[:gain] <= 0
end
# Create a decaying sound:
duration_ticks = 120
args.audio[:boom] = {
input: 'sounds/explosion.wav',
gain: 1.0,
decay_rate: 1.0 / duration_ticks
}
Procedural Synthesis
Generate audio from code using mathematical wave functions.
Sine wave
def sine_wave(frequency: 440, duration_ticks: 60, gain: 0.5)
sample_rate = 44100
num_samples = (sample_rate * duration_ticks / 60.0).ceil
samples = num_samples.map do |i|
gain * Math.sin(2.0 * Math::PI * frequency * i / sample_rate)
end
[2, sample_rate, samples] # [channels, sample_rate, data]
end
args.audio[:beep] = { input: sine_wave(frequency: 880, duration_ticks: 30) }
Caching by frequency
args.state.wave_cache ||= {}
args.state.wave_cache[440] ||= sine_wave(frequency: 440, duration_ticks: 60)
args.audio[:note_a] = { input: args.state.wave_cache[440] }
Harmonic decomposition (bell-like timbres)
def bell_sound(fundamental: 440)
harmonics = [
{ ratio: 0.5, gain: 1.0, duration: 120 },
{ ratio: 1.0, gain: 0.8, duration: 90 },
{ ratio: 2.0, gain: 0.5, duration: 60 },
{ ratio: 3.0, gain: 0.3, duration: 40 }
]
harmonics.map.with_index do |h, i|
id = :"bell_harmonic_#{Kernel.tick_count}_#{i}"
args.audio[id] = {
input: sine_wave(frequency: fundamental * h[:ratio],
duration_ticks: h[:duration],
gain: h[:gain]),
decay_rate: h[:gain] / h[:duration]
}
end
end
Interactive slider for real-time pitch/gain control
slider_rect = { x: 100, y: 300, w: 400, h: 20 }
if args.inputs.mouse.held && args.inputs.mouse.inside_rect?(slider_rect)
t = (args.inputs.mouse.x - slider_rect[:x]).to_f / (slider_rect[:w] - 1)
args.audio[:synth].pitch = 0.5 + t * 1.5 # 0.5x to 2.0x pitch
end
Beat Synchronisation
Accumulate fractional beats per tick to avoid rhythmic drift:
BPM = 120.0
args.state.beat_accumulator ||= 0.0
args.state.beats_per_tick ||= BPM / (60.0 * 60) # beats per tick at 60fps
args.state.beat_accumulator += args.state.beats_per_tick
current_beat = args.state.beat_accumulator.to_i
if current_beat != args.state.last_beat
args.state.last_beat = current_beat
# Fire on each beat:
args.outputs.sounds << 'sounds/kick.wav' if (current_beat % 4) == 0
args.outputs.sounds << 'sounds/snare.wav' if (current_beat % 4) == 2
end
Latency calibration
# Allow player to tap to calibrate audio latency
if args.inputs.keyboard.key_down.space
expected_beat_tick = args.state.quarter_beat_occurred_at
actual_tick = Kernel.tick_count
args.state.calibration_ticks += (actual_tick - expected_beat_tick) / 2
end
Note Mapping (Musical Scale)
A4 = 440.0
NOTE_OFFSETS = {
c: -9, d: -7, e: -5, f: -4, g: -2, a: 0, b: 2
}
def note_frequency(note, octave = 4)
semitones = NOTE_OFFSETS[note] + (octave - 4) * 12
A4 * (2.0 ** (semitones / 12.0))
end
# Usage:
freq = note_frequency(:c, 4) # => ~261.6 Hz (Middle C)
Waveform Visualisation
Render a waveform for debugging/aesthetics:
def render_waveform(args, samples, rect)
x_scale = rect[:w].to_f / samples.length
y_scale = rect[:h] / 2.0
cy = rect[:y] + rect[:h] / 2
args.outputs.static_lines << samples.map.with_index do |amp, i|
{ x: rect[:x] + i * x_scale, y: cy + amp * y_scale,
x2: rect[:x] + (i+1) * x_scale, y2: cy + (samples[i+1] || amp) * y_scale,
r: 0, g: 200, b: 255 }
end
end
Defaults Pattern for Audio Options
AUDIO_DEFAULTS = { gain: 1.0, pitch: 1.0, looping: false, fade_out: false }
def play_sound(args, id:, input:, **opts)
args.audio[id] = AUDIO_DEFAULTS.merge(opts).merge(input: input)
end
play_sound(args, id: :laser, input: 'sounds/laser.wav', gain: 0.6)
More from nitemaeric/dragonruby-skills
dragonruby
Build games and prototypes with DragonRuby GTK (DRGTK). Use this skill when the user asks to write DragonRuby game code, implement game mechanics, work with the DRGTK API, or debug DragonRuby projects.
9dragonruby-ui
UI controls in DragonRuby GTK — buttons, checkboxes, toggles, scroll views, menus, input remapping, tooltips, progress bars, accessibility. Use when building game menus, HUDs, settings screens, or in-game UI widgets.
6dragonruby-yard
Set up YARD documentation and Solargraph LSP autocomplete for DragonRuby GTK projects. Use when the user asks about IDE autocomplete, type support, Solargraph, or editor integration for DragonRuby.
5dragonruby-rendering
Advanced DragonRuby GTK rendering — render targets, cameras with world/screen space, pixel arrays, HD/lowrez resolution, thick lines, blendmodes, viewport culling, tiling textures. Use when the user asks about advanced visual effects, camera systems, or rendering performance.
5dragonruby-3d
3D graphics techniques in DragonRuby GTK — raycasting, Mode7 floor projection, matrix transformations, wireframe rendering, sprite-based 3D, VR patterns. Use when the user asks about 3D visuals or pseudo-3D effects.
2dragonruby-pathfinding
Pathfinding algorithms in DragonRuby GTK — A*, BFS, flood fill, priority queues, spatial hashing, quad trees, line-of-sight. Use when the user asks about enemy AI navigation, movement grids, or reachability.
2