dragonruby-audio

Advanced audio in DragonRuby GTK — spatial audio, procedural synthesis, beat synchronisation, crossfading, audio queues. Use when the user asks about DragonRuby music, sound effects beyond basic playback, rhythm games, or generated audio.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dragonruby-audio" with this command: npx skills add nitemaeric/dragonruby-skills/nitemaeric-dragonruby-skills-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)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

dragonruby

No summary provided by upstream source.

Repository SourceNeeds Review
General

dragonruby-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

dragonruby-rendering

No summary provided by upstream source.

Repository SourceNeeds Review
General

dragonruby-yard

No summary provided by upstream source.

Repository SourceNeeds Review