app/console/audio.rb

the library's own source, console 0.2.0 · back to Console::Audio

# Console::Audio -- sound effects and music.
#
# DragonRuby has no `play` method: you write into `args.audio` keyed by a symbol
# of your choosing, or push a one-shot into `args.outputs.sounds`. This module
# wraps both so a cart says `sfx(:jump)` and never touches a file path.
#
#   sfx :jump
#   sfx :hit, pitch: 0.8, gain: 0.6
#   music :title
#   music_gain 0.4
#   stop_music
#
# Music is tracked under one stable key, so calling `music` again crossfades by
# replacement instead of stacking loops. SFX are one-shots and self-clean.
module Console
  class Audio
    SFX_KEY_PREFIX = 'sfx_'
    MUSIC_KEY = 'console_music'

    attr_reader :registry
    attr_accessor :master_gain

    def initialize(args)
      @args = args
      @registry = {}
      @master_gain = 1.0
      @current_music = nil
      @current_music_name = nil
      @muted = false
      @missing = []
    end

    def args
      @args
    end

    # --- registration -----------------------------------------------------

    # teach the console where a sound lives
    #
    # The path is resolved against the booted cart, so `register :jump,
    # 'sounds/jump.wav'` inside a cart means that cart's own file.
    def register(name, path)
      @registry[name.to_s] = Assets.resolve path
    end

    def register_all(hash)
      hash.each { |k, v| register k, v }
    end

    def music_library(hash)
      hash.each { |k, v| register "music_#{k}", v }
    end

    def resolve(name)
      key = name.to_s
      @registry[key] ||
        @registry["music_#{key}"] ||
        @registry["sfx_#{key}"] ||
        begin
          @missing << key unless @missing.include?(key)
          nil
        end
    end

    def missing_names
      @missing.dup
    end

    # --- sfx --------------------------------------------------------------

    # Play a one-shot. gain and pitch are floats (DragonRuby warns otherwise).
    def sfx(name, options = {})
      return nil if @muted
      path = resolve name
      return nil unless path
      prim = { path: path }
      gain = options[:gain]
      pitch = options[:pitch]
      prim[:gain] = (gain.to_f * @master_gain) if gain
      prim[:pitch] = pitch.to_f if pitch
      prim[:looped] = false if options[:loop]
      @args.outputs.sounds << prim
      prim
    end

    # --- music ------------------------------------------------------------

    # Start (or swap) the looping music track. Repeating the same name is a
    # no-op, so this is safe to call every frame.
    def music(name, options = {})
      return nil if @muted
      return @current_music_name if name == @current_music_name
      stop_music
      path = resolve(name)
      unless path
        @current_music_name = name
        return nil
      end
      entry = {
        input: path,
        looping: true,
        gain: (options[:gain] || 1.0).to_f * @master_gain,
        pitch: (options[:pitch] || 1.0).to_f
      }
      @args.audio[MUSIC_KEY] = entry
      @current_music = entry
      @current_music_name = name
      entry
    end

    def stop_music
      @args.audio[MUSIC_KEY] = nil
      @current_music = nil
      @current_music_name = nil
      nil
    end

    def music_name
      @current_music_name
    end

    def music_playing?
      !@current_music_name.nil?
    end

    def music_gain(value)
      return @current_music[:gain] if value.nil? && @current_music
      return nil if value.nil?
      @args.audio[MUSIC_KEY] = nil if @current_music
      @master_gain = value.to_f
      music @current_music_name, gain: value if @current_music_name
      @master_gain
    end

    # Volume of the currently playing track only.
    def track_gain(value)
      return nil unless @current_music
      return @current_music[:gain] if value.nil?
      @args.audio[MUSIC_KEY] = { input: @current_music[:input],
                                  looping: true,
                                  gain: value.to_f,
                                  pitch: @current_music[:pitch] }
      value.to_f
    end

    # --- global -----------------------------------------------------------

    def mute
      @muted = true
      stop_music
      @args.audio.volume = 0.0
      true
    end

    def unmute
      @muted = false
      @args.audio.volume = 1.0
      true
    end

    def muted?
      @muted
    end

    def toggle_mute
      @muted ? unmute : mute
    end

    # Stop everything this console owns, leaving other audio alone.
    def stop_all
      stop_music
    end
  end
end