[XP][VX][VXA] MCI Audio Player

Started by ForeverZer0, April 29, 2012, 05:58:53 pm

Previous topic - Next topic

ForeverZer0

April 29, 2012, 05:58:53 pm Last Edit: October 28, 2018, 06:02:15 pm by Blizzard
MCI Audio Player
Authors: ForeverZer0
Version: 1.3
Type: Custom Audio Module
Key Term: Game Utility



Introduction

This script acts a either a drop-in replacement or enhancement to the built-in Audio module. It contains a plethora of functions that the default player lacks, such as seek, pause, record, fade in/out, and many more. It works by totally bypassing the built-in audio library, and directly accessing Window's "winmm" library using Ruby's Win32API. This library is home to the Media Control Interface, or MCI, which provides functions for manipulating audio and video. This allows for much more control over sound, and the ability for RPG Maker to play additional audio formats, since all that is needed is to distribute the appropriate codecs along with your game in order to use them.


Features


  • Full seek functions, accurate to the millisecond

  • Ability to read file lengths

  • Ability to pause and resume a playing file

  • Functions for transitioning from one volume to another over a given number of frames, to both lower and higher volumes

  • Can create as many mixers as needed, which allows to play multiple sounds simultaneously, which gives the ability to play more than one BGM or BGS at a time

  • Record function to capture input from the user (.wav format only)

  • Ability to set volume to left and right speaker independently

  • Mute functions

  • Can set the speed of playback

  • Ability to set treble and bass (Not all devices support this)

  • Searches RTP files and uses them automatically if file is not local

  • Easy access for additional calls to the Media Control Interface

  • No porting external libraries with your game, all functioning is done within the script and the operating system

  • Can use any audio format you wish, so long as the appropriate codec is installed on the host computer

  • Compatibility with RMXP, RMVX, and RMVXA

  • Memorize/Restore audio on all/some channels




Screenshots

SAMPLE ONLY Incomplete Audio Player (XP): ShowHide



Demo

Demo Link (RMXP)(1.3)


Script

Spoiler: ShowHide
#=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=
# MCI Audio Player
# Author: ForeverZer0
# Version: 1.3
# Date: 7.5.2014
#=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=
#                             VERSION HISTORY
#=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=
# v.1.0   4.29.2012
#   - Original Release
#
# v.1.1   4.29.2012
#   - Fixed a typo
#   - Added instructions for ensuring all channels stop when player closes game
#   - Added RPG Maker VX compatibility
#   - Added RPG Maker VX Ace compatibility
#   - Fixed duration to ensure a minimum of 1 frame, else nothing happens
#   - Added "start position" argument to "play" call. This ensures VXA
#     functionality, as well as a minor improvement to the system
#
# v.1.2   7.8.2012
#   - Fixed incorrect filepaths that mixed forward and backward slashes
#   - Fixed a bug that would cause full paths to files not to play sometimes
#   - Added an enumerator for iterating Audio mixers ("Audio.each_mixer")
#   - Added methods to Game_System for memorizing/restoring audio
#
# v.1.3   7.5.2014
#   - Fixed bug that would cause BGM/BGS to restart from beginning on transfer
#     to new map with same BGM or BGS
#=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=
#
# Introduction:
#
#   This script acts a either a drop-in replacement or enhancement to the
#   built-in Audio module. It contains a plethora of functions that the default
#   player lacks, such as seek, pause, record, fade in/out, and many more. It
#   works by totally bypassing the built-in audio library, and directly
#   accessing Window's "winmm" library using Ruby's Win32API. This library is
#   home to the Media Control Interface, or MCI, which provides functions for
#   manipulating audio and video. This allows for much more control over sound,
#   and the ability for RPG Maker to play additional audio formats, since all
#   that is needed is to distribute the appropriate codecs along with your game
#   in order to use them.
#
# Features:
#
#   - Full seek functions, accurate to the millisecond
#   - Ability to read file lengths
#   - Ability to pause and resume a playing file
#   - Functions for transitioning from one volume to another over a given number
#     frames, to both lower and higher volumes
#   - Can create as many mixers as needed, which allows to play multiple sounds
#     simultaneously, which gives the ability to play more than one BGM or BGS
#     at a time
#   - Record function to capture input from the user (.wav format only)
#   - Ability to set volume to left and right speaker independently
#   - Mute functions
#   - Can set the speed of playback
#   - Ability to set treble and bass (Not all devices support this)
#   - Searches RTP files and uses them automatically if file is not local
#   - Easy access for additional calls to the Media Control Interface
#   - No porting external libraries with your game, all functioning is done
#     within the script and the operating system
#   - Can use any audio format you wish, so long as the appropriate codec is
#     installed on the host computer
#   - Full compatibility with RMXP, RMVX, and RMVX Ace
#   - Memorize and restore all/some channels
#
# Instructions:
#
#   - Scroll below to make the setting for what RPG Maker version you are using
#   - Place script anywhere above Main
#   - Only one setting (below) to set the MCI player as default audio player,
#     which is by default "true". If false, the MCI player will only be used for
#     special circumstances via script calls
#   - There are a whole bunch of new script calls available for the Audio module
#     There is full documentation if you look below, which I reccomend that you
#     look at if you choose to use this script, but most are self-explanatory.
#     Here is a partial list, words in caps are arguments that need replaced.

#     - play(FILENAME, VOLUME, SPEED, REPEAT)
#     - pause
#     - restart
#     - pause
#     - resume
#     - stop
#     - close
#     - volume/volume=
#     - set_volume_left(VOLUME)
#     - set_volume_right(VOLUME)
#     - mute
#     - fade(DURATION, FRAMES)
#     - speed/speed=
#     - duration
#     - position/position=
#     - seek(MILLISECOND)
#     - playing?
#     - paused?
#     - recording?
#     - treble/treble=
#     - bass/bass=
#     - status
#     - record(BITS_PER_SAMPLE, SAMPLERATE, CHANNELS)
#     - save(FILENAME)
#     
#     To play a sound on a mixer is pretty straightforward, you don't even need
#     to create a mixer first, that is done automatically. All you need to do
#     is use a unique name for each mixer you want to control, and use the
#     above methods on it. For example, to play a file:
#
#     Audio['myName'].play('myFile.mp3')
#
#     From here on, you can access the same mixer using 'myName' as the key to
#     perform further actions. Such as...
#
#     Audio['myName'].pause            - Pauses playback
#     Audio['myName'].resume           - Resumes playback
#     Audio['myName'].volume = 50      - Sets volume to fifty
#     Audio['myName'].fade(80, 0)      - Fades volume to 0 in 80 frames
#     Audio['myName'].fade(240, 100)   - Transitions volume to 100 in 6 seconds
#     Audio['myName'].record           - Begins recording
#     Audio['myName'].save('file.wav') - Saves recorded file
#     Audio['myName'].seek(4500)       - Sets playback position to 4.5 seconds
#
#     As you can see, all mixers are accessed in a hash-like way via the Audio
#     module. By default, 'BGM', 'BGS', 'ME', and 'SE' are mixers used as
#     replacements for the built-in audio library, so use names other them if
#     creating additional mixers.
#
#     There is also a script call to make a call using mciSendString if you are
#     familiar with the library at all. I simplified it down a bit, but it can
#     be used with the following snippet:
#
#         Audio.mci_eval(MCI_COMMAND)
#
#     For more information about using MCI commands, please see the full
#     documentation at MSDN.
#
# http://msdn.microsoft.com/en-us/library/windows/desktop/dd743572(v=vs.85).aspx
#
#     There are also a few methods available via the Game_System class for
#     memorizing/restoring audio at its current position.
#
#     ex.
#
#     $game_system.memorize_audio               - Memorize all channels
#     $game_system.memorize_audio('BGM')        - Memorize BGM channel
#     $game_system.memorize_audio('BGM', 'BGS') - Memorize BGM and BGS channels
#     $game_system.restore_audio                - Restore all channels
#     $game_system.restore_audio('BGM')         - Restore BGM channel
#     $game_system.restore_audio('BGM', 'BGS')  - Restore BGM and BGS channels
#
#     You can use as many arguments for each method as you wish. Omitting
#     arguments will simply have it memorize/restore all channels.
#
# Compatibility:
#
#   - Not tested on Linux running under Wine. If anyone tests this, let me know.
#   - Not compatible with DREAM.
#
# Credits/Thanks:
#
#   - ForeverZer0, for the script
#
# Authors Notes:
#
#   - Changing pitch will be different. MCI does not have a function for this,
#     so I am changing the speed as a generic substitute. Changing the speed
#     does change the pitch, but it true sound pitch alters the sampling rate
#     as well, which this player does not. You have a couple alternatives if
#     you absolutely need the pitch change:
#
#     1. Edit the files using an external editor and import it
#     2. Use the default system to play such sounds using the alias names.
#
#=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=

#===============================================================================
# ** Audio
#===============================================================================

module Audio

  # Set true/false if the MCI Player should be the default audio controller. It
  # will maintain all the same functionality as the standard control, and all
  # normal calls to the Audio module will work as normal, but it will also
  # extend the its functionality. If false, RMXP's standard library will be used
  # and this player will only be used for custom mixers.
  MCI_DEFAULT = true
 
  # Due to vast differences in how the format is handled, some of this player's
  # features do not work with MIDI, most notably volume control, which also
  # effects fading. I have created alternate controls to control MIDI volume,
  # but they can only work with the sacrifice of many other functions, and the
  # volume applies to ALL playing MIDIs, not just the one the volume is applied
  # to. I decided this was not worth it, so I omitted volume control. If you
  # are willing to make them sacrifices, you can enable this mode by setting
  # MIDI_MODE to true. If you your game relies heavily on MIDI, but you still
  # would like to use this script, there are conversion programs available,
  # which I can assist you with if need be.
  MIDI_MODE = false
 
  # Enter the code for the version of RPG Maker you are using this script for.
  #   RMXP  = 0
  #   RMVX  = 1
  #   RMVXA = 2
  RPG_VERSION = 0
 
#===============================================================================
# ** Mixer
#-------------------------------------------------------------------------------
# This class acts a wrapper for Window's Media Control Interface (MCI).
#===============================================================================

  MCISendString = Win32API.new('winmm', 'mciSendString', 'PPLL', 'L')
  MIDIOutSetVolume = Win32API.new('winmm', 'midiOutSetVolume', 'LL', 'L')

  class Mixer
   
    attr_reader :name           # The arbitrary name of the mixer
    attr_reader :filepath       # The path of the currently loaded file
    attr_reader :repeat         # Flag if playback is set to repeat
    attr_reader :filename       # Name of the file being played
   
    #---------------------------------------------------------------------------
    # * Object Initialization
    #     name    : The unique name of the mixer
    #---------------------------------------------------------------------------
    def initialize(name)
      @name = name
      @fade_duration = 0
      @fade_volume = 0
      @opened = false
      @repeat = false
      @midi = false
    end
    #---------------------------------------------------------------------------
    # * Load a file and prepare for playback
    #     filename  : The path to the file.
    #---------------------------------------------------------------------------
    def open(filename)
      if File.exists?(filename)
        path = filename
      else
        path = Dir::glob(filename + '.*')[0]
        if path == nil
          directory = File.dirname(filename)
          if RTP.subfolder?(directory)
            file = File.basename(filename)
            path = RTP[directory].find {|f| File.basename(f, '.*') == file }
          end
        end
      end
      @filepath = path.gsub(/\\/, '/')
      if MIDI_MODE && ['.mid', '.midi'].include?(File.extname(path))
        @midi = true
        cmd = "open \"#{path}\" type sequencer alias #{@name}"
      else
        cmd = "open \"#{path}\" type mpegvideo alias #{@name}"
      end
      MCISendString.call(cmd, nil, 0, 0)
      MCISendString.call("set #{@name} time format milliseconds", nil, 0, 0)
      MCISendString.call("set #{@name} seek exactly on", nil, 0, 0)
      @opened = true
    end
    #---------------------------------------------------------------------------
    # * Began playback on the mixer
    #     filename  : The name of the file to play
    #     volume    : The volume to play the file at
    #     speed     : The speed to play the the fule at
    #     repeat    : Flag if playback should loop after done playing
    #     start     : The position, in milliseconds, to begin playback at
    #---------------------------------------------------------------------------
    def play(filename, volume = 100, speed = 100, repeat = false, start = 0)
      @filename = filename
      self.close
      self.open(filename)
      self.speed = speed
      if MIDI_MODE && @midi
        @midi_master = @midi_right = @midi_left = volume * 10
        self.set_volume(volume)
        MCISendString.call("play #{@name} from 0", nil, 0, 0)
        return
      end
      self.set_volume(volume)
      @repeat = repeat
      if start != 0
        MCISendString.call("seek #{@name} to #{start}", nil, 0, 0)
      else
        MCISendString.call("seek #{@name} to start", nil, 0, 0)
      end
      MCISendString.call("play #{@name}#{repeat ? ' repeat' : ''}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Restarts playback of the currently loaded file from the beginning
    #---------------------------------------------------------------------------
    def restart
      MCISendString.call("seek #{@name} to start", nil, 0, 0)
      MCISendString.call("play #{@name}#{repeat ? ' repeat' : ''}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Pause playback and maintain current position
    #---------------------------------------------------------------------------
    def pause
      MCISendString.call("pause #{@name}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Resume playback on a paused mixer from previous position
    #---------------------------------------------------------------------------
    def resume
      MCISendString.call("resume #{@name}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Stops playback, setting position back to the start
    #---------------------------------------------------------------------------
    def stop
      MCISendString.call("seek #{@name} to start", nil, 0, 0)
      MCISendString.call("stop #{@name}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Closes the opened file and frees it from memory
    #---------------------------------------------------------------------------
    def close
      MCISendString.call("close #{@name}", nil, 0, 0)
      @filepath = nil
      @opened = false
    end
    #---------------------------------------------------------------------------
    # * Gets the actual volume of the mixer, an integer from 0 to 1000
    #---------------------------------------------------------------------------
    def real_volume
      if MIDI_MODE && @midi
        return @midi_master
      else
        data = "\0" * 128
        MCISendString.call("status #{@name} volume", data, 128, 0)
        return data.delete("\0").to_i
      end
    end
    #---------------------------------------------------------------------------
    # * Sets the actual volume of the mixer
    #     value    : The volume level, integer between 0 and 1000
    #---------------------------------------------------------------------------
    def real_volume=(value)
      self.set_real_volume(value)
    end
    #---------------------------------------------------------------------------
    # * Sets the actual volume of the mixer
    #     value    : The volume level, integer between 0 and 1000
    #---------------------------------------------------------------------------
    def set_real_volume(value)
      vol = [0, [value, 1000].min].max
      if MIDI_MODE && @midi
        @midi_master = value
        self.set_midi_volume(value, value)
      else
        MCISendString.call("setaudio #{@name} volume to #{vol}", nil, 0, 0)
      end
    end
    #---------------------------------------------------------------------------
    # * Returns the volume of the mixer
    #---------------------------------------------------------------------------
    def volume
      return self.real_volume / 10   
    end
    #---------------------------------------------------------------------------
    # * Sets the volume of the mixer
    #     value    : The volume level, integer between 0 and 100
    #---------------------------------------------------------------------------
    def volume=(value)
      value = [0, [value, 100].min].max
      self.set_real_volume(value * 10)
    end
    #---------------------------------------------------------------------------
    # * Sets the volume of the mixer
    #     value    : The volume level, integer between 0 and 100
    #---------------------------------------------------------------------------
    def set_volume(value)
      value = [0, [value, 100].min].max
      self.set_real_volume(value * 10)
    end
    #---------------------------------------------------------------------------
    # * Set the volume of the mixer in the left speaker only
    #     value   : Volume level, integer between 0 and 100
    #---------------------------------------------------------------------------
    def set_volume_left(value)
      vol = [0, [value * 10, 1000].min].max
      if MIDI_MODE && @midi
        self.set_midi_volume(value, @midi_right)
      else
        MCISendString.call("setaudio #{@name} left volume to #{vol}", nil, 0, 0)
      end
    end
    #---------------------------------------------------------------------------
    # * Set the volume of the mixer in the right speaker only
    #     value   : Volume level, integer between 0 and 100
    #---------------------------------------------------------------------------
    def set_volume_right(value)
      vol = [0, [value * 10, 1000].min].max
      if MIDI_MODE && @midi
        self.set_midi_volume(@midi_left, value)
      else
        MCISendString.call("setaudio #{@name} right volume to #{vol}", nil, 0, 0)
      end
    end
    #---------------------------------------------------------------------------
    # * Special handling for adjusting MIDI volume. MCI cannot handle the volume
    #   for this format directly, so we need to precalculate the channels and
    #   make the call to the MIDI synthesizer ourselves.
    #     left    : The volume of the left channel
    #     right   : The volume of the right channel
    #
    #   NOTE:
    #   It is recommended that you do not call this method directly.
    #---------------------------------------------------------------------------
    def set_midi_volume(left, right)
      @midi_left, @midi_right = left, right
      left = (0xFFFF * (left / 1000.0)).round
      right = (0xFFFF * (right / 1000.0)).round
      vol = right.to_s(16) + left.to_s(16)
      MIDIOutSetVolume.call(0, vol.to_i(16))
    end
    #---------------------------------------------------------------------------
    # * Mutes sound from the mixer
    #     bool    : True/false flag to mute/unmute sound
    #---------------------------------------------------------------------------
    def mute(bool)
      MCISendString.call("setaudio #{@name} #{bool ? 'on' : 'off'}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Transition volume to another volume
    #     duration    : The number of frames the transition will take
    #     target      : The target volume to transition to
    #---------------------------------------------------------------------------
    def fade(duration, target = 0)
      @fade_volume = target * 10
      @fade_duration = [duration, 1].max
    end
    #---------------------------------------------------------------------------
    # * Returns the current speed of playback  (100 = normal)
    #---------------------------------------------------------------------------
    def speed
      data = "\0" * 256
      MCISendString.call("status #{@name} speed", data, 256, 0)
      data.delete!("\0")
      return data.to_i / 10
    end
    #---------------------------------------------------------------------------
    # * Set the current speed of playback
    #     value   : The rate of playback to set
    #---------------------------------------------------------------------------
    def speed=(value)
      set_speed(value)
    end
    #---------------------------------------------------------------------------
    # * Set the current speed of playback
    #     value   : The rate of playback to set
    #---------------------------------------------------------------------------
    def set_speed(value)
      value = [0, [2000, value * 10].min].max
      MCISendString.call("set #{@name} speed #{value}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Gets the length of the loaded file in milliseconds
    #---------------------------------------------------------------------------
    def duration
      if self.playing?
        length = "\0" * 256
        MCISendString.call("status #{@name} length", length, 256, 0)
        length.delete!("\0")
        return length.to_i
      end
      return 0
    end
    #---------------------------------------------------------------------------
    # * Returns duration as a string in normal MM:SS format
    #---------------------------------------------------------------------------
    def duration_string
      seconds = self.duration / 1000
      return sprintf('%2d:%02d', seconds / 60, seconds % 60)
    end
    #---------------------------------------------------------------------------
    # * Returns the current position of playback, in milliseconds
    #---------------------------------------------------------------------------
    def position
      pos = "\0" * 256
      MCISendString.call("status #{@name} position", pos, 256, 0)
      pos.delete!("\0")
      return pos.to_i
    end
    #---------------------------------------------------------------------------
    # * Returns current position as a string in normal MM:SS format
    #---------------------------------------------------------------------------
    def position_string
      seconds = self.position / 1000
      return sprintf('%2d:%02d', seconds / 60, seconds % 60)
    end
    #---------------------------------------------------------------------------
    # * Sets the current playback position
    #     value   : The time in milliseconds to set current playback
    #---------------------------------------------------------------------------
    def position=(value)
      self.seek(value)
    end
    #---------------------------------------------------------------------------
    # * Sets the current playback position
    #     value   : The time in milliseconds to set current playback
    #---------------------------------------------------------------------------
    def seek(value)
      cmd = "#{self.playing? ? 'play' : 'seek'} #{@name} from #{value}"
      MCISendString.call(cmd, nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Returns tha "playing" status of the mixer
    #---------------------------------------------------------------------------
    def playing?
      return self.status == 'playing'
    end
    #---------------------------------------------------------------------------
    # * Returns tha "paused" status of the mixer
    #---------------------------------------------------------------------------
    def paused?
      return self.status == 'paused'
    end
    #---------------------------------------------------------------------------
    # * Returns true/false if file is currently loaded for playback
    #---------------------------------------------------------------------------
    def opened?
      return @opened
    end
    #---------------------------------------------------------------------------
    # * Returns true/false if mixer is currently recording
    #---------------------------------------------------------------------------
    def recording?
      return self.status == 'recording'
    end
    #---------------------------------------------------------------------------
    # * Returns the mixer's bass value
    #---------------------------------------------------------------------------
    def treble
      data = "\0" * 128
      MCISendString.call("status #{@name} treble", data, 128, 0)
      data.delete!("\0")
      return data.to_i
    end
    #---------------------------------------------------------------------------
    # * Set mixer treble
    #     value   : Treble value, integer between 0 and 1000
    #---------------------------------------------------------------------------
    def treble=(value)
      set_treble(value)
    end
    #---------------------------------------------------------------------------
    # * Set mixer treble
    #     value   : Treble value, integer between 0 and 1000
    #---------------------------------------------------------------------------
    def set_treble(value)
      value = [0, [value, 1000].min].max
      MCISendString.call("setaudio #{@name} treble to #{value}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Returns the mixer's bass value
    #---------------------------------------------------------------------------
    def bass
      data = "\0" * 128
      MCISendString.call("status #{@name} bass", data, 128, 0)
      data.delete!("\0")
      return data.to_i
    end
    #---------------------------------------------------------------------------
    # * Set mixer bass
    #     value   : Bass value, integer between 0 and 1000
    #---------------------------------------------------------------------------
    def bass=(value)
      set_bass(value)
    end
    #---------------------------------------------------------------------------
    # * Set mixer bass
    #     value   : Bass value, integer between 0 and 1000
    #---------------------------------------------------------------------------
    def set_bass(value)
      value = [0, [value, 1000].min].max
      MCISendString.call("setaudio #{@name} bass to #{value}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Gets the current status
    #---------------------------------------------------------------------------
    def status
      data = "\0" * 256
      MCISendString.call("status #{@name} mode", data, 256, 0)
      return data.delete("\0")
    end
    #---------------------------------------------------------------------------
    # * Begins recording from the input, typically the PC's microphone
    #     bits_ps     : Bits per sample the file will be recorded at
    #     sample_rate : Sample rate the the file will be recorded at
    #     channels    : Number of channels that will be opened for recording
    #
    #   * WARNING *
    #     Make sure that "stop", "close" or "save" is performed on this mixer
    #     within a reasonable of amount of time. While the mixer is recording,
    #     the file is held in RAM, which will become very large if left without
    #     closing it, and eventually slow down the PC and/or crash the game.
    #     Basically I'm just saying "don't forget you are recording"
    #---------------------------------------------------------------------------
    def record(bits_ps = 16, sample_rate = 44100, channels = 2)
      self.close
      MCISendString.call("open new type waveaudio alias #{@name}", nil, 0, 0)
      MCISendString.call("set #{@name} bitspersample #{bits_ps}", nil, 0, 0)
      MCISendString.call("set #{@name} samplespersec #{sample_rate}", nil, 0, 0)
      MCISendString.call("set #{@name} channels #{channels}", nil, 0, 0)
      MCISendString.call("record #{@name}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Saves a recording into WAV format
    #     filename  : The path of the file t save, must have '.wav' extension
    #---------------------------------------------------------------------------
    def save(filename)
      if self.recording?
        MCISendString.call("stop #{@name}", nil, 0, 0)
      end
      if File.extname(filename) != '.wav'
        filename += '.wav'
      end
      MCISendString.call("save #{@name} #{filename}", nil, 0, 0)
      MCISendString.call("close #{@name}", nil, 0, 0)
    end
    #---------------------------------------------------------------------------
    # * Frame update
    #---------------------------------------------------------------------------
    def update
      if @fade_duration >= 1
        d = @fade_duration
        self.set_real_volume((self.real_volume * (d - 1) + @fade_volume) / d)
        @fade_duration -= 1
      end
    end
  end

#===============================================================================
# ** Audio
#-------------------------------------------------------------------------------
# The metaclass of the Audio module. This class is a wrapper between the default
# audio controls and the MCI Player controls.
#===============================================================================
 
  class << self
    #---------------------------------------------------------------------------
    # * Use MCI Player play function
    #---------------------------------------------------------------------------
    alias mci_bgm_play bgm_play
    def bgm_play(filename, volume = 100, pitch = 100, start = 0)
      if MCI_DEFAULT
        mixer_play('BGM', filename, volume, pitch, true, start)
      elsif RPG_VERSION == 2
        mci_bgm_play(filename, volume, pitch, start)
      else
        mci_bgm_play(filename, volume, pitch)
      end
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player play function
    #---------------------------------------------------------------------------
    alias mci_bgs_play bgs_play
    def bgs_play(filename, volume = 100, pitch = 100, start = 0)
      if MCI_DEFAULT
        mixer_play('BGS', filename, volume, pitch, true, start)
      elsif RPG_VERSION == 2
        mci_bgs_play(filename, volume, pitch, start)
      else
        mci_bgs_play(filename, volume, pitch)
      end
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player play function
    #---------------------------------------------------------------------------
    alias mci_me_play me_play
    def me_play(filename, volume = 100, pitch = 100, start = 0)
      if MCI_DEFAULT
        mixer_play('ME', filename, volume, pitch, false, start)
      elsif RPG_VERSION == 2
        mci_me_play(filename, volume, pitch, start)
      else
        mci_me_play(filename, volume, pitch)
      end
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player play function
    #---------------------------------------------------------------------------
    alias mci_se_play se_play
    def se_play(filename, volume = 100, pitch = 100, start = 0)
      if MCI_DEFAULT
        mixer_play('SE', filename, volume, pitch, false, start)
      elsif RPG_VERSION == 2
        mci_se_play(filename, volume, pitch, start)
      else
        mci_se_play(filename, volume, pitch, start)
      end
    end
    #---------------------------------------------------------------------------
    # Use MCI Player to play a file on a mixer using given parameters
    #---------------------------------------------------------------------------
    def mixer_play(mixer_name, filename, volume, pitch, repeat, start = 0)
      if ['BGM', 'BGS'].include?(mixer_name)
        return if self[mixer_name].filename == filename
      end
      self[mixer_name].play(filename, volume, pitch, repeat, start)
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player stop
    #---------------------------------------------------------------------------
    alias mci_bgm_stop bgm_stop
    def bgm_stop
      MCI_DEFAULT ? @mixers['BGM'].stop : mci_bgm_stop
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player stop
    #---------------------------------------------------------------------------
    alias mci_bgs_stop bgs_stop
    def bgs_stop
      MCI_DEFAULT ? @mixers['BGS'].stop : mci_bgs_stop
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player stop
    #---------------------------------------------------------------------------
    alias mci_me_stop me_stop
    def me_stop
      MCI_DEFAULT ? @mixers['ME'].stop : mci_me_stop
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player stop
    #---------------------------------------------------------------------------
    alias mci_se_stop se_stop
    def se_stop
      MCI_DEFAULT ? @mixers['SE'].stop : mci_se_stop
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player fade
    #---------------------------------------------------------------------------
    alias mci_bgm_fade bgm_fade
    def bgm_fade(time)
      rate = RPG_VERSION == 0 ? 40 : 60
      MCI_DEFAULT ? @mixers['BGM'].fade((time / 1000) * rate) :
        mci_bgm_fade(time)
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player fade
    #---------------------------------------------------------------------------
    alias mci_bgs_fade bgs_fade
    def bgs_fade(time)
      rate = RPG_VERSION == 0 ? 40 : 60
      MCI_DEFAULT ? @mixers['BGS'].fade((time / 1000) * rate) :
        mci_bgs_fade(time)
    end
    #---------------------------------------------------------------------------
    # * Use MCI Player fade
    #---------------------------------------------------------------------------
    alias mci_me_fade me_fade
    def me_fade(time)
      rate = RPG_VERSION == 0 ? 40 : 60
      MCI_DEFAULT ? @mixers['ME'].fade((time / 1000) * rate) :
        mci_me_fade(time)
    end
  end
  #-----------------------------------------------------------------------------
  # * Gives hash-type access of the mixers of the module
  #-----------------------------------------------------------------------------
  def self.[](mixer_name)
    unless @mixers.has_key?(mixer_name)
      @mixers[mixer_name] = Mixer.new(mixer_name)
    end
    return @mixers[mixer_name]
  end
  #-----------------------------------------------------------------------------
  # * Frame update
  #-----------------------------------------------------------------------------
  def self.update
    @mixers.each_value {|mixer| mixer.update }
  end
  #-----------------------------------------------------------------------------
  # * Simplified method for making calls directly to the Media Command Interface
  #-----------------------------------------------------------------------------
  def self.mci_eval(command)
    data = "\0" * 256
    MCISendString.call(command, data, 256, 0)
    return data.delete("\0")
  end
  #-----------------------------------------------------------------------------
  # * Iterator for the Audio mixers
  #-----------------------------------------------------------------------------
  def self.each_mixer
    @mixers.each_value {|mixer| yield mixer }
  end
  #-----------------------------------------------------------------------------
  # * Object initialization
  #-----------------------------------------------------------------------------
  def self.init
    if @mixers != nil
      # Don't remove this, it prevents memory leaks when F12 us used to restart
      MCISendString.call('close all', nil, 0, 0)
    end
    @mixers = {}
    ['BGM', 'BGS', 'ME', 'SE'].each {|name| @mixers[name] = Mixer.new(name) }
  end 
end

#===============================================================================
# ** Graphics
#-------------------------------------------------------------------------------
# Syncs the audio update used for fade effects with the frame update
#===============================================================================
module Graphics
 
  class << self
   
    alias mci_player_update update
    def update
      mci_player_update
      Audio.update
    end
  end
end

#===============================================================================
# ** RTP
#-------------------------------------------------------------------------------
# Provides functions for getting the games RTP path(s) and files
#===============================================================================

module RTP
 
  # RMXP
  if Audio::RPG_VERSION == 0
    SUBFOLDERS = [
      'Graphics/Animations', 'Graphics/Autotiles', 'Graphics/Battlebacks',
      'Graphics/Battlers', 'Graphics/Characters', 'Graphics/Fogs',
      'Graphics/Gameovers', 'Graphics/Icons', 'Graphics/Panoramas',
      'Graphics/Pictures', 'Graphics/Tilesets', 'Graphics/Titles',
      'Graphics/Transitions', 'Graphics/Windowskins', 'Audio/BGM',
      'Audio/BGS', 'Audio/ME', 'Audio/SE'
    ]
  # RMVX
  elsif Audio::RPG_VERSION == 1
    SUBFOLDERS = [
      'Graphics/Animations', 'Graphics/Battlers', 'Graphics/Characters',
      'Graphics/Faces', 'Graphics/Parallaxes', 'Graphics/Pictures',
      'Graphics/System', 'Audio/BGM', 'Audio/BGS', 'Audio/ME', 'Audio/SE'
    ]
  # RMVXA
  elsif Audio::RPG_VERSION == 2
    SUBFOLDERS = [
      'Graphics/Animations', 'Graphics/Battlers', 'Graphics/Characters',
      'Graphics/Faces', 'Graphics/Parallaxes', 'Graphics/Pictures',
      'Graphics/System', 'Audio/BGM', 'Audio/BGS', 'Audio/ME', 'Audio/SE'
    ]
  end
  #-----------------------------------------------------------------------------
  # * Object initialization
  #-----------------------------------------------------------------------------
  def self.init
    @ini = Win32API.new('kernel32', 'GetPrivateProfileStringA', 'PPPPLP', 'L')
    @library = "\0" * 256
    @ini.call('Game', 'Library', '', @library, 256, '.\\Game.ini')
    @library.delete!("\0")
    @rtp_path = Win32API.new(@library, 'RGSSGetRTPPath', 'L', 'L')
    @path_with_rtp = Win32API.new(@library, 'RGSSGetPathWithRTP', 'L', 'P')
    @directories = {}
    SUBFOLDERS.each {|folder| @directories[folder] = entries(folder) }
    @initialized = true
  end
  #-----------------------------------------------------------------------------
  # * Returns an array of the full paths of all the game's installed RTPs
  #-----------------------------------------------------------------------------
  def self.paths
    paths = [1, 2, 3].collect {|id| @path_with_rtp.call(@rtp_path.call(id)) }
    paths = paths.find_all {|path| path != '' }
    # This is kind of a crappy way of doing this until the RMVX call works...
    common = File.join(ENV['CommonProgramFiles'], 'Enterbrain')
    common = case Audio::RPG_VERSION
    when 0 then File.join(common, 'RGSS', 'Standard')
    when 1 then File.join(common, 'RGSS2', 'RPGVX')
    when 2 then File.join(common, 'RGSS3', 'RPGVXAce')
    end
    if !paths.include?(common) && File.directory?(common)
      paths.push(common)
    end
    return paths
  end
  #-----------------------------------------------------------------------------
  # * Gives hash-like access to the RTP subfolders
  #-----------------------------------------------------------------------------
  def self.[](folder)
    return subfolder?(folder) ? @directories[folder] : []
  end
  #-----------------------------------------------------------------------------
  # * Returns true/false if the given subfolder exists
  #-----------------------------------------------------------------------------
  def self.subfolder?(folder)
    return @directories.has_key?(folder)
  end
  #-----------------------------------------------------------------------------
  # * Get a complete list of full paths of files found in the given subfolder
  #   subfolder   : The RTP folder whose files you want to get
  #-----------------------------------------------------------------------------
  def self.entries(subfolder)
    files = []
    paths.each {|path|
      dir = path + '\\' + subfolder
      if File.directory?(dir)
        files = (Dir.entries(dir) - ['.', '..']).collect {|f| dir + '\\' + f }
      end
    }
    return files
  end
end

#===============================================================================
# ** Game_System
#===============================================================================

class Game_System
  #-----------------------------------------------------------------------------
  # * Public Instance Variables
  #-----------------------------------------------------------------------------
  attr_accessor :memorized_audio
  #-----------------------------------------------------------------------------
  # * Memorize Audio
  #      channels : Names of channels, or omit argument to memorize all channels
  #-----------------------------------------------------------------------------
  def memorize_audio(*channels)
    @audio_memory = {}
    if channels.empty?
      Audio.each_mixer {|m|
        data = [m.filepath, m.volume, m.speed, m.repeat, m.position]
        @audio_memory[m.name] = data
      }
    else
      channels.each {|channel|
        m = Audio[channel]
        data = [m.filepath, m.volume, m.speed, m.repeat, m.position]
        @audio_memory[channel] = data
      }
    end
  end
  #-----------------------------------------------------------------------------
  # * Restore Audio
  #      channels : Names of channels, or omit argument to restore all channels
  #-----------------------------------------------------------------------------
  def restore_audio(*channels)
    unless @audio_memory == nil || @audio_memory.empty?
      keys = channels.empty? ? @audio_memory.keys : channels
      keys.each {|name|
        data = @audio_memory[name]
        if data != nil && data[0] != nil
          Audio[name].play(data[0], data[1], data[2], data[3])
          Audio[name].seek(data[4])
          @audio_memory.delete(name)
        end
      }
    end
  end
end

# Intialize the MCI Player
RTP.init
Audio.init


by Blizzard: If you have issues with SE channel, here's an additional snippet that won't stop playing SEs when new SEs are started. Put it before the line that says RPG.init

Spoiler: ShowHide

module Audio

  class << self
   
    @@counter = 0
   
    def mixer_play(mixer_name, filename, volume, pitch, repeat, start = 0)
      if ['BGM', 'BGS'].include?(mixer_name)
        return if self[mixer_name].filename == filename
      end
      if mixer_name != 'SE'
        self[mixer_name].play(filename, volume, pitch, repeat, start)
      else
        @@counter += 1
        mixer = Mixer.new("#{mixer_name}_#{@@counter}")
        mixers = self[mixer_name]
        mixers.push(mixer)
        mixer.play(filename, volume, pitch, repeat, start)
      end
    end
   
    def se_stop
      MCI_DEFAULT ? @mixers['SE'].each {|mixer| mixer.stop} : mci_se_stop
    end
   
  end
 
  def self.[](mixer_name)
    unless @mixers.has_key?(mixer_name)
      if mixer_name != 'SE'
        @mixers[mixer_name] = Mixer.new(mixer_name)
      else
        @mixers[mixer_name] = []
      end
    end
    return @mixers[mixer_name]
  end
 
  def self.update
    self.each_mixer {|mixer| mixer.update }
    @mixers.each {|key, mixer|
      if key != 'SE'
        mixer.update
      else
        removed_mixers = []
        mixer.each {|se_mixer|
          se_mixer.update
          removed_mixers.push(se_mixer) if !se_mixer.playing?
        }
        removed_mixers.each {|se_mixer|
          se_mixer.close
          mixer.delete(se_mixer)
        }
      end
    }
  end
 
  def self.each_mixer
    @mixers.each {|key, mixer|
      if key != 'SE'
        yield mixer
      else
        mixer.each {|se_mixer| yield se_mixer }
      end
    }
  end

  def self.init
    if @mixers != nil
      # Don't remove this, it prevents memory leaks when F12 us used to restart
      MCISendString.call('close all', nil, 0, 0)
    end
    @mixers = {}
    ['BGM', 'BGS', 'ME'].each {|name| @mixers[name] = Mixer.new(name) }
    @mixers['SE'] = []
  end 
 
end



Instructions

Simply set the code in the configuration to what version of RPG Maker you are using.
See script for detailed instructions on scrip calls available to you, and demo for examples of a few.

On some systems, audio can continue to play after the RGSS player has closed. To prevent this, add the following line to the botton of "Main", after all the code:

Audio.mci_eval('close all')




Compatibility


  • Not tested on Linux running under Wine. I don't have a VM installed at the present time, so if anyone tests this, let me know.

  • High chance of incompatibility with programs that encrypt audio files

  • Due to vast differences in how the format is handled, some of this player's features do not work with MIDI, most notably volume control, which also effects fading. I have created alternate controls to control MIDI volume, but they can only work with the sacrifice of many other functions, and the volume applies to ALL playing MIDIs, not just the one the volume is applied to. I decided this was not worth it, so I omitted volume control of MIDI by default, but if you are willing to make these sacrifices, there is a setting to enable it. If you your game relies heavily on MIDI, but you still would like to use this script, there are conversion programs available, which I can assist you with if need be.




Credits and Thanks


  • ForeverZer0, for the script

  • Blizzard, for the additional SE patch




Author's Notes

If you are looking for a good codec pack, I recommend K-Lite Codec Pack. This is a single package that will cover pretty much every standard audio format available, and even a few exotics (especially with the Mega-Pack).

Changing pitch will be different. MCI does not have a function for this, so I am changing the speed as a generic substitute. Changing the speed does change the pitch, but it true sound pitch alters the sampling rate as well, which this player does not. You have a couple alternatives if you absolutely need the pitch change:


  • Edit the files using an external editor and import it

  • Use the default system to play such sounds using the alias names



For more information about using MCI commands, please see the full documentation at MSDN.

Most audio formats should play by default on most systems, but most notably is a lack of an OGG codec that will be used by default.
If your OGG files fail to play, download and install the codec found here.

OpenCodecs (2.53 MB)(x86/x64)
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.

Calintz

well this looks wonderful, +1.

nathmatt

the music u set to play dose not stop after you close the rgss player
Join Dead Frontier
Sorry, I will no longer be scripting for RMXP. I may or may not give support for my scripts. I don't have the will to script in RGSS anymore.
My script


ForeverZer0

Hmm, I don't have a problem with that, but either way, there is an easy solution.

At the bottom of Main, add this line:

Audio.mci_eval('close all')


I can't reproduce the issue, but I'll add a note to the main post. Thanks for letting me know.
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.

nathmatt

thats not working it's stuck playing
Join Dead Frontier
Sorry, I will no longer be scripting for RMXP. I may or may not give support for my scripts. I don't have the will to script in RGSS anymore.
My script


ForeverZer0

Is 'Game.exe' in your active processes list in the Task Manager?
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.

nathmatt

some how firefox was the problem i closed it and the music stopped
Join Dead Frontier
Sorry, I will no longer be scripting for RMXP. I may or may not give support for my scripts. I don't have the will to script in RGSS anymore.
My script


ForeverZer0

That is very strange.
Has it done it on any following plays?

I only ask cause I have ran this multiple thousands of times, experimenting with all sorts of things, and never had that happen once. If its a problem on machines other than my own, or an issue with other software, I want to get it taken care off.
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.

nathmatt

never mind i feel completely retarded my bro had a web based game going that was the music i was hearing not yours :^_^':
Join Dead Frontier
Sorry, I will no longer be scripting for RMXP. I may or may not give support for my scripts. I don't have the will to script in RGSS anymore.
My script


ForeverZer0

You're killing me, lol  :D

Oh, well, glad to hear all is good.
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

Very nice job F0. Seriously, this is seriously amazing. I used your built in trebble and bass methods and had a lot of fun with my sub woofer. :3 *levels up*

ForeverZer0

Working on VX and VXA support right now. Everything is pretty much the same except for the RTP searching, but for some reason the "RGSSGetPathWithRTP" function in VX's DLL isn't returning the correct path. It returns only "C" instead of the actual RTP path. Maybe its my install or something...

If someone who has VX installed has the time, and is willing to spend 10 seconds running a test for me, please paste the following code in your script editor and tell me what the message window says. It doesn't matter where you paste it.

ini = Win32API.new('kernel32', 'GetPrivateProfileStringA', 'PPPPLP', 'L')
library = "\0" * 256
ini.call('Game', 'Library', '', library, 256, '.\\Game.ini')
library.delete!("\0")

RGSSGetRTPPath = Win32API.new(library, 'RGSSGetRTPPath', 'L', 'L')
RGSSGetPathWithRTP = Win32API.new(library, 'RGSSGetPathWithRTP', 'L', 'P')

paths = []
[1, 2, 3].each {|id|
  rtp = RGSSGetRTPPath.call(id)
  path = RGSSGetPathWithRTP.call(rtp)
  paths.push(path) if path != ''
}
p paths

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


ForeverZer0

April 29, 2012, 09:36:33 pm #13 Last Edit: April 29, 2012, 11:08:27 pm by ForeverZer0
Damn, time to try and dissect the DLL then. Thanks for checking for me G_G. ;)


EDIT:
Sorry for updating again so fast, but I added a few things:


  • Added RPVX compatibility

  • Added RPVXA compatibility

  • Fixed a typo

  • Made a minimum audio transition of 1, since nothing would happen if you used 0

  • Added a "start position" argument to "play". This was mainly to maintain functionality of VXA's Audio module, but it can be used by all three now.

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


LiTTleDRAgo

sound with *.ogg extension can't be played

there is no error message, but the sound isn't played

ForeverZer0

QuoteCan use any audio format you wish, so long as the appropriate codec is installed on the host computer
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

Basically if Media Player can play it, this script can play it. wma should also work.
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.

LiTTleDRAgo

yes, I already installed the codec (K-Lite ver 8.xx standard)
the *.ogg files also can played normally if I remove the script or set MCI_DEFAULT to false

also I noticed another error
Quote
    #---------------------------------------------------------------------------
    # * Use MCI Player play function
    #---------------------------------------------------------------------------
    alias mci_se_play se_play
    def se_play(filename, volume = 100, pitch = 100, start = 0)
      if MCI_DEFAULT
        mixer_play('SE', filename, volume, pitch, false, start)
      elsif RPG_VERSION == 2
        mci_se_play(filename, volume, pitch, start)
      else
        mci_se_play(filename, volume, pitch, start)
      end
    end

ForeverZer0

Setting MCI_DEFAULT to false simply uses RM's engine, you are just bypassing the script.
Here's an .ogg codec.

http://xiph.org/dshow/downloads/
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.