audite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *~
2
+ ext/*/Makefile
3
+ ext/*/*.o
4
+ ext/*/*.bundle
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ Audite - a portable mp3 player in Ruby
2
+ ======================================
3
+
4
+ Audite is a portable ruby library for playing mp3 files built on
5
+ libmp123 and portaudio.
6
+
7
+ ## Features
8
+
9
+ * Nonblocking playback using native threads
10
+ * Realtime seeking
11
+ * Realtime events
12
+ * Progress information
13
+ * Simple level meter
14
+
15
+ ## Player
16
+
17
+ Audite ships an example player, just try `audite test.mp3`.
18
+
19
+ ## Example
20
+
21
+ ```
22
+ require 'audite'
23
+
24
+ player = Audite.new
25
+
26
+ player.events.on(:complete) do
27
+ exit
28
+ end
29
+
30
+ player.events.on(:level) do |level|
31
+ puts level
32
+ end
33
+
34
+ player.events.on(:position_change) do |pos|
35
+ puts pos
36
+ end
37
+
38
+ player.load('test.mp3')
39
+ player.start_stream
40
+ player.forward(20)
41
+
42
+ ```
43
+
44
+ ## OSX Install
45
+
46
+ ```
47
+ brew install portaudio
48
+ brew install mpg123
49
+
50
+ gem install audite
51
+ ```
data/audite.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "audite"
5
+ s.version = "0.1.0"
6
+ s.author = "Matthias Georgi"
7
+ s.email = "matti.georgi@gmail.com"
8
+ s.homepage = "http://georgi.github.com/audite"
9
+ s.summary = "Portable mp3 player"
10
+ s.description = "Portable mp3 player built on mpg123 and portaudio"
11
+
12
+ s.bindir = 'bin'
13
+ s.files = `git ls-files`.split("\n")
14
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
15
+ s.require_paths = ["lib"]
16
+
17
+ s.extra_rdoc_files = ["README.md"]
18
+
19
+ s.extensions << 'ext/mpg123/extconf.rb'
20
+ s.extensions << 'ext/portaudio/extconf.rb'
21
+ end
data/bin/audite ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'audite'
4
+ require 'curses'
5
+
6
+ player = Audite.new
7
+ player.load(ARGV[0])
8
+ player.start_stream
9
+
10
+ player.events.on(:complete) do
11
+ exit
12
+ end
13
+
14
+ player.events.on(:level) do |level|
15
+ p = (level * Curses.cols).ceil
16
+ Curses.setpos(2, 0)
17
+ Curses.addstr(">" * p + " " * (Curses.cols - p))
18
+ Curses.refresh
19
+ end
20
+
21
+ player.events.on(:position_change) do |pos|
22
+ p = ((pos / player.length_in_seconds) * Curses.cols).ceil
23
+ Curses.setpos(0, 0)
24
+ Curses.addstr("playing #{ARGV[0]} #{player.position.ceil} seconds of #{player.length_in_seconds.ceil} total")
25
+ Curses.setpos(1, 0)
26
+ Curses.addstr("#" * p + " " * (Curses.cols - p))
27
+ Curses.refresh
28
+ end
29
+
30
+ Curses.init_screen
31
+ Curses.noecho
32
+ Curses.stdscr.keypad(true)
33
+ Curses.clear
34
+
35
+ while c = Curses.getch
36
+ case c
37
+ when Curses::KEY_LEFT
38
+ player.rewind
39
+ when Curses::KEY_RIGHT
40
+ player.forward
41
+ when ' '
42
+ player.toggle
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ require 'mkmf'
2
+
3
+ unless have_header('mpg123.h')
4
+ puts "please install mpg123 headers"
5
+ exit
6
+ end
7
+
8
+ unless have_library('mpg123')
9
+ puts "please install mpg123 lib"
10
+ exit
11
+ end
12
+
13
+ create_makefile('mpg123')
@@ -0,0 +1,139 @@
1
+ #include <ruby.h>
2
+
3
+ #include <stdio.h>
4
+ #include <strings.h>
5
+ #include <mpg123.h>
6
+
7
+ void cleanup(mpg123_handle *mh)
8
+ {
9
+ mpg123_close(mh);
10
+ mpg123_delete(mh);
11
+ }
12
+
13
+ static VALUE rb_cMpg123;
14
+
15
+ VALUE rb_mpg123_new(VALUE klass, VALUE filename) {
16
+ int err = MPG123_OK;
17
+ mpg123_handle *mh;
18
+ VALUE mpg123;
19
+ long rate;
20
+ int channels, encoding;
21
+
22
+ Check_Type(filename, T_STRING);
23
+
24
+ if ((mh = mpg123_new(NULL, &err)) == NULL) {
25
+ rb_raise(rb_eStandardError, "%s", mpg123_plain_strerror(err));
26
+ }
27
+
28
+ mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_FORCE_FLOAT, 0.);
29
+
30
+ if (mpg123_open(mh, (char*) RSTRING_PTR(filename)) != MPG123_OK ||
31
+ mpg123_getformat(mh, &rate, &channels, &encoding) != MPG123_OK) {
32
+ rb_raise(rb_eStandardError, "%s", mpg123_strerror(mh));
33
+ }
34
+
35
+ if (encoding != MPG123_ENC_FLOAT_32) {
36
+ rb_raise(rb_eStandardError, "bad encoding");
37
+ }
38
+
39
+ mpg123_format_none(mh);
40
+ mpg123_format(mh, rate, channels, encoding);
41
+
42
+ return Data_Wrap_Struct(rb_cMpg123, 0, cleanup, mh);
43
+ }
44
+
45
+ VALUE rb_mpg123_close(VALUE self)
46
+ {
47
+ mpg123_close(DATA_PTR(self));
48
+ return self;
49
+ }
50
+
51
+
52
+ VALUE rb_mpg123_read(VALUE self, VALUE _size)
53
+ {
54
+ int size = FIX2INT(_size);
55
+ float *buffer = malloc(size * sizeof(float));
56
+ VALUE result = rb_ary_new2(size);
57
+ mpg123_handle *mh = NULL;
58
+ size_t done = 0;
59
+ int i;
60
+ int err = MPG123_OK;
61
+
62
+ Data_Get_Struct(self, mpg123_handle, mh);
63
+ err = mpg123_read(mh, (unsigned char *) buffer, size * sizeof(float), &done);
64
+
65
+ if (err == MPG123_OK || err == MPG123_DONE) {
66
+ for (i = 0; i < size; i++) {
67
+ rb_ary_store(result, i, rb_float_new(buffer[i]));
68
+ }
69
+ return result;
70
+ }
71
+ else {
72
+ rb_raise(rb_eStandardError, "%s", mpg123_plain_strerror(err));
73
+ }
74
+ }
75
+
76
+ VALUE rb_mpg123_length(VALUE self)
77
+ {
78
+ return INT2FIX(mpg123_length(DATA_PTR(self)));
79
+ }
80
+
81
+ VALUE rb_mpg123_spf(VALUE self)
82
+ {
83
+ return rb_float_new(mpg123_spf(DATA_PTR(self)));
84
+ }
85
+
86
+ VALUE rb_mpg123_tpf(VALUE self)
87
+ {
88
+ return rb_float_new(mpg123_tpf(DATA_PTR(self)));
89
+ }
90
+
91
+ VALUE rb_mpg123_tell(VALUE self)
92
+ {
93
+ return INT2FIX(mpg123_tell(DATA_PTR(self)));
94
+ }
95
+
96
+ VALUE rb_mpg123_tellframe(VALUE self)
97
+ {
98
+ return INT2FIX(mpg123_tellframe(DATA_PTR(self)));
99
+ }
100
+
101
+ VALUE rb_mpg123_seek(VALUE self, VALUE offset)
102
+ {
103
+ return INT2FIX(mpg123_seek(DATA_PTR(self), FIX2INT(offset), SEEK_SET));
104
+ }
105
+
106
+ VALUE rb_mpg123_seek_frame(VALUE self, VALUE offset)
107
+ {
108
+ return INT2FIX(mpg123_seek_frame(DATA_PTR(self), FIX2INT(offset), SEEK_SET));
109
+ }
110
+
111
+ VALUE rb_mpg123_timeframe(VALUE self, VALUE seconds)
112
+ {
113
+ return INT2FIX(mpg123_timeframe(DATA_PTR(self), NUM2DBL(seconds)));
114
+ }
115
+
116
+ void Init_mpg123(void) {
117
+ int err = MPG123_OK;
118
+
119
+ err = mpg123_init();
120
+
121
+ if (err != MPG123_OK) {
122
+ rb_raise(rb_eStandardError, "%s", mpg123_plain_strerror(err));
123
+ }
124
+
125
+ rb_cMpg123 = rb_define_class("Mpg123", rb_cObject);
126
+
127
+ rb_define_singleton_method(rb_cMpg123, "new", rb_mpg123_new, 1);
128
+
129
+ rb_define_method(rb_cMpg123, "close", rb_mpg123_close, 0);
130
+ rb_define_method(rb_cMpg123, "read", rb_mpg123_read, 1);
131
+ rb_define_method(rb_cMpg123, "length", rb_mpg123_length, 0);
132
+ rb_define_method(rb_cMpg123, "spf", rb_mpg123_spf, 0);
133
+ rb_define_method(rb_cMpg123, "tpf", rb_mpg123_tpf, 0);
134
+ rb_define_method(rb_cMpg123, "tell", rb_mpg123_tell, 0);
135
+ rb_define_method(rb_cMpg123, "tellframe", rb_mpg123_tellframe, 0);
136
+ rb_define_method(rb_cMpg123, "seek", rb_mpg123_seek, 1);
137
+ rb_define_method(rb_cMpg123, "seek_frame", rb_mpg123_seek_frame, 1);
138
+ rb_define_method(rb_cMpg123, "timeframe", rb_mpg123_timeframe, 1);
139
+ }
@@ -0,0 +1,13 @@
1
+ require 'mkmf'
2
+
3
+ unless have_header('portaudio.h')
4
+ puts "please install portaudio headers"
5
+ exit
6
+ end
7
+
8
+ unless have_library('portaudio')
9
+ puts "please install portaudio lib"
10
+ exit
11
+ end
12
+
13
+ create_makefile('portaudio')
@@ -0,0 +1,151 @@
1
+ #include <ruby.h>
2
+ #include <pthread.h>
3
+ #include <portaudio.h>
4
+
5
+ typedef struct {
6
+ PaStream *stream;
7
+ int size;
8
+ float *buffer;
9
+ pthread_mutex_t mutex;
10
+ pthread_cond_t cond;
11
+ } Portaudio;
12
+
13
+ static VALUE rb_cPortaudio;
14
+
15
+ void free_portaudio(void *ptr)
16
+ {
17
+ Portaudio *portaudio = (Portaudio *) ptr;
18
+
19
+ if (portaudio) {
20
+ pthread_cond_destroy(&portaudio->cond);
21
+ pthread_mutex_destroy(&portaudio->mutex);
22
+
23
+ if (portaudio->buffer) {
24
+ free(portaudio->buffer);
25
+ }
26
+
27
+ Pa_CloseStream(portaudio->stream);
28
+ }
29
+ }
30
+
31
+
32
+ static int paCallback(const void *inputBuffer,
33
+ void *outputBuffer,
34
+ unsigned long framesPerBuffer,
35
+ const PaStreamCallbackTimeInfo* timeInfo,
36
+ PaStreamCallbackFlags statusFlags,
37
+ void *userData )
38
+ {
39
+ Portaudio *portaudio = (Portaudio *) userData;
40
+ float *out = (float*) outputBuffer;
41
+ unsigned int i;
42
+
43
+ pthread_mutex_lock(&portaudio->mutex);
44
+
45
+ for (i = 0; i < framesPerBuffer * 2; i++) {
46
+ *out++ = portaudio->buffer[i];
47
+ }
48
+
49
+ pthread_mutex_unlock(&portaudio->mutex);
50
+ pthread_cond_signal(&portaudio->cond);
51
+
52
+ return 0;
53
+ }
54
+
55
+ VALUE rb_portaudio_new(VALUE klass)
56
+ {
57
+ PaError err;
58
+ VALUE self;
59
+ Portaudio *portaudio = (Portaudio *) malloc(sizeof(Portaudio));
60
+
61
+ pthread_mutex_init(&portaudio->mutex, NULL);
62
+ pthread_cond_init(&portaudio->cond, NULL);
63
+
64
+ portaudio->size = 4096 * 2;
65
+ portaudio->buffer = (float *) malloc(sizeof(float) * portaudio->size);
66
+
67
+ err = Pa_OpenDefaultStream(&portaudio->stream,
68
+ 0, /* no input channels */
69
+ 2, /* stereo output */
70
+ paFloat32, /* 32 bit floating point output */
71
+ 44100, /* 44100 sample rate*/
72
+ 4096, /* frames per buffer */
73
+ paCallback, /* this is your callback function */
74
+ (void*) portaudio);
75
+
76
+ if (err != paNoError) {
77
+ rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
78
+ }
79
+
80
+ self = Data_Wrap_Struct(rb_cPortaudio, 0, free_portaudio, portaudio);
81
+
82
+ return self;
83
+ }
84
+
85
+
86
+ VALUE portaudio_wait(void *ptr)
87
+ {
88
+ Portaudio *portaudio = (Portaudio *) ptr;
89
+
90
+ pthread_cond_wait(&portaudio->cond, &portaudio->mutex);
91
+
92
+ return Qnil;
93
+ }
94
+
95
+ VALUE rb_portaudio_write(VALUE self, VALUE buffer)
96
+ {
97
+ int i;
98
+ Portaudio *portaudio;
99
+ Data_Get_Struct(self, Portaudio, portaudio);
100
+
101
+ Check_Type(buffer, T_ARRAY);
102
+
103
+ for (i = 0; i < portaudio->size; i++) {
104
+ portaudio->buffer[i] = NUM2DBL(rb_ary_entry(buffer, i));
105
+ }
106
+
107
+ rb_thread_blocking_region(portaudio_wait, portaudio, RUBY_UBF_IO, NULL);
108
+
109
+ return self;
110
+ }
111
+
112
+ VALUE rb_portaudio_start(VALUE self)
113
+ {
114
+ Portaudio *portaudio;
115
+ Data_Get_Struct(self, Portaudio, portaudio);
116
+ int err = Pa_StartStream(portaudio->stream);
117
+
118
+ if (err != paNoError) {
119
+ rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
120
+ }
121
+
122
+ return self;
123
+ }
124
+
125
+ VALUE rb_portaudio_stop(VALUE self)
126
+ {
127
+ Portaudio *portaudio;
128
+ Data_Get_Struct(self, Portaudio, portaudio);
129
+ int err = Pa_StopStream(portaudio->stream);
130
+
131
+ if (err != paNoError) {
132
+ rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
133
+ }
134
+
135
+ return self;
136
+ }
137
+
138
+ void Init_portaudio(void) {
139
+ int err = Pa_Initialize();
140
+
141
+ if (err != paNoError) {
142
+ rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
143
+ }
144
+
145
+ rb_cPortaudio = rb_define_class("Portaudio", rb_cObject);
146
+
147
+ rb_define_singleton_method(rb_cPortaudio, "new", rb_portaudio_new, 0);
148
+ rb_define_method(rb_cPortaudio, "write", rb_portaudio_write, 1);
149
+ rb_define_method(rb_cPortaudio, "start", rb_portaudio_start, 0);
150
+ rb_define_method(rb_cPortaudio, "stop", rb_portaudio_stop, 0);
151
+ }
data/lib/audite.rb ADDED
@@ -0,0 +1,153 @@
1
+ require 'portaudio'
2
+ require 'mpg123'
3
+
4
+ class Audite
5
+ class Events
6
+ def initialize
7
+ @handlers = Hash.new {|h,k| h[k] = [] }
8
+ end
9
+
10
+ def on(event, &block)
11
+ @handlers[event] << block
12
+ end
13
+
14
+ def trigger(event, *args)
15
+ @handlers[event].each do |handler|
16
+ handler.call(*args)
17
+ end
18
+ end
19
+ end
20
+
21
+ attr_reader :events
22
+
23
+ def initialize
24
+ @events = Events.new
25
+ @stream = Portaudio.new
26
+ @thread = start_thread
27
+ end
28
+
29
+ def start_thread
30
+ Thread.start do
31
+ loop do
32
+ @stream.write(process(4096))
33
+ end
34
+ end
35
+ end
36
+
37
+ def process(samples)
38
+ if tell >= length
39
+ events.trigger(:complete)
40
+ stop_stream
41
+ (0..samples).map { 0 }
42
+
43
+ elsif slice = read(samples * 2)
44
+ events.trigger(:level, level(slice))
45
+ events.trigger(:position_change, position)
46
+
47
+ slice
48
+ else
49
+ (0..samples).map { 0 }
50
+ end
51
+
52
+ rescue => e
53
+ puts e.message
54
+ puts e.backtrace
55
+ end
56
+
57
+ def start_stream
58
+ unless @active
59
+ @active = true
60
+ @stream.start
61
+ end
62
+ end
63
+
64
+ def stop_stream
65
+ if @active
66
+ @active = false
67
+ @stream.stop
68
+ end
69
+ end
70
+
71
+ def toggle
72
+ if @active
73
+ stop_stream
74
+ else
75
+ start_stream
76
+ end
77
+ end
78
+
79
+ def load(file)
80
+ @mp3 = Mpg123.new(file)
81
+ end
82
+
83
+ def time_per_frame
84
+ @mp3.tpf
85
+ end
86
+
87
+ def samples_per_frame
88
+ @mp3.spf
89
+ end
90
+
91
+ def tell
92
+ @mp3.tell
93
+ end
94
+
95
+ def length
96
+ @mp3.length
97
+ end
98
+
99
+ def read(samples)
100
+ @mp3.read(samples)
101
+ end
102
+
103
+ def seconds_to_frames(seconds)
104
+ seconds / time_per_frame
105
+ end
106
+
107
+ def seconds_to_samples(seconds)
108
+ seconds_to_frames(seconds) * samples_per_frame
109
+ end
110
+
111
+ def samples_to_seconds(samples)
112
+ samples_to_frames(samples) * time_per_frame
113
+ end
114
+
115
+ def samples_to_frames(samples)
116
+ samples / samples_per_frame
117
+ end
118
+
119
+ def seek(seconds)
120
+ samples = seconds_to_samples(seconds)
121
+
122
+ if (0..length).include?(samples)
123
+ @mp3.seek(samples)
124
+ events.trigger(:position_change, position)
125
+ end
126
+ end
127
+
128
+ def position
129
+ samples_to_seconds(tell)
130
+ end
131
+
132
+ def length_in_seconds
133
+ samples_to_seconds(length)
134
+ end
135
+
136
+ def level(slice)
137
+ slice.inject(0) {|s, i| s + i.abs } / slice.size
138
+ end
139
+
140
+ def rewind(seconds = 1)
141
+ if @mp3
142
+ start_stream
143
+ seek(position - seconds)
144
+ end
145
+ end
146
+
147
+ def forward(seconds = 1)
148
+ if @mp3
149
+ start_stream
150
+ seek(position + seconds)
151
+ end
152
+ end
153
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: audite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matthias Georgi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-26 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Portable mp3 player built on mpg123 and portaudio
15
+ email: matti.georgi@gmail.com
16
+ executables:
17
+ - audite
18
+ extensions:
19
+ - ext/mpg123/extconf.rb
20
+ - ext/portaudio/extconf.rb
21
+ extra_rdoc_files:
22
+ - README.md
23
+ files:
24
+ - .gitignore
25
+ - README.md
26
+ - audite.gemspec
27
+ - bin/audite
28
+ - ext/mpg123/extconf.rb
29
+ - ext/mpg123/mpg123.c
30
+ - ext/portaudio/extconf.rb
31
+ - ext/portaudio/portaudio.c
32
+ - lib/audite.rb
33
+ homepage: http://georgi.github.com/audite
34
+ licenses: []
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 1.8.24
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: Portable mp3 player
57
+ test_files: []
58
+ has_rdoc: