Skip to content

Instantly share code, notes, and snippets.

@domgetter
Last active August 29, 2015 13: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 domgetter/9442127 to your computer and use it in GitHub Desktop.
Save domgetter/9442127 to your computer and use it in GitHub Desktop.
require 'ffi'
# POC: play multiple arbitrary signals to a sound device
# This code defines a play_freq method which takes a frequency and a time (in milliseconds)
# and uses the waveOut native windows multimedia functions to output the frequency to the
# chosen sound device (the default device in this example)
# The code here is a basis for arbitrary, multi-signal output from any source which can be
# encoded into a PCM format.
# The example code at the bottom plays 3 simultaneous, real-time generated signals.
module Win32
WAVE_FORMAT_PCM = 1
WAVE_MAPPER = -1
#define an HWAVEOUT struct for use by all the waveOut functions
# it's a handle to a waveOut stream, so starting up multiple
# streams using different handles allows for simultaneous playback
class HWAVEOUT < FFI::Struct
layout :i, :int
end
#define WAVEHDR which is a header to a block of audio
#lpData is a pointer to the block of native memory that,
# in this case, is an integer array of PCM data
class WAVEHDR < FFI::Struct
def initialize(dwBufferLength, dwLoops, dwFlags, lpData)
self[:dwBufferLength] = dwBufferLength
self[:dwLoops] = dwLoops
self[:dwFlags] = dwFlags
self[:lpData] = lpData
end
layout :lpData, :pointer,
:dwBufferLength, :ulong,
:dwBytesRecorded, :ulong,
:dwUser, :ulong,
:dwFlags, :ulong,
:dwLoops, :ulong,
:lpNext, :pointer,
:reserved, :ulong
end
#define WAVEFORMATEX which defines the format (PCM in this case) and various properties
# like sampling rate, number of channels, etc.
class WAVEFORMATEX < FFI::Struct
def initialize(nSamplesPerSec, wBitsPerSample, nChannels, cbSize = 0)
self[:wFormatTag] = WAVE_FORMAT_PCM
self[:nChannels] = nChannels
self[:nSamplesPerSec] = nSamplesPerSec
self[:wBitsPerSample] = wBitsPerSample
self[:cbSize] = cbSize
self[:nBlockAlign] = (self[:wBitsPerSample] >> 3) * self[:nChannels]
self[:nAvgBytesPerSec] = self[:nBlockAlign] * self[:nSamplesPerSec]
end
layout :wFormatTag, :ushort,
:nChannels, :ushort,
:nSamplesPerSec, :ulong,
:nAvgBytesPerSec, :ulong,
:nBlockAlign, :ushort,
:wBitsPerSample, :ushort,
:cbSize, :ushort
end
class Sound
extend FFI::Library
private
typedef :uintptr_t, :hwaveout
typedef :uint, :mmresult
typedef :ulong, :dword
ffi_lib :winmm
# attach the necessary waveOut functions with proper parameters
attach_function :waveOutOpen, [:pointer, :uint, :pointer, :dword, :dword, :dword], :mmresult
attach_function :waveOutPrepareHeader, [:hwaveout, :pointer, :uint], :mmresult
attach_function :waveOutWrite, [:hwaveout, :pointer, :uint], :mmresult
attach_function :waveOutUnprepareHeader, [:hwaveout, :pointer, :uint], :mmresult
attach_function :waveOutClose, [:hwaveout], :mmresult
ffi_lib FFI::Library::LIBC
#also import LIBC memory allocation functions to port Ruby objects to native memory
attach_function :malloc, [:size_t], :pointer
attach_function :free, [:pointer], :void
public
def self.play_freq(freq, duration = 1000, normalizer = 1)
#for this to work, we have to do 5 things in order
# 1. open a waveOut stream
# 2. prepare a header for each block of audio we shove into the stream
# 3. shove the PCM data with it's well-formed header into the stream
# 4. deconstruct the header when it's no longer in use
# 5. close up the stream
hWaveOut = HWAVEOUT.new
sample_rate = 44100
bits_per_sample = 16
channels = 1
wfx = WAVEFORMATEX.new(sample_rate, bits_per_sample, channels)
# waveOutOpen opens up the stream
if ((error_code = waveOutOpen(hWaveOut.pointer, WAVE_MAPPER, wfx.pointer, 0, 0, 0)) != 0)
raise SystemCallError, FFI.errno, "waveOutOpen: #{error_code}"
end
data = fill_data(freq, normalizer, wfx, duration)
#we have to allocate the data buffer in native ram, since C doesn't understand Ruby objects
data_buffer = malloc(data.first.size * data.size)
data_buffer.write_array_of_int data
buffer_length = wfx[:nAvgBytesPerSec]*duration/1000
header = WAVEHDR.new(buffer_length, 1, (4 | 8), data_buffer)
#waveOutPrepareHeader configures the header for the block of audio we're about to stream
if ((error_code = waveOutPrepareHeader(hWaveOut[:i], header.pointer, header.size)) != 0)
raise SystemCallError, FFI.errno, "waveOutPrepareHeader: #{error_code}"
end
# yield control back to the thread at this point and continue from here later
# so that multiple tones are played simultaneously
Thread.pass
# waveOutWrite actually does the work. This is where the data is streamed.
if ((error_code = waveOutWrite(hWaveOut[:i], header.pointer, header.size)) != 0)
raise SystemCallError, FFI.errno, "waveOutWrite: #{error_code}"
end
#waveOutUnprepareHeader does a few things, one of which is that it tells us if the
#current block is still streaming so we can go do other things like process
# audio for the buffer
while (waveOutUnprepareHeader(hWaveOut[:i], header.pointer, header.size) == 33)
sleep 0.1
end
# like good programmers, we close up the stream...
if ((error_code = waveOutClose(hWaveOut[:i])) != 0)
raise SystemCallError, FFI.errno, "waveOutClose: #{error_code}"
end
#...and of course, free up the allocated memory for the audio buffer! (otherwise, memory leak)
free data_buffer
self
end
private
def self.fill_data(freq, normalizer, wfx, duration)
#this just makes up a second's worth of PCM data from two frequencies, 440Hz and 660Hz
# it encodes the two freqs to PCM in real time, and it takes less than a tenth of a second
data = []
ramp = 200.0
#time = Time.now
samps = (wfx[:nSamplesPerSec]/2*duration/1000.0).floor
samps.times do |sample|
angle = (2.0*Math::PI*freq) * sample/samps * duration/1000
factor = 0.5*Math.sin(angle) + 0.5
x = 32768.0*factor/normalizer
# these two conditionals ramp up the amplitude of the wave at the beginning and end
# of playback to prevent those horrible audio clicks, although sometimes there
# is still a click in the beginning
if sample < ramp
x *= sample/ramp
end
if samps - sample < ramp
x *= (samps - sample)/ramp
end
data << x.floor
end
# output for general idea of computation time for raw digital signal processing.
# One second for one wave takes around 10 milliseconds
#puts "This took about #{(1000.0*(Time.now - time)).round} milliseconds to compute"
data
end
end
end
# array of three frequencies to iterate through It is an A major chord.
freqs = [440, 5.0/4*440, 660]
threads = []
# start up three threads to play all three frequencies at once
freqs.each do |freq|
threads << Thread.new { Win32::Sound.play_freq(freq) }
end
threads.each do |thread|
thread.join
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment