require 'ffi'
module Win32
WAVE_FORMAT_PCM = 1
WAVE_MAPPER = -1
class HWAVEOUT < FFI::Struct
layout :i, :int
end
class WAVEHDR < FFI::Struct
def initialize(dwBufferLength, dwBytesRecorded, dwLoops, dwFlags, lpData)
self[:dwBufferLength] = dwBufferLength
self[:dwBytesRecorded] = dwBytesRecorded
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
class WAVEFORMATEX < FFI::Struct
def initialize(nSamplesPerSec, wBitsPerSample, nChannels, cbSize)
self[:nSamplesPerSec] = nSamplesPerSec
self[:wBitsPerSample] = wBitsPerSample
self[:nChannels] = nChannels
self[:cbSize] = cbSize
self[:wFormatTag] = WAVE_FORMAT_PCM
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_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
attach_function :malloc, [:size_t], :pointer
attach_function :calloc, [:size_t], :pointer
attach_function :realloc, [:pointer, :size_t], :pointer
attach_function :free, [:pointer], :void
attach_function :memcpy, [:pointer, :pointer, :size_t], :pointer
def self.fill_data
data = []
period = 1.0/440.0
ramp = 1500.0
time = Time.now
44100.times do |sample|
t = sample/44100.0
angle = (2.0*Math::PI/period) * t
angle2 = 1.5*angle
factor = 0.5*Math.sin(angle) + 0.5
factor2 = 0.5*Math.sin(angle2) + 0.5
x = 0.5*32768.0*(factor + factor2)
if sample < ramp
x *= sample/ramp
end
if 44100 - sample < ramp
x *= (44100 - sample)/ramp
end
data << x.floor
end
puts "This took #{Time.now - time} milliseconds to compute"
data
end
public
def self.play
hWaveOut = HWAVEOUT.new
wfx = WAVEFORMATEX.new(44100, 16, 2, 0)
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
data_buffer = malloc(data.first.size * data.size)
data_buffer.write_array_of_int data
header = WAVEHDR.new(4*44100, 0, 1, (4 | 8), data_buffer)
if ((error_code = waveOutPrepareHeader(hWaveOut[:i], header.pointer, header.size)) != 0)
raise SystemCallError, FFI.errno, "waveOutPrepareHeader: #{error_code}"
end
if ((error_code = waveOutWrite(hWaveOut[:i], header.pointer, header.size)) != 0)
raise SystemCallError, FFI.errno, "waveOutWrite: #{error_code}"
end
while (waveOutUnprepareHeader(hWaveOut[:i], header.pointer, header.size) == 33)
sleep 0.1
end
if ((error_code = waveOutClose(hWaveOut[:i])) != 0)
raise SystemCallError, FFI.errno, "waveOutClose: #{error_code}"
end
self
end
end
end
Win32::Sound.play
Saturday, March 8, 2014
Holy crap I've done it
I've done it. I can now play two arbitrary frequencies through FFI using native library calls. And all in 155 lines of code. Also, it takes 76 milliseconds to calculate 1 second of PCM audio from scratch on my system.
waveOutOpen
I have successfully made a library call to waveOutOpen!
require 'ffi'
module WaveOutWrapper
WAVE_FORMAT_PCM = 1
WAVE_MAPPER = -1
class WAVEFORMATEX < FFI::Struct
def initialize(nSamplesPerSec, wBitsPerSample, nChannels, cbSize)
self[:nSamplesPerSec] = nSamplesPerSec
self[:wBitsPerSample] = wBitsPerSample
self[:nChannels] = nChannels
self[:cbSize] = cbSize
self[:wFormatTag] = WAVE_FORMAT_PCM
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 :uint, :mmresult
typedef :ulong, :dword
ffi_lib :winmm
attach_function :waveOutOpen, [:pointer, :uint, :pointer, :dword, :dword, :dword], :mmresult
attach_function :waveOutClose, [:pointer], :mmresult
public
def self.play
hWaveOut = FFI::MemoryPointer.new(:int)
wfx = WAVEFORMATEX.new(44100, 16, 2, 0)
waveOutOpenResult = waveOutOpen(hWaveOut, WAVE_MAPPER, wfx.pointer, 0, 0, 0)
raise SystemCallError, FFI.errno, "waveOutOpen didn't work: #{waveOutOpenResult}" if waveOutOpenResult != 0
waveOutClose(hWaveOut)
self
end
end
end
WaveOutWrapper::Sound.play # this won't actually play anything, but it doesn't return an error!
Audio and Ruby
I'm working on a gem which will make songs for me. This may be a pie-in-the-sky dream, but I'm still working on it. One thing I need is to be able to play arbitrary digital signals to a sound device. A cursory google search will show that there is a gem to do just this!
gem install win32-audio
And then all you have to do is load up the gem, and make a native system call to make a beep.
This code will play a 440Hz tone for 500 millisecond. But there's a problem. It's synchronous. I need to be able to play multiple tones at once, or even be able to mix some signals and stream the result to a sound device.
The win32-audio uses the ffi gem to do its dirty work, so I've decided to jump in and do the same.
My plan is to code up a gem (or an extension to win32-audio) which makes native calls to the waveOut multimedia library in windows. Hopefully then I'll be able to shove in arbitrary PCM signal to a sound device of my choosing, and even with as many channels as I'd like!
It's basically this:
1. waveOutOpen to open up a device for streaming
2. waveOutPrepareHeader to prepare a block of audio for playback
3. waveOutWrite to submit the prepared audio to the device for playing
4. waveOutClose to close up the stream when I'm done
As much as I wish it was simple to just define these four functions in ffi, it gets a little more hairy. Of course, I have to go in and define and set up memory management for the underlying code which relies on various structs like WAVEFORMATEX and WAVEHDR.
Wish me luck!
gem install win32-audio
And then all you have to do is load up the gem, and make a native system call to make a beep.
require 'win32/sound'include Win32Sound.beep(440, 500)
This code will play a 440Hz tone for 500 millisecond. But there's a problem. It's synchronous. I need to be able to play multiple tones at once, or even be able to mix some signals and stream the result to a sound device.
The win32-audio uses the ffi gem to do its dirty work, so I've decided to jump in and do the same.
My plan is to code up a gem (or an extension to win32-audio) which makes native calls to the waveOut multimedia library in windows. Hopefully then I'll be able to shove in arbitrary PCM signal to a sound device of my choosing, and even with as many channels as I'd like!
It's basically this:
1. waveOutOpen to open up a device for streaming
2. waveOutPrepareHeader to prepare a block of audio for playback
3. waveOutWrite to submit the prepared audio to the device for playing
4. waveOutClose to close up the stream when I'm done
As much as I wish it was simple to just define these four functions in ffi, it gets a little more hairy. Of course, I have to go in and define and set up memory management for the underlying code which relies on various structs like WAVEFORMATEX and WAVEHDR.
Wish me luck!
Subscribe to:
Comments (Atom)