[Ruby] FMOD Low-Level API Wrapper

Started by ForeverZer0, March 22, 2018, 04:52:32 pm

Previous topic - Next topic

ForeverZer0

March 22, 2018, 04:52:32 pm Last Edit: March 23, 2018, 02:55:02 pm by ForeverZer0
Introduction
I was in need of a good audio API when working on a C# application, and was unable to find an existing library that fit my needs, so I ended up making my own using the FMOD Low-Level API (the predecessor to the legacy FmodEx). After working with FMOD, and learning the ins-and-outs of how it worked, I saw the potential for making a Ruby port as well. Searching online turned up very little as far as any type of wrapper that could ever be considered close to complete, and only had the basic functionality of FMOD implemented. This including existing RPG Maker scripts that wrapped the basic functionality of the old FmodEx, but nothing more. This prompted me to write my own wrapper.

Being my programming experience begin with Chaos Project and old Ruby 1.8.2, I decided to write in a way that would remain compatible with old versions of Ruby, and foregoing using any Ruby gems or functionality that didn't exist in Ruby 1.8.2, and decided to stick with using Win32API to wrap the FMOD functions, and didn't allow myself the convenience of using a new library like Fiddle of FFI to make things simple.

Currently, the project is approaching the alpha stage. Nearly ALL core functions are implemented, but there has been no in-depth testing yet, and there are still a few mostly uncommon functionalities that need worked on. I have spent A LOT of time making documentation for it, as it is a huge API that definitely has a learning curve. If you don't believe me, see the spoiler.

Spoiler: ShowHide


In the future, I will be releasing this as a gem, but as it stands now, the project can be loaded into RPG Maker with a simple "require", and built upon that. I may even create a basic Audio module rewrite with this codebase in the future, but if anyone else feels like doing it, feel free to check out the code and build upon it for RPG Maker Audio module.



Features

  • Support for 20+ audio formats "out of the box", including all the RPG Maker standards (.mp3, .ogg, .mid, .wma, ect.) with support for more via a plugin system using existing FMOD plugins libraries

  • All basic playback control (play, pause, resume, seek, volume, etc, etc.) that you would expect of any audio player.

  • 20+ built-in effects for sound, including pitch shifting, flange, advanced reverb environments, multi-band parametric equalizer, chorus, tremolo, oscillator,  and many, many more.

  • Streaming of all audio formats, not just MP3

  • Full 3D and 2D support, with distance/directional based sound, each with own reverb environment.

  • Support for custom speaker setups, full surround sound support for up to 12 speakers.

  • Driver selection.

  • Supports music tags (ID3, etc) if that's your thing...

  • Much, much more.



As I stated above, this project is still in its infancy, but already far surpasses any existing audio API that I could find for the Ruby language, and because I love you all, I kept it compatible with RPG Maker XP and above.

This post is meant to a preview for a full release, and I would recommend not to port it as it currently is in a completed project. As so much yet is untested to any passable standard, lacks complete protection against segmentation faults, etc.



Download

Scipts

FMOD DLL (Required)

Documentation

Source - Github



Getting Started

Download the scripts, place unzipped directory in RPG Maker project folder along with the downloaded FMOD dll.
Open script editor, add the line:
require File.expand_path('lib/fmod')


Just to give you idea to get started, snippet to play a sound:
sys = FMOD::System.create
sound = sys.create_sound('My Audio File.mp3')
sys.play_sound(sound)


Things to remember, the documentation, which is posted above, is currently lacking a lot of examples, so feel free to download the help file from FMOD's website, which can still be helpful on how to do things until the examples are created.

The documentation is not compiled, but can simply be unzipped and opening the "index" file within, which will open it in your browser.

FMOD object's need disposed! Once you are done using a sound, system, etc, call dispose or release on it to free the underlying handle. Obviously this behavior would be taken care of automatically when ported to RPG Maker. This is a developer's API, not an end-user product.

