audite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: