diff --git a/examples/Ruby/README.md b/examples/Ruby/README.md new file mode 100644 index 00000000..162a37c1 --- /dev/null +++ b/examples/Ruby/README.md @@ -0,0 +1,19 @@ +Sample program to detect hotword. + +Dependencies +=== +snowboy shared lib +`cd ../../swig/Ruby/C && make` + +FFI for ruby. +`sudo gem i ffi` + + +This sample uses portaudio to capture with. +a library is provied in `../../swig/Ruby/ext/capture/port-audio` + +`cd ../../swig/Ruby/ext/capture/port-audio && make` + +Usage +=== +`ruby port-audio-sample.rb` diff --git a/examples/Ruby/port-audio-sample.rb b/examples/Ruby/port-audio-sample.rb new file mode 100644 index 00000000..5b9074a8 --- /dev/null +++ b/examples/Ruby/port-audio-sample.rb @@ -0,0 +1,23 @@ +$: << File.join(File.dirname(__FILE__), "..", "..", "swig", "Ruby", "lib") + + +require "snowboy" +require "snowboy/capture/port-audio/port-audio-capture" + +resource_path = "../../resources/common.res" +model_path = "../../resources/models/snowboy.umdl" + +snowboy = Snowboy::Detector.new(model: model_path, resource: resource_path, gain: 1, sensitivity: 0.5) +pac = Snowboy::Capture::PortAudio.new + +puts "Listening... press Ctrl-c to exit." + +pac.run(snowboy.sample_rate, snowboy.n_channels, snowboy.bits_per_sample) do |data, length| + result = snowboy.run_detection(data, length, false) + + if result > 0 + puts "Hotword %d detected." % result + end +end + +while true; end diff --git a/swig/Ruby/C/Makefile b/swig/Ruby/C/Makefile new file mode 100644 index 00000000..fe7e4667 --- /dev/null +++ b/swig/Ruby/C/Makefile @@ -0,0 +1,17 @@ +include libsnowboydetect.mk + +SHAREDLIB = libsnowboydetect.so + +OBJFILES = snowboy-detect-c-wrapper.o + +all: $(SHAREDLIB) + +%.a: + $(MAKE) -C ${@D} ${@F} + +# We have to use the C++ compiler to link. +$(SHAREDLIB): $(SNOWBOYDETECTLIBFILE) $(OBJFILES) + $(CXX) $(OBJFILES) $(SNOWBOYDETECTLIBFILE) $(LDLIBS) -shared -o $(SHAREDLIB) + +clean: + -rm -f *.o *.a $(SHAREDLIB) $(OBJFILES) diff --git a/swig/Ruby/C/libsnowboydetect.mk b/swig/Ruby/C/libsnowboydetect.mk new file mode 100644 index 00000000..c96d9807 --- /dev/null +++ b/swig/Ruby/C/libsnowboydetect.mk @@ -0,0 +1,55 @@ +TOPDIR := ../../../ +DYNAMIC := True +CC := +CXX := +LDFLAGS := +LDLIBS := + +CFLAGS := +CXXFLAGS += -D_GLIBCXX_USE_CXX11_ABI=0 + +ifeq ($(DYNAMIC), True) + CFLAGS += -fPIC + CXXFLAGS += -fPIC +endif + +ifeq ($(shell uname -m | cut -c 1-3), x86) + CFLAGS += -msse -msse2 + CXXFLAGS += -msse -msse2 +endif + +ifeq ($(shell uname), Darwin) + # By default Mac uses clang++ as g++, but people may have changed their + # default configuration. + CC := clang + CXX := clang++ + CFLAGS += -I$(TOPDIR) -Wall -I$(PORTAUDIOINC) + CXXFLAGS += -I$(TOPDIR) -Wall -Wno-sign-compare -Winit-self \ + -DHAVE_POSIX_MEMALIGN -DHAVE_CLAPACK + LDLIBS += -ldl -lm -framework Accelerate -framework CoreAudio \ + -framework AudioToolbox -framework AudioUnit -framework CoreServices + SNOWBOYDETECTLIBFILE := $(TOPDIR)/lib/osx/libsnowboy-detect.a +else ifeq ($(shell uname), Linux) + CC := gcc + CXX := g++ + CFLAGS += -I$(TOPDIR) -Wall + CXXFLAGS += -I$(TOPDIR) -std=c++0x -Wall -Wno-sign-compare \ + -Wno-unused-local-typedefs -Winit-self -rdynamic \ + -DHAVE_POSIX_MEMALIGN + LDLIBS += -ldl -lm -Wl,-Bstatic -Wl,-Bdynamic -lrt -lpthread\ + -L/usr/lib/atlas-base -lf77blas -lcblas -llapack_atlas -latlas + SNOWBOYDETECTLIBFILE := $(TOPDIR)/lib/ubuntu64/libsnowboy-detect.a + ifneq (,$(findstring arm,$(shell uname -m))) + SNOWBOYDETECTLIBFILE := $(TOPDIR)/lib/rpi/libsnowboy-detect.a + endif +endif + +# Suppress clang warnings... +COMPILER = $(shell $(CXX) -v 2>&1 ) +ifeq ($(findstring clang,$(COMPILER)), clang) + CXXFLAGS += -Wno-mismatched-tags -Wno-c++11-extensions +endif + +# Set optimization level. +CFLAGS += -O3 +CXXFLAGS += -O3 diff --git a/swig/Ruby/C/snowboy-detect-c-wrapper.cc b/swig/Ruby/C/snowboy-detect-c-wrapper.cc new file mode 120000 index 00000000..49aedb3e --- /dev/null +++ b/swig/Ruby/C/snowboy-detect-c-wrapper.cc @@ -0,0 +1 @@ +../../../examples/C/snowboy-detect-c-wrapper.cc \ No newline at end of file diff --git a/swig/Ruby/C/snowboy-detect-c-wrapper.h b/swig/Ruby/C/snowboy-detect-c-wrapper.h new file mode 120000 index 00000000..df4846ef --- /dev/null +++ b/swig/Ruby/C/snowboy-detect-c-wrapper.h @@ -0,0 +1 @@ +../../../examples/C/snowboy-detect-c-wrapper.h \ No newline at end of file diff --git a/swig/Ruby/README.md b/swig/Ruby/README.md new file mode 100644 index 00000000..39d121c7 --- /dev/null +++ b/swig/Ruby/README.md @@ -0,0 +1,32 @@ +Ruby bindings for `snowboy` + +Dependencies +=== +snowboy shared lib +`cd ./C && make` + +FFI for ruby. +`sudo gem i ffi` + +Extra +=== +A simple audio capture tool is provided in `./ext/capture/port-audio` +`cd ./ext/capture/port-audio && make` + +Usage +=== +```ruby +require "./lib/snowboy" + +snowboy = Snowboy::Detect.new(resource: resource_path, model: model_path) + +# get audio data +# ... + +result = snowboy.run_detection(data, data_length, false) + +if result > 0 + # handle result +end +``` + diff --git a/swig/Ruby/ext/capture/port-audio/Makefile b/swig/Ruby/ext/capture/port-audio/Makefile new file mode 100644 index 00000000..a96633a7 --- /dev/null +++ b/swig/Ruby/ext/capture/port-audio/Makefile @@ -0,0 +1,20 @@ +include port-audio-capture.mk + +SHAREDLIB = port-audio-capture.so + +OBJFILES = port-audio-capture.o + +all: $(SHAREDLIB) + +%.a: + $(MAKE) -C ${@D} ${@F} + +# We have to use the C++ compiler to link. +$(SHAREDLIB): $(PORTAUDIOLIBS) $(OBJFILES) + $(CXX) $(OBJFILES) $(PORTAUDIOLIBS) $(LDLIBS) -shared -o $(SHAREDLIB) + +$(PORTAUDIOLIBS): + @-./install_portaudio.sh + +clean: + -rm -f *.o *.a $(SHAREDLIB) $(OBJFILES) diff --git a/swig/Ruby/ext/capture/port-audio/install_portaudio.sh b/swig/Ruby/ext/capture/port-audio/install_portaudio.sh new file mode 120000 index 00000000..b653772a --- /dev/null +++ b/swig/Ruby/ext/capture/port-audio/install_portaudio.sh @@ -0,0 +1 @@ +../../../../../examples/C/install_portaudio.sh \ No newline at end of file diff --git a/swig/Ruby/ext/capture/port-audio/patches b/swig/Ruby/ext/capture/port-audio/patches new file mode 120000 index 00000000..c67d76cc --- /dev/null +++ b/swig/Ruby/ext/capture/port-audio/patches @@ -0,0 +1 @@ +../../../../../examples/C/patches \ No newline at end of file diff --git a/swig/Ruby/ext/capture/port-audio/port-audio-capture.c b/swig/Ruby/ext/capture/port-audio/port-audio-capture.c new file mode 100644 index 00000000..4495d7df --- /dev/null +++ b/swig/Ruby/ext/capture/port-audio/port-audio-capture.c @@ -0,0 +1,160 @@ +// bindings/Ruby/ext/capture/port-audio/port-audio-capture.c + +// Copyright 2017 KITT.AI (author: Guoguo Chen, PpiBbuRr) + +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + // Pointer to the ring buffer memory. + char* ringbuffer; + // Ring buffer wrapper used in PortAudio. + PaUtilRingBuffer pa_ringbuffer; + // Pointer to PortAudio stream. + PaStream* pa_stream; + // Number of lost samples at each LoadAudioData() due to ring buffer overflow. + int num_lost_samples; + // Wait for this number of samples in each LoadAudioData() call. + int min_read_samples; + // Pointer to the audio data. + int16_t* audio_data; +} rb_snowboy_port_audio_capture_t; + +int rb_snowboy_port_audio_capture_callback(const void* input, + void* output, + unsigned long frame_count, + const PaStreamCallbackTimeInfo* time_info, + PaStreamCallbackFlags status_flags, + void* user_data) { + + rb_snowboy_port_audio_capture_t* capture = (rb_snowboy_port_audio_capture_t*) user_data; + ring_buffer_size_t num_written_samples = PaUtil_WriteRingBuffer(&capture->pa_ringbuffer, input, frame_count); + capture->num_lost_samples += frame_count - num_written_samples; + return paContinue; +} + +void rb_snowboy_port_audio_capture_start_audio_capturing(rb_snowboy_port_audio_capture_t* capture, int sample_rate, + int num_channels, int bits_per_sample) { + capture->audio_data = NULL; + capture->num_lost_samples = 0; + capture->min_read_samples = sample_rate * 0.1; + + // Allocates ring buffer memory. + int ringbuffer_size = 16384; + capture->ringbuffer = (char*)( + PaUtil_AllocateMemory(bits_per_sample / 8 * ringbuffer_size)); + if (capture->ringbuffer == NULL) { + fprintf(stderr, "Fail to allocate memory for ring buffer.\n"); + exit(1); + } + + // Initializes PortAudio ring buffer. + ring_buffer_size_t rb_init_ans = + PaUtil_InitializeRingBuffer(&capture->pa_ringbuffer, bits_per_sample / 8, + ringbuffer_size, capture->ringbuffer); + if (rb_init_ans == -1) { + fprintf(stderr, "Ring buffer size is not power of 2.\n"); + exit(1); + } + + // Initializes PortAudio. + PaError pa_init_ans = Pa_Initialize(); + if (pa_init_ans != paNoError) { + fprintf(stderr, "Fail to initialize PortAudio, error message is %s.\n", + Pa_GetErrorText(pa_init_ans)); + exit(1); + } + + PaError pa_open_ans; + if (bits_per_sample == 8) { + pa_open_ans = Pa_OpenDefaultStream( + &capture->pa_stream, num_channels, 0, paUInt8, sample_rate, + paFramesPerBufferUnspecified, rb_snowboy_port_audio_capture_callback, capture); + } else if (bits_per_sample == 16) { + pa_open_ans = Pa_OpenDefaultStream( + &capture->pa_stream, num_channels, 0, paInt16, sample_rate, + paFramesPerBufferUnspecified, rb_snowboy_port_audio_capture_callback, capture); + } else if (bits_per_sample == 32) { + pa_open_ans = Pa_OpenDefaultStream( + &capture->pa_stream, num_channels, 0, paInt32, sample_rate, + paFramesPerBufferUnspecified, rb_snowboy_port_audio_capture_callback, capture); + } else { + fprintf(stderr, "Unsupported BitsPerSample: %d.\n", bits_per_sample); + exit(1); + } + if (pa_open_ans != paNoError) { + fprintf(stderr, "Fail to open PortAudio stream, error message is %s.\n", + Pa_GetErrorText(pa_open_ans)); + exit(1); + } + + PaError pa_stream_start_ans = Pa_StartStream(capture->pa_stream); + if (pa_stream_start_ans != paNoError) { + fprintf(stderr, "Fail to start PortAudio stream, error message is %s.\n", + Pa_GetErrorText(pa_stream_start_ans)); + exit(1); + } +} + +void rb_snowboy_port_audio_capture_stop_audio_capturing(rb_snowboy_port_audio_capture_t* capture) { + if (capture->audio_data != NULL) { + free(capture->audio_data); + capture->audio_data = NULL; + } + Pa_StopStream(capture->pa_stream); + Pa_CloseStream(capture->pa_stream); + Pa_Terminate(); + PaUtil_FreeMemory(capture->ringbuffer); +} + +int rb_snowboy_port_audio_capture_load_audio_data(rb_snowboy_port_audio_capture_t* capture) { + if (capture->audio_data != NULL) { + free(capture->audio_data); + capture->audio_data = NULL; + } + + // Checks ring buffer overflow. + if (capture->num_lost_samples > 0) { + fprintf(stderr, "Lost %d samples due to ring buffer overflow.\n", + capture->num_lost_samples); + capture->num_lost_samples = 0; + } + + ring_buffer_size_t num_available_samples = 0; + while (true) { + num_available_samples = + PaUtil_GetRingBufferReadAvailable(&capture->pa_ringbuffer); + if (num_available_samples >= capture->min_read_samples) { + break; + } + Pa_Sleep(5); + } + + // Reads data. + num_available_samples = PaUtil_GetRingBufferReadAvailable(&capture->pa_ringbuffer); + capture->audio_data = malloc(num_available_samples * sizeof(int16_t)); + ring_buffer_size_t num_read_samples = PaUtil_ReadRingBuffer( + &capture->pa_ringbuffer, capture->audio_data, num_available_samples); + if (num_read_samples != num_available_samples) { + fprintf(stderr, "%d samples were available, but only %d samples were read" + ".\n", num_available_samples, num_read_samples); + } + return num_read_samples; +} + +rb_snowboy_port_audio_capture_t* rb_snowboy_port_audio_capture_new() { + rb_snowboy_port_audio_capture_t* capture = malloc(sizeof(rb_snowboy_port_audio_capture_t)); + + memset(capture, 0, sizeof(rb_snowboy_port_audio_capture_t)); + + return capture; +} + +int16_t* rb_snowboy_port_audio_capture_get_audio_data(rb_snowboy_port_audio_capture_t* capture) { + return capture->audio_data; +} diff --git a/swig/Ruby/ext/capture/port-audio/port-audio-capture.mk b/swig/Ruby/ext/capture/port-audio/port-audio-capture.mk new file mode 100644 index 00000000..79a6154d --- /dev/null +++ b/swig/Ruby/ext/capture/port-audio/port-audio-capture.mk @@ -0,0 +1,53 @@ +DYNAMIC := True +CC := +CXX := +LDFLAGS := +LDLIBS := +PORTAUDIOINC := portaudio/install/include +PORTAUDIOLIBS := portaudio/install/lib/libportaudio.a + +CFLAGS := +CXXFLAGS += -D_GLIBCXX_USE_CXX11_ABI=0 + +ifeq ($(DYNAMIC), True) + CFLAGS += -fPIC + CXXFLAGS += -fPIC +endif + +ifeq ($(shell uname -m | cut -c 1-3), x86) + CFLAGS += -msse -msse2 + CXXFLAGS += -msse -msse2 +endif + +ifeq ($(shell uname), Darwin) + # By default Mac uses clang++ as g++, but people may have changed their + # default configuration. + CC := clang + CXX := clang++ + CFLAGS += -I$(TOPDIR) -Wall -I$(PORTAUDIOINC) + CXXFLAGS += -I$(TOPDIR) -Wall -Wno-sign-compare -Winit-self \ + -DHAVE_POSIX_MEMALIGN -DHAVE_CLAPACK -I$(PORTAUDIOINC) + LDLIBS += -ldl -lm -framework Accelerate -framework CoreAudio \ + -framework AudioToolbox -framework AudioUnit -framework CoreServices \ + $(PORTAUDIOLIBS) +else ifeq ($(shell uname), Linux) + CC := gcc + CXX := g++ + CFLAGS += -I$(TOPDIR) -Wall -I$(PORTAUDIOINC) + CXXFLAGS += -I$(TOPDIR) -std=c++0x -Wall -Wno-sign-compare \ + -Wno-unused-local-typedefs -Winit-self -rdynamic \ + -DHAVE_POSIX_MEMALIGN -I$(PORTAUDIOINC) + LDLIBS += -ldl -lm -Wl,-Bstatic -Wl,-Bdynamic -lrt -lpthread $(PORTAUDIOLIBS) -lasound\ + #ifneq (,$(findstring arm,$(shell uname -m))) + #endif +endif + +# Suppress clang warnings... +COMPILER = $(shell $(CXX) -v 2>&1 ) +ifeq ($(findstring clang,$(COMPILER)), clang) + CXXFLAGS += -Wno-mismatched-tags -Wno-c++11-extensions +endif + +# Set optimization level. +CFLAGS += -O3 +CXXFLAGS += -O3 diff --git a/swig/Ruby/lib/snowboy.rb b/swig/Ruby/lib/snowboy.rb new file mode 100644 index 00000000..92e61bf8 --- /dev/null +++ b/swig/Ruby/lib/snowboy.rb @@ -0,0 +1,139 @@ +$: << BASE_PATH=File.dirname(__FILE__) + +require 'ffi' + +module Snowboy + module Lib + extend FFI::Library + + so_path = File.expand_path(File.join(BASE_PATH, '..', 'C', 'libsnowboydetect.so')) + + if File.exist?(so_path) + ffi_lib so_path + else + ffi_lib "libsnowboydetect" + end + + attach_function :SnowboyDetectConstructor, [:string, :string], :pointer + attach_function :SnowboyDetectDestructor, [:pointer], :void + attach_function :SnowboyDetectBitsPerSample, [:pointer], :int + attach_function :SnowboyDetectSampleRate, [:pointer], :int + attach_function :SnowboyDetectNumChannels, [:pointer], :int + attach_function :SnowboyDetectNumHotwords, [:pointer], :int + attach_function :SnowboyDetectReset, [:pointer], :bool + attach_function :SnowboyDetectUpdateModel, [:pointer], :void + attach_function :SnowboyDetectApplyFrontend, [:pointer, :bool], :void + attach_function :SnowboyDetectRunDetection, [:pointer, :pointer, :int, :bool], :int + attach_function :SnowboyDetectSetAudioGain, [:pointer, :float], :void + attach_function :SnowboyDetectSetSensitivity, [:pointer, :string], :void + end + + class Detector + attr_reader :resource, :model, :sensitivity, :audio_gain, :ptr + + # @param model [String, Array] path(s) pointing to the model file(s) + # @param sensitivity [Numeric, Array] the sensitivity level(s per hotword of all +models+) + # @param gain [Numeric] the gain level + # @param resource [String] path to the resource file + # + def initialize resource: nil, model: nil, sensitivity: 0.5, gain: 1 + model = value2str(model) + + @ptr = Lib::SnowboyDetectConstructor(resource, model) + + @resource = resource + @model = model + + self.sensitivity = sensitivity; + self.audio_gain = gain; + end + + # @param gain [Numeric] the gain level + def audio_gain= gain + @audio_gain = gain + + Lib::SnowboyDetectSetAudioGain(ptr, audio_gain) + end + + # @param lvl [Numeric, Array] specifying level(s per model) + # + def sensitivity= lvl + if @model.is_a?(Array) and !sensitivity.is_a?(Array) + v = lvl + lvl = [] + + n_hotwords.times do + lvl << v + end + end + + @sensitivity = o=value2str(lvl) + + Lib::SnowboyDetectSetSensitivity(ptr, o) + end + + # @return [Integer] sample rate + # + def sample_rate + Lib::SnowboyDetectSampleRate(ptr) + end + + # @return [Integer] bits per sample + # + def bits_per_sample + Lib::SnowboyDetectBitsPerSample(ptr) + end + + # @return [Integer] number of channels + # + def n_channels + Lib::SnowboyDetectNumChannels(ptr) + end + + # @return [Integer] number of hotwords + # + def n_hotwords + Lib::SnowboyDetectNumHotwords(ptr) + end + + def reset + Lib::SnowboyDetectReset(ptr) + end + + # @param bool [true, false] apply frontend + # + def apply_frontend bool + Lib::SnowboyDetectApplyFrontend(ptr, bool) + end + + def update_model + Lib::SnowboyDetectUpdateModel(ptr) + end + + # run detection on audio +data+ + # + # @param data [FFI::MemoryPointer] representing the audio data + # @param length [Integer] the data length + # @param is_end [true, false] defaults false + # + def run_detection data, length, is_end=false + Lib::SnowboyDetectRunDetection(ptr, data, length, is_end) + end + + private + def value2str obj + if obj.is_a?(String) + obj + elsif obj.is_a?(Array) + obj.map do |o| + o.to_s + end.join(",") + elsif obj.is_a?(Numeric) + obj.to_s + else + obj.to_s + end + end + end +end + diff --git a/swig/Ruby/lib/snowboy/capture/port-audio/port-audio-capture.rb b/swig/Ruby/lib/snowboy/capture/port-audio/port-audio-capture.rb new file mode 100644 index 00000000..07ce722a --- /dev/null +++ b/swig/Ruby/lib/snowboy/capture/port-audio/port-audio-capture.rb @@ -0,0 +1,68 @@ +module Snowboy + module Capture + class PortAudio + module Lib + extend FFI::Library + + ffi_lib File.expand_path(File.join(File.dirname(__FILE__), "..", '..', '..', '..', 'ext', 'capture', 'port-audio', 'port-audio-capture.so')) + + attach_function :rb_snowboy_port_audio_capture_start_audio_capturing, [:pointer, :int, :int, :int], :void + attach_function :rb_snowboy_port_audio_capture_stop_audio_capturing, [:pointer], :void + attach_function :rb_snowboy_port_audio_capture_load_audio_data, [:pointer], :int + attach_function :rb_snowboy_port_audio_capture_get_audio_data, [:pointer], :pointer + attach_function :rb_snowboy_port_audio_capture_new, [], :pointer + end + + attr_reader :ptr + def initialize + @ptr = Lib.rb_snowboy_port_audio_capture_new() + end + + def start_capture(rate, channels, bps) + Lib.rb_snowboy_port_audio_capture_start_audio_capturing(@ptr, rate, channels, bps) + end + + def stop_capture + @running = false + Lib.rb_snowboy_port_audio_capture_stop_audio_capturing(@ptr) + end + + def load_audio_data + Lib.rb_snowboy_port_audio_capture_load_audio_data(@ptr) + end + + def get_audio_data + Lib.rb_snowboy_port_audio_capture_get_audio_data(@ptr) + end + + attr_accessor :thread + def run(rate, channels, bps, &b) + raise ArgumentError.new("No Block passed") unless b + + @running = true + + start_capture(rate, channels, bps) + + @thread = Thread.new do + begin + while running? + if (len=load_audio_data) > 0 + b.call(get_audio_data, len) + end + end + + stop_capture + rescue => e + puts e + puts e.backtrace.join("\n") + stop_capture + end + end + end + + def running? + @running + end + end + end +end