Below is generic audio module I wrote real quick just as an example as an example. It is already far more capable than the built-in Audio, but really more intended as just boiler-plate code.
Audio Module: ShowHide
$:.unshift(File.join(File.dirname(__FILE__), 'lib'))

require 'fmod'

# Undefine the old Audio module so there is no interference.
if defined? Audio
  Object.send(:remove_const, :Audio)
end

##
# Complete redefinition of the Audio module with additional capabilities.
#
# @note <b>Notes on Pitch</b>
#
#   By default, pitch is changed by simply altering the speed of the sound. If
#   you wish to accomplish true pitch shifting, use the PitchShifter DSP unit,
#   which alters pitch without changing playback speed.
# @author Eric "ForeverZer0" Freed
module Audio

  ##
  # Only change if you have a project that renames the "Game.exe" and "Game.ini"
  EXE = 'Game'

  ##
  # File extensions that will be included for searching audio paths.
  # For performance reasons, only include extensions that your project uses.
  EXTENSIONS = %w[.mp3 .ogg .mid .wma]

  ##
  # Channel names, as symbols. Removing the default names listed here will break
  # backwards compatibility.
  CHANNELS = [:BGM, :BGS, :ME, :SE].freeze

  include FMOD::Enums
  include FMOD::Effects

  ##
  # Retrieves a hash using the symbols located in {CHANNELS} mapping to each
  # channel handle.
  # @return [Hash<Symbol, FMOD::Channel>] The {FMOD::Channel} instances.
  def self.channels
    @channels
  end

  ##
  # Begins playback on the specified channel.
  #
  # File extensions may be omitted. All paths should be relative to the base
  # project directory. RTP directories will also be searched if file is not
  # found locally.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param filename [String] Relative filename to play. File extensions may be
  #   omitted.
  # @param volume [Integer] Volume level. By default this within range of +0+
  #   to +100+, but FMOD supports amplification and higher values.
  # @param pitch [Integer] Pitch level. +50+ to +200+.
  # @param looping [Boolean] +true+ to have sound loop; otherwise +false+.
  # @param effects [FMOD::DSP] Optional DSP effects to apply to the sound. This
  #   is more efficient than adding later.
  # @return [FMOD::Channel] The channel instance that is playing the sound.
  def self.play(channel, filename, volume, pitch, looping, *effects)
    if channel_playing?(channel, filename)
      @channels[channel].volume = volume * 0.01
      @channels[channel].pitch = pitch * 0.01
      return
    end
    @sounds[channel].release unless @sounds[channel].nil?
    mode = looping ? MODE[:loop_normal] : MODE[:default]
    @sounds[channel] = @fmod.create_stream(full_path(filename), mode)
    @channels[channel] = @fmod.play_sound(@sounds[channel], nil, true)
    @channels[channel].volume = volume * 0.01
    @channels[channel].pitch = pitch * 0.01
    effects.reverse.each { |dsp| @channels[channel].add_dsp 0, dsp }
    @channels[channel].resume
  end

  ##
  # Creates a DSP unit that can be applied to a channel to alter sound.
  #
  # Optionally applies it to channels if specified.
  # @param type [Integer, Class] Either an integer value specified in the
  #   {DSP_TYPE} enumeration, or a class that is defined in the {FMOD::Effects}
  #   namespace.
  # @param channels [Symbol] Any number of channel names defined in {CHANNELS}
  #   to apply the effect to.
  # @return [FMOD::DSP] The created DSP effect.
  def self.create_effect(type, *channels)
    @effects << @fmod.create_dsp(type)
    channels.each { |channel| add_effect(channel, @effects.last) }
    @effects.last
  end

  ##
  # Adds an existing DSP effect to a channel.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param effect [FMOD::DSP] AN existing DSP effect.
  # @return [void]
  def self.add_effect(channel, effect)
    return unless channel_valid?(channel) && effect.is_a?(FMOD::DSP)
    @channels[channel].add_dsp(0, effect)
  end

  ##
  # Removes an existing DSP effect from a channel.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param effect [FMOD::DSP, Integer] Either the DSP effect instance to remove,
  #   or the index into the channel's DSP chain to remove.
  # @return [void]
  def self.remove_effect(channel, effect)
    return unless channel_valid?(channel)
    @channels[channel].remove_dsp(effect)
  end

  ##
  # Retrieves the loaded effect on the channel at the given index.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param index [Integer] Index into the channel's DSP chain to retrieve the
  #   desired effect.
  # @return [FMOD::DSP, nil] The desired DSP effect, or +nil+ if channel or
  #   index was invalid.
  def self.effect(channel, index)
    return unless channel_valid?(channel)
    @channels[channel][index]
  end

  ##
  # Returns an array of created DSP effects.
  # @param channel [Symbol] If specified, returns only effects currently applied
  #   to the channel.
  # @return [Array<FMOD::DSP>] Array of all effects created by the user.
  def self.effects(channel = nil)
    return @effects if channel.nil?
    return unless channel_valid?(channel)
    @channels[channel].dsps
  end

  ##
  # Retrieves the current volume of a sound.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @return [Integer] The current volume, a value typically between 0 and 100.
  def self.volume(channel)
    channel_valid?(channel) ? (@channels[channel].volume * 100).to_i : 0
  end

  ##
  # Changes the volume of a channel.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param volume [Integer] The volume to set, typically a value between 0 and
  #   100, though amplification is supported.
  # @return [Channel, nil] The channel object if channel was valid; otherwise
  #   +nil+.
  def self.change_volume(channel, volume)
    return unless channel_valid?(channel)
    @channels[channel].volume  = volume * 0.01
    @channels[channel]
  end

  ##
  # Retrieves the current pitch of a sound.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @return [Integer] The current pitch, a value between 50 and 200.
  def self.pitch(channel)
    channel_valid?(channel) ? (@channels[channel].pitch * 100).to_i : 100
  end

  ##
  # Changes the pitch of a channel.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param pitch [Integer] The pitch to set, a value between 50 (one octave
  #   down) and 200 (one octave up).
  # @note Out of range values will be clamped.
  # @return [Channel, nil] The channel object if channel was valid; otherwise
  #   +nil+.
  def self.change_pitch(channel, pitch)
    return unless channel_valid?(channel)
    @channels[channel].pitch = pitch * 0.01
    @channels[channel]
  end

  ##
  # Retrieves the current playback position of the sound.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @return [Integer] The current position, in milliseconds.
  def self.position(channel)
    channel_valid?(channel) ? @channels[channel].position : 0
  end

  ##
  # Moves the playback position to the specified location in the sound.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param ms [Integer] The playback position, in milliseconds, to set.
  # @return [Channel, nil] The channel object if channel was valid; otherwise
  #   +nil+.
  def self.seek(channel, ms)
    return unless channel_valid?(channel)
    @channels[channel].seek(ms)
  end

  ##
  # Stops playback on the specified channel.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @return [Channel, nil] The channel object if channel was valid; otherwise
  #   +nil+.
  def self.stop(channel)
    return unless channel_valid?(channel)
    @channels[channel].stop
  end

  ##
  # Pauses playback on the specified channel.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @return [Channel, nil] The channel object if channel was valid; otherwise
  #   +nil+.
  def self.pause(channel)
    return unless channel_valid?(channel)
    @channels[channel].pause
  end

  ##
  # Resumes a previously paused channel, or does nothing if channel is invalid
  # or not paused.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @return [Channel, nil] The channel object if channel was valid; otherwise
  #   +nil+.
  def self.resume(channel)
    return unless channel_valid?(channel)
    @channels[channel].resume
  end

  ##
  # This is same as "time-stretching", changing the speed of playback without
  # effecting the pitch of the sound.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param speed [Float] A values between 0.5 (half speed) and 2.0 (double
  #   speed).
  # @return [void]
  def self.tempo(channel, speed)
    return unless channel_valid?(channel)
    speed = [0.5, speed, 2.0].sort[1]
    pitch_shifter = @channels[channel].dsps.find { |dsp| dsp.type == 13 }
    if pitch_shifter.nil?
      pitch_shifter = create_effect(13, channel)
      pitch_shifter.fft_size = 4096
    end
    @channels[channel].pitch = speed
    pitch_shifter.pitch = 1.0 / speed
  end

  ##
  # Changes the playback speed of a sound. Altering the speed also changed the
  # pitch.
  #
  # Negative values are accepted to play a sound backwards.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param hz [Numeric] The frequency, in Hz, to play the channel at. Default
  #   speed is +44100+ Hz, so +-44100+ is normal speed backwards, +22050+ is
  #   half speed, +88200+ is double, etc.
  def self.frequency(channel, hz)
    return unless channel_valid?(channel)
    @channels[channel].frequency = hz
  end

  ##
  # Transitions volume on the specified channel to the a target volume of the
  # given number of milliseconds.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param time [Integer] Length of time in milliseconds the transition will
  #   occur over.
  # @param target_volume [Integer] The target volume to transition to, typically
  #   a value between 0 and 100, though amplification is supported.
  # @note When volume is transitioned to +0+, the channel will be stopped when
  #   after reaching that volume.
  def self.fade(channel, time, target_volume = 0)
    return unless channel_valid?(channel)
    volume = @channels[channel].volume - (target_volume * 0.01)
    frames = time / Graphics.frame_rate
    increment = volume / frames
    @fade_points.delete_if { |fade| fade[0] == channel }
    @fade_points.push [channel, frames, increment, target_volume]
  end

  ##
  # Checks if the specified channel is still valid and references an existing
  # FMOD Channel. FMOD reuses channels automatically and can "steal" them for
  # higher priority sounds when necessary, or more commonly after a sound has
  # completed playing and the channel is stopped.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @return [Boolean] +true+ if channel is still valid; otherwise +false+.
  def self.channel_valid?(channel)
    return false if @channels[channel].nil?
    @channels[channel].valid?
  end

  ##
  # Checks if the given channel is currently playing. If a filename is
  # specified, additionally checks if that is the file being played.
  # @param channel [Symbol] The name of the channel being played, a value
  #   defined in the {CHANNELS} array.
  # @param filename [String, nil] A filename to check if is playing on the
  #   channel, or +nil+ to ignore.
  # @return [Boolean] Returns +true+ if the channel is playing, and additionally
  #   if the filename is playing if specified; otherwise +false+.
  def self.channel_playing?(channel, filename = nil)
    return false unless channel_valid?(channel)
    if @channels[channel].playing?
      name = @channels[channel].sound.name
      return File.basename(name, File.extname(name)) == File.basename(filename)
    end
    false
  end

  ##
  # Initializes the Audio module.
  # @api private
  # @return [void]
  def self.initialize
    return if defined? @fmod
    @fmod = FMOD::System.create
    build_paths
    @found = {}
    @sounds = {}
    @channels = {}
    @effects = []
    @fade_points = []
  end

  ##
  # Updates the FMOD system object and fading.
  # @note This is called automatically each frame with each Graphics update,
  #   there is no need to call this method manually.
  # @api private
  # @return [void]
  def self.update
    @fmod.update
    @fade_points.delete_if do |fade|
      if channel_valid?(fade[0])
        @channels[fade[0]].volume -= fade[2]
        fade[1] -= 1
        if fade[1] <= 0
          @channels[fade[0]].stop if fade[3] == 0
          return true
        end
        return false
      end
      true
    end
  end

  ##
  # Builds the search paths that will be used for searching audio files,
  # including RTP paths defined in the projects .ini file.
  # @return [void]
  # @api private
  def self.build_paths
    ini = Win32API.new('kernel32','GetPrivateProfileString','pppplp','l')
    ini.call(EXE, 'Library', '', lib = "\0" * 256, 256, "./#{EXE}.ini")
    lib.delete!("\0")
    rtp_path = Win32API.new(lib, 'RGSSGetRTPPath', 'l', 'l')
    path_with_rtp = Win32API.new(lib, 'RGSSGetPathWithRTP', 'l', 'p')
    @search_paths = [Dir.getwd]
    (1..3).each do |i|
      id = rtp_path.call(i)
      rtp = path_with_rtp.call(id)
      next if rtp == ''
      @search_paths << rtp.gsub('\\', '/')
    end
  end

  ##
  # Returns the full path from a relative path. File extension may be omitted,
  # in which case any matching file that has an extension in the {EXTENSIONS}
  # setting will be retrieved.
  #
  # Searches locally first, and RTP directories if not found.
  # @param relative_path [String] A filename relative to the base directory.
  # @api private
  # @return [String] The full path to the file, including found extension.
  # @raise [Errno::ENOENT] Raised when a matching file cannot be found.
  # @note Once found, filenames are cached to reduce overhead on repeated calls.
  def self.full_path(relative_path)
    key = relative_path.to_sym
    if @found.has_key?(key)
      return @found[key]
    end
    catch :found do
      @search_paths.each do |dir|
        if File.extname(relative_path) != ''
          filename = File.join(dir, relative_path)
          next unless FileTest.exists?(filename)
          @found[key] = filename
          break
        else
          EXTENSIONS.each do |ext|
            filename = File.join(dir, relative_path + ext)
            next unless FileTest.exists?(filename)
            @found[key] = filename
            throw :found
          end
        end
      end
    end
    # noinspection RubyResolve
    raise Errno::ENOENT, relative_path unless @found.has_key?(key)
    @found[key]
  end

  ##
  # @deprecated Use {Audio.play}. Backwards compatibility for existing Audio.
  # Starts BGM playback. Sets the file name, volume, and pitch in turn.
  #
  # Also automatically searches files included in RGSS-RTP.
  # @param filename [String] Relative filename to play. File extensions may be
  #   omitted.
  # @param volume [Integer] Volume level. By default this within range of +0+
  #   to +100+, but FMOD supports amplification and higher values.
  # @param pitch [Integer] Pitch level. +50+ to +200+.
  # @return [void]
  def self.bgm_play(filename, volume = 100, pitch = 100)
    play(:BGM, filename, volume, pitch, true)
  end

  ##
  # @deprecated Use {Audio.play}. Backwards compatibility for existing Audio.
  # Starts BGS playback. Sets the file name, volume, and pitch in turn.
  #
  # Also automatically searches files included in RGSS-RTP.
  # @param filename [String] Relative filename to play. File extensions may be
  #   omitted.
  # @param volume [Integer] Volume level. By default this within range of +0+
  #   to +100+, but FMOD supports amplification and higher values.
  # @param pitch [Integer] Pitch level. +50+ to +200+.
  # @return [void]
  def self.bgs_play(filename, volume = 100, pitch = 100)
    play(:BGS, filename, volume, pitch, true)
  end

  ##
  # @deprecated Use {Audio.play}. Backwards compatibility for existing Audio.
  # Starts ME playback. Sets the file name, volume, and pitch in turn.
  #
  # Also automatically searches files included in RGSS-RTP.
  # @param filename [String] Relative filename to play. File extensions may be
  #   omitted.
  # @param volume [Integer] Volume level. By default this within range of +0+
  #   to +100+, but FMOD supports amplification and higher values.
  # @param pitch [Integer] Pitch level. +50+ to +200+.
  # @return [void]
  def self.me_play(filename, volume = 100, pitch = 100)
    play(:ME, filename, volume, pitch, false)
  end

  ##
  # @deprecated Use {Audio.play}. Backwards compatibility for existing Audio.
  # Starts SE playback. Sets the file name, volume, and pitch in turn.
  #
  # Also automatically searches files included in RGSS-RTP.
  # @param filename [String] Relative filename to play. File extensions may be
  #   omitted.
  # @param volume [Integer] Volume level. By default this within range of +0+
  #   to +100+, but FMOD supports amplification and higher values.
  # @param pitch [Integer] Pitch level. +50+ to +200+.
  # @return [void]
  def self.se_play(filename, volume = 100, pitch = 100)
    play(:SE, filename, volume, pitch, false)
  end

  ##
  # @deprecated Use {Audio.stop}. Backwards compatibility for existing Audio.
  # Stops BGM playback.
  def self.bgm_stop
    stop(:BGM)
  end

  ##
  # @deprecated Use {Audio.stop}. Backwards compatibility for existing Audio.
  # Stops BGS playback.
  def self.bgs_stop
    stop(:BGS)
  end

  ##
  # @deprecated Use {Audio.stop}. Backwards compatibility for existing Audio.
  # Stops ME playback.
  def self.me_stop
    stop(:ME)
  end

  ##
  # @deprecated Use {Audio.stop}. Backwards compatibility for existing Audio.
  # Stops SE playback.
  def self.se_stop
    stop(:SE)
  end

  ##
  # @deprecated Use {Audio.fade}. Backwards compatibility for existing Audio.
  # Transitions the BGM volume (up or down) to the target volume over the
  # specified interval.
  # @param time [Integer] The time in milliseconds for the transition to be
  #   applied. The volume will be adjusted on a per-frame basis.
  # @return [void]
  def self.bgm_fade(time, target_volume = 0)
    fade(:BGM, time, target_volume)
  end

  ##
  # @deprecated Use {Audio.fade}. Backwards compatibility for existing Audio.
  # Transitions the BGS volume (up or down) to the target volume over the
  # specified interval.
  # @param time [Integer] The time in milliseconds for the transition to be
  #   applied. The volume will be adjusted on a per-frame basis.
  # @return [void]
  def self.bgs_fade(time, target_volume = 0)
    fade(:BGS, time, target_volume)
  end

  ##
  # @deprecated Use {Audio.fade}. Backwards compatibility for existing Audio.
  # Transitions the ME volume (up or down) to the target volume over the
  # specified interval.
  # @param time [Integer] The time in milliseconds for the transition to be
  #   applied. The volume will be adjusted on a per-frame basis.
  # @return [void]
  def self.me_fade(time, target_volume = 0)
    fade(:ME, time, target_volume)
  end

  ##
  # @deprecated Use {Audio.fade}. Backwards compatibility for existing Audio.
  # Transitions the SE volume (up or down) to the target volume over the
  # specified interval.
  # @param time [Integer] The time in milliseconds for the transition to be
  #   applied. The volume will be adjusted on a per-frame basis.
  # @return [void]
  def self.se_fade(time, target_volume = 0)
    fade(:SE, time, target_volume)
  end

  ##
  # Disposes the Audio module, releasing handles to the underlying objects.
  # @return [void]
  # @note It is strongly recommended not to alter or interfere with this call.
  # @api private
  def self.finalize
    @channels.each_value { |channel| channel.stop if channel.valid? }
    @effects.each { |dsp| dsp.release }
    @sounds.each_value { |sound| sound.release }
    @fmod.release
  end

  private_class_method :build_paths
  private_class_method :full_path
  private_class_method :finalize

  initialize
end

module Graphics

  class << self

    ##
    # Original update method for Graphics.
    # @return [void]
    alias update_audio update

    ##
    # Piggybacks the Graphics update of the Audio module. Updating is necessary
    # for fading and some other less common functions.
    # @return [void]
    def update
      Audio.update
      update_audio
    end
  end
end


module Kernel
  alias finalize_audio exit
  def exit(*args)
    begin
      Audio.send(:finalize)
    ensure
      finalize_audio(*args)
    end
  end
end








I am done scripting for RMXP. I will likely not offer support for even my own scripts anymore, but feel free to ask on the forum, there are plenty of other talented scripters that can help you.

G_G

Extremely impressive Zer0! I remember you talking about this awhile ago, cool to actually see it now. Can't wait to see KK20 be enslaved to make an RM script for it. You should definitely come back into discord every once in awhile too, its fun to catch up and we all share are programming rambles there.

Awesome job man.

ForeverZer0

I am not really pleased with the structs or enum implementation.

For structs, inheriting from Struct.new(:whatever) is a little bit of a no-no, since you are also creating an anonymous class that never gets used. I used it as a bit of shortcut to writing all the attr_accessors and initializers manually, but it is not really the best practice to do that.

As far as the Enum class goes, I had a perfect idea to implement that better, using simple symbols or integers, without the need of enclosing it within the parent enum class, but now that it is so far integrated, it is going to be a painful change to alter all of the methods that use en enum, which is a few hundred.

