Skip to content

Instantly share code, notes, and snippets.

@mistydemeo
Created January 21, 2012 04:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mistydemeo/76900f8e7b9f7e1592ee to your computer and use it in GitHub Desktop.
Save mistydemeo/76900f8e7b9f7e1592ee to your computer and use it in GitHub Desktop.
require_relative 'dsl'
module Paula
# Select the appropriate player, then supply the player object
def self.factory song
# Stop the last player before creating a new one
# TODO This probably breaks if the laster player is garbage collected
@last_player.stop if @last_player
@last_player = eval "Paula::#{@formats[File.extname(song).downcase]}::API.new song"
end
# allow libraries to make themselves known
def self.declare hash
@formats ||= {}
@prefer ||= {}
hash[:extensions].each do |ext|
@formats.update ext => hash[:library] unless prefer? ext
end
end
# Extensions declared by "prefer" will not be overwritten by other declarations
# This should *never* be used in a library declaration, only by client players
def self.prefer hash
@prefer ||= {}
@formats ||= {}
hash[:extensions].each do |ext|
@formats.update ext => hash[:library]
@prefer.update ext => true
end
end
def self.prefer? ext
@prefer[ext]
end
def self.rate
@rate ||= 48000
end
def self.rate= rate
@rate = rate
end
def self.loops
@loops ||= 2
end
def self.loops= loops
@loops = loops
end
def self.default_duration
@default_duration ||= 300000
end
def self.default_duration= duration
@default_duration = duration
end
def self.sample_size
@sample_size ||= 16384
end
def self.sample_size= size
@sample_size = size
end
class Library
attr_accessor :song, :duration, :elapsed, :samples
def initialize song, opts={buffer: true}
@song = song
# set up the byte buffer; this works the same for all libraries
if opts[:buffer]
@mutex = Mutex.new
@buffer = ''
@byte_position = 0
@render_thread = Thread.new do
@unbuffered_samples.each do |sample|
@buffer << sample
end
end
@samples = Enumerator.new do |yielder|
yielder.yield @buffer[@byte_position..@byte_position+Paula.sample_size]
@byte_position += Paula.sample_size
@elapsed = (@byte_position / ((Paula.rate * 16 * 2) / 8).to_f * 1000).to_i
end
end
end
def title
File.basename @song
end
def stop
end
class << self
def buffer opts={}
size == opts[:size] || 16384
@raw_buffer = opts[:type], *opts[:arguments]
# assumes stereo, 16-bit
@buffers_per_second = ((Paula.rate * 16 * 2) / 8) / size.to_f
end
end
end
end
require 'ffi'
Paula.declare library: 'MDXMini', extensions: ['.mdx']
module Paula
module MDXMini
extend FFI::Library
ffi_lib "mdxmini"
attach_function :mdx_open, [:pointer, :string, :string], :int
attach_function :mdx_set_rate, [:int], :void
attach_function :mdx_set_max_loop, [:pointer, :int], :void
attach_function :mdx_next_frame, [:pointer], :int
attach_function :mdx_frame_length, [:pointer], :int
attach_function :mdx_make_buffer, [:pointer, :int], :void
attach_function :mdx_calc_sample, [:pointer, :pointer, :int], :int
attach_function :mdx_get_title, [:pointer, :pointer], :void
# this returns a value in seconds
attach_function :mdx_get_length, [:pointer], :int
attach_function :mdx_get_tracks, [:pointer], :int
attach_function :mdx_get_current_notes, [:pointer, :pointer, :int], :void
attach_function :mdx_stop, [:pointer], :void
attach_function :mdx_get_sample_size, [:pointer], :int
attach_function :mdx_get_buffer_size, [:pointer], :int
# t_mdxmini struct used in C library
class T_mdxmini < FFI::Struct
layout :samples, :int,
:channels, :int,
:mdx, :pointer,
:pdx, :pointer,
:self, :pointer
end
class API < Paula::Library
#buffer type: FFI::MemoryPointer,
# arguments: [:short, 16384/2, true], size: 16384
def initialize song
# mdx_set_rate *must* come before mdx_open
Paula::MDXMini.mdx_set_rate Paula.rate
@mini = Paula::MDXMini::T_mdxmini.new
Paula::MDXMini.mdx_open @mini, song, File.dirname(song)
Paula::MDXMini.mdx_set_max_loop @mini, Paula.loops
@sample_buffer = FFI::MemoryPointer.new :short, 16384/2, true
@buffers_per_second = ((Paula.rate * 16 * 2) / 8) / @sample_buffer.size.to_f
@duration = 1000 * Paula::MDXMini.mdx_get_length(@mini)
@unbuffered_samples= Enumerator.new do |yielder|
buffers = 0
begin
buffers += 1
MDXMini.mdx_calc_sample @mini, @sample_buffer, 4096
yielder.yield @sample_buffer.read_bytes(@sample_buffer.size)
end while buffers < (@duration * @buffers_per_second / 1000)
end
super
end
def title
ptr = FFI::MemoryPointer.new :char, 100
Paula::MDXMini.mdx_get_title @mini, ptr
ptr.read_bytes(100).force_encoding('Shift_JIS').encode!('UTF-8').rstrip!
rescue
"Unable to retrieve title..."
end
def stop
Paula::MDXMini.mdx_stop @mini
end
end
end
end
require 'ffi'
Paula.declare library: 'PMDMini', extensions: ['.m', '.mz', '.m2']
module Paula
module PMDMini
extend FFI::Library
ffi_lib "pmdmini"
attach_function :pmd_init, [ ], :void
attach_function :pmd_setrate, [ :int ], :void
attach_function :pmd_is_pmd, [ :string ], :int
# pmd_play name is somewhat misleading; this loads a song from disk
attach_function :pmd_play, [ :string ], :int
# pmd_length_sec and pmd_loop_sec have reduced precision compared to
# internal length; use pmd_length and pmd_loop instead
attach_function :pmd_length_sec, [ ], :int
attach_function :pmd_loop_sec, [ ], :int
attach_function :pmd_renderer, [ :pointer, :int ], :void
attach_function :pmd_stop, [ ], :void
attach_function :pmd_get_title, [ :pointer ], :void
attach_function :pmd_get_compo, [ :pointer ], :void
attach_function :pmd_get_tracks, [ ], :int
attach_function :pmd_get_current_notes, [ :pointer, :int ], :void
# full song length should be calculated by combining these two variables
# values are in milliseconds
attach_variable :pmd_length, :int
attach_variable :pmd_loop, :int
class API < Paula::Library
#buffer type: FFI::MemoryPointer,
# arguments: [:short, 16384/2, true], size: 16384
def initialize song
@sample_buffer = FFI::MemoryPointer.new :char, 16384, true
@buffers_per_second = ((Paula.rate * 16 * 2) / 8) / @sample_buffer.size.to_f
Paula::PMDMini.pmd_init
# pmd_setrate comes *after* pmd_ini, *before* pmd_play
Paula::PMDMini.pmd_setrate Paula.rate
Paula::PMDMini.pmd_play song
@duration = Paula::PMDMini.pmd_length + Paula::PMDMini.pmd_loop
@unbuffered_samples = Enumerator.new do |yielder|
buffers = 0
begin
buffers += 1
@elapsed = (buffers / @buffers_per_second * 1000).to_i
Paula::PMDMini.pmd_renderer @sample_buffer, 4096
yielder.yield @sample_buffer.read_bytes(@sample_buffer.size)
end while buffers < (@duration * @buffers_per_second / 1000)
end
super
end
def title
ptr = FFI::MemoryPointer.new :char, 1024, true
Paula::PMDMini.pmd_get_title ptr
ptr.read_bytes(1024).force_encoding('Shift_JIS').encode!('UTF-8').rstrip!
rescue
"Unable to retrieve title..."
end
def stop
Paula::PMDMini.pmd_stop
end
end
end
end
require 'coreaudio'
require_relative 'paula'
class SamplePlayer
class << self; include Paula::DSL; end
loops 2
default_duration 300000 # milliseconds
def load song
@player = Paula.factory(File.expand_path song)
end
def play opts
@player.samples.each do |sample|
# print the current playtime
print "#{backspace*10}#{@player.elapsed / 1000}/#{@player.duration / 1000}"
# write the audio to the buffer
opts[:to] << NArray.to_narray(sample, NArray::SINT, 2, 4096)
end
end
def backspace
"\b"
end
end
# initialize the sound card
dev = CoreAudio.default_output_device
outbuf = dev.output_buffer 1024
outbuf.start
# create a player object, load the song, and play
player = SamplePlayer.new
player.load ARGV[0]
player.play to: outbuf
outbuf.stop
puts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment