Last active
August 29, 2015 13:57
-
-
Save domgetter/9442127 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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