And still more documentation with examples. I have spent far more time documenting than even writing code, getting really sick of it, lol.
I am done scripting for RMXP. I will likely not offer support for even my own scripts anymore, but feel free to ask on the forum, there are plenty of other talented scripters that can help you.

Blizzard

<3

What MIDI synthesizer are you using?
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.

ForeverZer0

MIDI control is native to FMOD, I am not 100% sure if uses its own, or just chooses based on the platform. Considering FMOD is used on about every kind of hardware and video game console out there, I imagine it probably uses its own for consistency. The documentation is a little light on the actual internal codecs, etc. and how they work.

It does have great MIDI support built-in, though, per MIDI channel gain, sound fonts, etc.

These methods in the Sound class deal with MIDI volumes, etc.
Spoiler: ShowHide
    ##
    # Retrieves the volume of a MOD/S3M/XM/IT/MIDI music channel volume.
    # @param index [Integer] MOD/S3M/XM/IT/MIDI music sub-channel to retrieve
    #   the volume for.
    # @return [Float] The volume of the channel from +0.0+ to +1.0+.
    #   Default = +1.0+.
    def music_volume(index)
      return 0 if index < 0 || index >= music_channel_count
      volume = FMOD.ptr
      FMOD.invoke(:Sound_GetMusicChannelVolume, handle, index, volume )
      volume.unpack('f').first
    end

    ##
    # Sets the volume of a MOD/S3M/XM/IT/MIDI music channel volume.
    # @param index [Integer] MOD/S3M/XM/IT/MIDI music sub-channel to set a
    #   linear volume for.
    # @param volume [Float] Volume of the channel from +0.0+ to +1.0+.
    #   Default = +1.0+.
    # @return [void]
    def set_music_volume(index, volume)
      return if index < 0 || index >= music_channel_count
      volume = volume.to_f.clamp(0.0, 1.0).to_win32
      FMOD.invoke(:Sound_SetMusicChannelVolume, index, volume)
    end
I am done scripting for RMXP. I will likely not offer support for even my own scripts anymore, but feel free to ask on the forum, there are plenty of other talented scripters that can help you.

Blizzard

Nice. If now somebody could pull the samples out of XP, we'd finally have a worthy MIDI audio replacement. xD
Check out Daygames and our games:

King of Booze 2      King of Booze: Never Ever
Drinking Game for Android      Never have I ever for Android
Drinking Game for iOS      Never have I ever for iOS


Quote from: winkioI do not speak to bricks, either as individuals or in wall form.

Quote from: Barney StinsonWhen I get sad, I stop being sad and be awesome instead. True story.

ForeverZer0

Quote from: Blizzard on March 23, 2018, 02:39:16 pm
Nice. If now somebody could pull the samples out of XP, we'd finally have a worthy MIDI audio replacement. xD


Added a generic Audio module as an example to get started with. Even in its basic form, already far more capable than RMXP.

  • Backwards compatible

  • Tempo changing

  • Adding DSP effects

  • Frequency changing

  • Pause, seek, etc. functions



I did not yet implement checking if sound is disabled via the properties of RMXP. I can add that snippet as well soon, as it is little complicated reading the registry keys.
I am done scripting for RMXP. I will likely not offer support for even my own scripts anymore, but feel free to ask on the forum, there are plenty of other talented scripters that can help you.