audite-lib 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/LICENSE +7 -0
- data/README.md +71 -0
- data/Rakefile +1 -0
- data/audite.gemspec +23 -0
- data/ext/mpg123/extconf.rb +13 -0
- data/ext/mpg123/mpg123.c +157 -0
- data/ext/portaudio/extconf.rb +38 -0
- data/ext/portaudio/portaudio.c +283 -0
- data/lib/audite.rb +188 -0
- data/spec/30sec.mp3 +0 -0
- data/spec/audite_spec.rb +14 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 041f8e7065b49fc7d94f63647ac4c1f6d584f715
|
4
|
+
data.tar.gz: ccbf5a030a7523d61bf785ffa7387b9792dfcd0c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8ed5cc854d9472737ed0e27ca389e8be9c194480ed44b2458b84ba1f612a1af832114e15d090044b1a3a3f04206c31f02f2b402259780b035f82fe9d04770f4d
|
7
|
+
data.tar.gz: 16c86468704fb1ee735c024e5884e9bf63d8c3e7b729cfb79674daf4af41704ca59f8fd6ba988601f62f937c7813c4e04fd2d550dd37ba25e0fe8a704f7718dc
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013 Matthias Georgi
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
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
|
+
## Usage
|
16
|
+
|
17
|
+
Audite ships an example player:
|
18
|
+
|
19
|
+
```
|
20
|
+
audite test.mp3
|
21
|
+
```
|
22
|
+
|
23
|
+
## API Example
|
24
|
+
|
25
|
+
```
|
26
|
+
require 'audite'
|
27
|
+
|
28
|
+
player = Audite.new
|
29
|
+
|
30
|
+
player.events.on(:complete) do
|
31
|
+
puts "COMPLETE"
|
32
|
+
end
|
33
|
+
|
34
|
+
player.events.on(:position_change) do |pos|
|
35
|
+
puts "POSITION: #{pos} seconds level #{player.level}"
|
36
|
+
end
|
37
|
+
|
38
|
+
player.load('test.mp3')
|
39
|
+
player.start_stream
|
40
|
+
player.forward(20)
|
41
|
+
player.thread.join
|
42
|
+
|
43
|
+
```
|
44
|
+
|
45
|
+
## Requirements
|
46
|
+
|
47
|
+
* Portaudio >= 19
|
48
|
+
* Mpg123 >= 1.14
|
49
|
+
|
50
|
+
## OSX Install
|
51
|
+
|
52
|
+
```
|
53
|
+
brew install portaudio
|
54
|
+
brew install mpg123
|
55
|
+
|
56
|
+
gem install audite
|
57
|
+
```
|
58
|
+
|
59
|
+
|
60
|
+
## Debian / Ubuntu Install
|
61
|
+
```
|
62
|
+
apt-get install libjack0 libjack-dev
|
63
|
+
apt-get install libportaudiocpp0 portaudio19-dev libmpg123-dev
|
64
|
+
gem install audite
|
65
|
+
```
|
66
|
+
|
67
|
+
## Soundcloud2000
|
68
|
+
|
69
|
+
Audite provides the audio engine for [Soundcloud2000][1]
|
70
|
+
|
71
|
+
[1]: https://github.com/grobie/soundcloud2000
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/audite.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "audite-lib"
|
5
|
+
s.version = "0.4.1"
|
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.files = `git ls-files`.split("\n")
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
|
15
|
+
s.add_development_dependency "bundler", "~> 1.3"
|
16
|
+
s.add_development_dependency "rake"
|
17
|
+
s.add_development_dependency "rspec"
|
18
|
+
|
19
|
+
s.extra_rdoc_files = ["README.md"]
|
20
|
+
|
21
|
+
s.extensions << 'ext/mpg123/extconf.rb'
|
22
|
+
s.extensions << 'ext/portaudio/extconf.rb'
|
23
|
+
end
|
data/ext/mpg123/mpg123.c
ADDED
@@ -0,0 +1,157 @@
|
|
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
|
+
VALUE new_mpg123 = Data_Wrap_Struct(rb_cMpg123, 0, cleanup, mh);
|
43
|
+
rb_iv_set(new_mpg123, "@file", filename);
|
44
|
+
return new_mpg123;
|
45
|
+
}
|
46
|
+
|
47
|
+
static VALUE rb_mpg123_file(VALUE self, VALUE obj)
|
48
|
+
{
|
49
|
+
return rb_iv_get(self, "@file");
|
50
|
+
}
|
51
|
+
|
52
|
+
VALUE rb_mpg123_close(VALUE self)
|
53
|
+
{
|
54
|
+
mpg123_close(DATA_PTR(self));
|
55
|
+
return self;
|
56
|
+
}
|
57
|
+
|
58
|
+
VALUE rb_mpg123_read(VALUE self, VALUE _size)
|
59
|
+
{
|
60
|
+
int size = FIX2INT(_size);
|
61
|
+
float *buffer = malloc(size * sizeof(float));
|
62
|
+
VALUE result = rb_ary_new2(size);
|
63
|
+
mpg123_handle *mh = NULL;
|
64
|
+
size_t done = 0;
|
65
|
+
int i;
|
66
|
+
int err = MPG123_OK;
|
67
|
+
|
68
|
+
Data_Get_Struct(self, mpg123_handle, mh);
|
69
|
+
err = mpg123_read(mh, (unsigned char *) buffer, size * sizeof(float), &done);
|
70
|
+
|
71
|
+
if (err == MPG123_OK || err == MPG123_DONE) {
|
72
|
+
for (i = 0; i < size; i++) {
|
73
|
+
rb_ary_store(result, i, rb_float_new(buffer[i]));
|
74
|
+
}
|
75
|
+
free(buffer);
|
76
|
+
return result;
|
77
|
+
}
|
78
|
+
else {
|
79
|
+
free(buffer);
|
80
|
+
rb_raise(rb_eStandardError, "%s", mpg123_plain_strerror(err));
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
VALUE rb_mpg123_length(VALUE self)
|
85
|
+
{
|
86
|
+
/*
|
87
|
+
* mpg123_length() only returns an estimated duration
|
88
|
+
* if the song hasn't previously been scanned.
|
89
|
+
* This can be incorrect if, for example, the song is corrupted
|
90
|
+
* and cannot be played after a certain point.
|
91
|
+
* Run mpg123_scan() first to get an accurate length reading.
|
92
|
+
*/
|
93
|
+
mpg123_scan(DATA_PTR(self));
|
94
|
+
return INT2FIX(mpg123_length(DATA_PTR(self)));
|
95
|
+
}
|
96
|
+
|
97
|
+
VALUE rb_mpg123_spf(VALUE self)
|
98
|
+
{
|
99
|
+
return rb_float_new(mpg123_spf(DATA_PTR(self)));
|
100
|
+
}
|
101
|
+
|
102
|
+
VALUE rb_mpg123_tpf(VALUE self)
|
103
|
+
{
|
104
|
+
return rb_float_new(mpg123_tpf(DATA_PTR(self)));
|
105
|
+
}
|
106
|
+
|
107
|
+
VALUE rb_mpg123_tell(VALUE self)
|
108
|
+
{
|
109
|
+
return INT2FIX(mpg123_tell(DATA_PTR(self)));
|
110
|
+
}
|
111
|
+
|
112
|
+
VALUE rb_mpg123_tellframe(VALUE self)
|
113
|
+
{
|
114
|
+
return INT2FIX(mpg123_tellframe(DATA_PTR(self)));
|
115
|
+
}
|
116
|
+
|
117
|
+
VALUE rb_mpg123_seek(VALUE self, VALUE offset)
|
118
|
+
{
|
119
|
+
return INT2FIX(mpg123_seek(DATA_PTR(self), FIX2INT(offset), SEEK_SET));
|
120
|
+
}
|
121
|
+
|
122
|
+
VALUE rb_mpg123_seek_frame(VALUE self, VALUE offset)
|
123
|
+
{
|
124
|
+
return INT2FIX(mpg123_seek_frame(DATA_PTR(self), FIX2INT(offset), SEEK_SET));
|
125
|
+
}
|
126
|
+
|
127
|
+
VALUE rb_mpg123_timeframe(VALUE self, VALUE seconds)
|
128
|
+
{
|
129
|
+
return INT2FIX(mpg123_timeframe(DATA_PTR(self), NUM2DBL(seconds)));
|
130
|
+
}
|
131
|
+
|
132
|
+
void Init_mpg123(void) {
|
133
|
+
int err = MPG123_OK;
|
134
|
+
|
135
|
+
err = mpg123_init();
|
136
|
+
|
137
|
+
if (err != MPG123_OK) {
|
138
|
+
rb_raise(rb_eStandardError, "%s", mpg123_plain_strerror(err));
|
139
|
+
}
|
140
|
+
|
141
|
+
rb_cMpg123 = rb_define_class("Mpg123", rb_cObject);
|
142
|
+
|
143
|
+
rb_define_singleton_method(rb_cMpg123, "new", rb_mpg123_new, 1);
|
144
|
+
|
145
|
+
rb_define_method(rb_cMpg123, "file", rb_mpg123_file, 0);
|
146
|
+
|
147
|
+
rb_define_method(rb_cMpg123, "close", rb_mpg123_close, 0);
|
148
|
+
rb_define_method(rb_cMpg123, "read", rb_mpg123_read, 1);
|
149
|
+
rb_define_method(rb_cMpg123, "length", rb_mpg123_length, 0);
|
150
|
+
rb_define_method(rb_cMpg123, "spf", rb_mpg123_spf, 0);
|
151
|
+
rb_define_method(rb_cMpg123, "tpf", rb_mpg123_tpf, 0);
|
152
|
+
rb_define_method(rb_cMpg123, "tell", rb_mpg123_tell, 0);
|
153
|
+
rb_define_method(rb_cMpg123, "tellframe", rb_mpg123_tellframe, 0);
|
154
|
+
rb_define_method(rb_cMpg123, "seek", rb_mpg123_seek, 1);
|
155
|
+
rb_define_method(rb_cMpg123, "seek_frame", rb_mpg123_seek_frame, 1);
|
156
|
+
rb_define_method(rb_cMpg123, "timeframe", rb_mpg123_timeframe, 1);
|
157
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
|
3
|
+
unless have_header('portaudio.h')
|
4
|
+
puts "portaudio.h not found"
|
5
|
+
puts "please brew install portaudio"
|
6
|
+
puts "or apt-get install portaudio19-dev"
|
7
|
+
exit
|
8
|
+
end
|
9
|
+
|
10
|
+
unless have_header('mpg123.h')
|
11
|
+
puts "mpg123.h not found"
|
12
|
+
puts "please brew install mpg123"
|
13
|
+
puts "or apt-get install libmpg123-dev"
|
14
|
+
exit
|
15
|
+
end
|
16
|
+
|
17
|
+
unless have_library('portaudio')
|
18
|
+
puts "lib portaudio not found"
|
19
|
+
puts "brew install portaudio"
|
20
|
+
puts "or apt-get install portaudio19-dev"
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
|
24
|
+
unless have_library('mpg123')
|
25
|
+
puts "lib mpg123 not found"
|
26
|
+
puts "please brew install mpg123"
|
27
|
+
puts "or apt-get install libmpg123-0"
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
|
31
|
+
unless have_type('PaStreamCallbackTimeInfo', 'portaudio.h')
|
32
|
+
puts "portaudio19 not found"
|
33
|
+
puts "brew install portaudio"
|
34
|
+
puts "or apt-get install portaudio19-dev"
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
create_makefile('portaudio')
|
@@ -0,0 +1,283 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <math.h>
|
3
|
+
#include <pthread.h>
|
4
|
+
#include <portaudio.h>
|
5
|
+
#include <mpg123.h>
|
6
|
+
#include <string.h>
|
7
|
+
|
8
|
+
typedef struct {
|
9
|
+
PaStream *stream;
|
10
|
+
int size;
|
11
|
+
float *buffer;
|
12
|
+
float rms;
|
13
|
+
pthread_mutex_t mutex;
|
14
|
+
pthread_cond_t cond;
|
15
|
+
} Portaudio;
|
16
|
+
|
17
|
+
static VALUE rb_cPortaudio;
|
18
|
+
|
19
|
+
void free_portaudio(void *ptr)
|
20
|
+
{
|
21
|
+
Portaudio *portaudio = (Portaudio *) ptr;
|
22
|
+
|
23
|
+
if (portaudio) {
|
24
|
+
pthread_cond_destroy(&portaudio->cond);
|
25
|
+
pthread_mutex_destroy(&portaudio->mutex);
|
26
|
+
|
27
|
+
if (portaudio->buffer) {
|
28
|
+
free(portaudio->buffer);
|
29
|
+
}
|
30
|
+
|
31
|
+
Pa_CloseStream(portaudio->stream);
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
static int paCallback(const void *inputBuffer,
|
36
|
+
void *outputBuffer,
|
37
|
+
unsigned long framesPerBuffer,
|
38
|
+
const PaStreamCallbackTimeInfo* timeInfo,
|
39
|
+
PaStreamCallbackFlags statusFlags,
|
40
|
+
void *userData )
|
41
|
+
{
|
42
|
+
Portaudio *portaudio = (Portaudio *) userData;
|
43
|
+
float *out = (float*) outputBuffer;
|
44
|
+
unsigned int i;
|
45
|
+
|
46
|
+
pthread_mutex_lock(&portaudio->mutex);
|
47
|
+
|
48
|
+
memcpy(out, portaudio->buffer, sizeof(float) * portaudio->size);
|
49
|
+
|
50
|
+
pthread_cond_broadcast(&portaudio->cond);
|
51
|
+
pthread_mutex_unlock(&portaudio->mutex);
|
52
|
+
|
53
|
+
return 0;
|
54
|
+
}
|
55
|
+
|
56
|
+
VALUE rb_portaudio_new(VALUE klass, VALUE framesPerBuffer, VALUE deviceName)
|
57
|
+
{
|
58
|
+
PaStreamParameters outputParameters;
|
59
|
+
const PaDeviceInfo *deviceInfo;
|
60
|
+
PaError err;
|
61
|
+
int device, numDevices, foundDevice = -1;
|
62
|
+
VALUE self;
|
63
|
+
Portaudio *portaudio = (Portaudio *) malloc(sizeof(Portaudio));
|
64
|
+
|
65
|
+
pthread_mutex_init(&portaudio->mutex, NULL);
|
66
|
+
pthread_cond_init(&portaudio->cond, NULL);
|
67
|
+
|
68
|
+
portaudio->size = FIX2INT(framesPerBuffer) * 2;
|
69
|
+
portaudio->buffer = (float *) malloc(sizeof(float) * portaudio->size);
|
70
|
+
numDevices = Pa_GetDeviceCount();
|
71
|
+
if( numDevices < 0 ) {
|
72
|
+
printf( "ERROR: Pa_CountDevices returned 0x%x\n", numDevices );
|
73
|
+
err = numDevices;
|
74
|
+
}
|
75
|
+
|
76
|
+
for ( device=0; device<numDevices; device++ ) {
|
77
|
+
deviceInfo = Pa_GetDeviceInfo( device );
|
78
|
+
if (strcmp(deviceInfo->name, StringValueCStr(deviceName)) == 0) {
|
79
|
+
foundDevice = device;
|
80
|
+
break;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
if (foundDevice == -1) {
|
85
|
+
err = Pa_OpenDefaultStream(&portaudio->stream,
|
86
|
+
0, /* no input channels */
|
87
|
+
2, /* stereo output */
|
88
|
+
paFloat32, /* 32 bit floating point output */
|
89
|
+
44100, /* sample rate*/
|
90
|
+
FIX2INT(framesPerBuffer),
|
91
|
+
paCallback,
|
92
|
+
(void*) portaudio);
|
93
|
+
} else {
|
94
|
+
bzero( &outputParameters, sizeof( outputParameters ) );
|
95
|
+
outputParameters.device = foundDevice;
|
96
|
+
outputParameters.channelCount = 2;
|
97
|
+
outputParameters.sampleFormat = paFloat32;
|
98
|
+
outputParameters.suggestedLatency = Pa_GetDeviceInfo(foundDevice)->defaultLowOutputLatency ;
|
99
|
+
|
100
|
+
err = Pa_OpenStream(&portaudio->stream,
|
101
|
+
NULL,
|
102
|
+
&outputParameters,
|
103
|
+
44100,
|
104
|
+
FIX2INT(framesPerBuffer),
|
105
|
+
paNoFlag,
|
106
|
+
paCallback,
|
107
|
+
(void*) portaudio);
|
108
|
+
}
|
109
|
+
|
110
|
+
if (err != paNoError) {
|
111
|
+
rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
|
112
|
+
}
|
113
|
+
|
114
|
+
self = Data_Wrap_Struct(rb_cPortaudio, 0, free_portaudio, portaudio);
|
115
|
+
|
116
|
+
return self;
|
117
|
+
}
|
118
|
+
|
119
|
+
|
120
|
+
VALUE portaudio_wait(void *ptr)
|
121
|
+
{
|
122
|
+
Portaudio *portaudio = (Portaudio *) ptr;
|
123
|
+
|
124
|
+
pthread_cond_wait(&portaudio->cond, &portaudio->mutex);
|
125
|
+
|
126
|
+
return Qnil;
|
127
|
+
}
|
128
|
+
|
129
|
+
float rms(float *v, int n)
|
130
|
+
{
|
131
|
+
int i;
|
132
|
+
float sum = 0.0;
|
133
|
+
|
134
|
+
for (i = 0; i < n; i++) {
|
135
|
+
sum += v[i] * v[i];
|
136
|
+
}
|
137
|
+
|
138
|
+
return sqrt(sum / n);
|
139
|
+
}
|
140
|
+
|
141
|
+
VALUE rb_portaudio_write_from_mpg(VALUE self, VALUE mpg)
|
142
|
+
{
|
143
|
+
int err;
|
144
|
+
size_t done = 0;
|
145
|
+
Portaudio *portaudio;
|
146
|
+
mpg123_handle *mh = NULL;
|
147
|
+
|
148
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
149
|
+
Data_Get_Struct(mpg, mpg123_handle, mh);
|
150
|
+
|
151
|
+
err = mpg123_read(mh, (unsigned char *) portaudio->buffer, portaudio->size * sizeof(float), &done);
|
152
|
+
|
153
|
+
portaudio->rms = rms(portaudio->buffer, portaudio->size);
|
154
|
+
|
155
|
+
switch (err) {
|
156
|
+
case MPG123_OK: return ID2SYM(rb_intern("ok"));
|
157
|
+
case MPG123_DONE: return ID2SYM(rb_intern("done"));
|
158
|
+
case MPG123_NEED_MORE: return ID2SYM(rb_intern("need_more"));
|
159
|
+
}
|
160
|
+
|
161
|
+
rb_raise(rb_eStandardError, "%s", mpg123_plain_strerror(err));
|
162
|
+
}
|
163
|
+
|
164
|
+
VALUE rb_portaudio_wait(VALUE self)
|
165
|
+
{
|
166
|
+
Portaudio *portaudio;
|
167
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
168
|
+
|
169
|
+
#ifdef RUBY_UBF_IO
|
170
|
+
rb_thread_call_without_gvl(portaudio_wait, portaudio, RUBY_UBF_IO, NULL);
|
171
|
+
#else
|
172
|
+
portaudio_wait(portaudio);
|
173
|
+
#endif
|
174
|
+
return self;
|
175
|
+
}
|
176
|
+
|
177
|
+
VALUE rb_portaudio_write(VALUE self, VALUE buffer)
|
178
|
+
{
|
179
|
+
int i, len;
|
180
|
+
Portaudio *portaudio;
|
181
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
182
|
+
|
183
|
+
Check_Type(buffer, T_ARRAY);
|
184
|
+
|
185
|
+
len = RARRAY_LEN(buffer);
|
186
|
+
|
187
|
+
if (len != portaudio->size) {
|
188
|
+
rb_raise(rb_eStandardError, "array length does not match buffer size: %d != %d", len, portaudio->size);
|
189
|
+
}
|
190
|
+
|
191
|
+
for (i = 0; i < len; i++) {
|
192
|
+
portaudio->buffer[i] = NUM2DBL(rb_ary_entry(buffer, i));
|
193
|
+
}
|
194
|
+
|
195
|
+
return self;
|
196
|
+
}
|
197
|
+
|
198
|
+
VALUE rb_portaudio_rms(VALUE self)
|
199
|
+
{
|
200
|
+
Portaudio *portaudio;
|
201
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
202
|
+
return rb_float_new(portaudio->rms);
|
203
|
+
}
|
204
|
+
|
205
|
+
VALUE rb_portaudio_start(VALUE self)
|
206
|
+
{
|
207
|
+
Portaudio *portaudio;
|
208
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
209
|
+
int err = Pa_StartStream(portaudio->stream);
|
210
|
+
|
211
|
+
pthread_cond_broadcast(&portaudio->cond);
|
212
|
+
|
213
|
+
if (err != paNoError) {
|
214
|
+
rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
|
215
|
+
}
|
216
|
+
|
217
|
+
return self;
|
218
|
+
}
|
219
|
+
|
220
|
+
VALUE rb_portaudio_stop(VALUE self)
|
221
|
+
{
|
222
|
+
Portaudio *portaudio;
|
223
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
224
|
+
int err = Pa_StopStream(portaudio->stream);
|
225
|
+
|
226
|
+
pthread_cond_broadcast(&portaudio->cond);
|
227
|
+
|
228
|
+
if (err != paNoError) {
|
229
|
+
rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
|
230
|
+
}
|
231
|
+
|
232
|
+
return self;
|
233
|
+
}
|
234
|
+
|
235
|
+
VALUE rb_portaudio_stream_stopped(VALUE self)
|
236
|
+
{
|
237
|
+
Portaudio *portaudio;
|
238
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
239
|
+
int err = Pa_IsStreamStopped(portaudio->stream);
|
240
|
+
|
241
|
+
if (err == 1) {
|
242
|
+
return Qtrue;
|
243
|
+
} else if (err == 0) {
|
244
|
+
return Qfalse;
|
245
|
+
}
|
246
|
+
|
247
|
+
rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
|
248
|
+
}
|
249
|
+
|
250
|
+
VALUE rb_portaudio_close(VALUE self)
|
251
|
+
{
|
252
|
+
Portaudio *portaudio;
|
253
|
+
Data_Get_Struct(self, Portaudio, portaudio);
|
254
|
+
int err = Pa_CloseStream(portaudio->stream);
|
255
|
+
|
256
|
+
pthread_cond_broadcast(&portaudio->cond);
|
257
|
+
|
258
|
+
if (err != paNoError) {
|
259
|
+
rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
|
260
|
+
}
|
261
|
+
|
262
|
+
return self;
|
263
|
+
}
|
264
|
+
|
265
|
+
void Init_portaudio(void) {
|
266
|
+
int err = Pa_Initialize();
|
267
|
+
|
268
|
+
if (err != paNoError) {
|
269
|
+
rb_raise(rb_eStandardError, "%s", Pa_GetErrorText(err));
|
270
|
+
}
|
271
|
+
|
272
|
+
rb_cPortaudio = rb_define_class("Portaudio", rb_cObject);
|
273
|
+
|
274
|
+
rb_define_singleton_method(rb_cPortaudio, "new", rb_portaudio_new, 2);
|
275
|
+
rb_define_method(rb_cPortaudio, "wait", rb_portaudio_wait, 0);
|
276
|
+
rb_define_method(rb_cPortaudio, "stopped?", rb_portaudio_stream_stopped, 0);
|
277
|
+
rb_define_method(rb_cPortaudio, "close", rb_portaudio_close, 0);
|
278
|
+
rb_define_method(rb_cPortaudio, "rms", rb_portaudio_rms, 0);
|
279
|
+
rb_define_method(rb_cPortaudio, "write", rb_portaudio_write, 1);
|
280
|
+
rb_define_method(rb_cPortaudio, "write_from_mpg", rb_portaudio_write_from_mpg, 1);
|
281
|
+
rb_define_method(rb_cPortaudio, "start", rb_portaudio_start, 0);
|
282
|
+
rb_define_method(rb_cPortaudio, "stop", rb_portaudio_stop, 0);
|
283
|
+
}
|
data/lib/audite.rb
ADDED
@@ -0,0 +1,188 @@
|
|
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, :active, :stream, :mp3, :thread, :file, :song_list
|
22
|
+
|
23
|
+
def initialize(buffer_size = 2**12, device_name = '')
|
24
|
+
@buffer_size = buffer_size
|
25
|
+
@device_name = device_name
|
26
|
+
@events = Events.new
|
27
|
+
@stream = Portaudio.new(@buffer_size, @device_name)
|
28
|
+
@song_list = []
|
29
|
+
end
|
30
|
+
|
31
|
+
def start_thread
|
32
|
+
@thread ||= Thread.start do
|
33
|
+
loop do
|
34
|
+
process @stream.write_from_mpg(@mp3)
|
35
|
+
@stream.wait
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def level
|
41
|
+
@stream.rms
|
42
|
+
end
|
43
|
+
|
44
|
+
def process(status)
|
45
|
+
if [:done, :need_more].include? status
|
46
|
+
request_next_song
|
47
|
+
events.trigger(:complete)
|
48
|
+
else
|
49
|
+
events.trigger(:position_change, position)
|
50
|
+
end
|
51
|
+
|
52
|
+
rescue => e
|
53
|
+
$stderr.puts e.message
|
54
|
+
$stderr.puts e.backtrace
|
55
|
+
end
|
56
|
+
|
57
|
+
def current_song_name
|
58
|
+
File.basename mp3.file
|
59
|
+
end
|
60
|
+
|
61
|
+
def request_next_song
|
62
|
+
if songs_in_queue?
|
63
|
+
set_current_song
|
64
|
+
start_stream
|
65
|
+
else
|
66
|
+
stop_stream
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def close
|
71
|
+
stream.close
|
72
|
+
exit
|
73
|
+
end
|
74
|
+
|
75
|
+
def start_stream
|
76
|
+
unless @active || !song_loaded?
|
77
|
+
@active = true
|
78
|
+
@stream.start
|
79
|
+
start_thread
|
80
|
+
events.trigger(:toggle, @active)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def stop_stream
|
85
|
+
if @active
|
86
|
+
@active = false
|
87
|
+
@thread = nil unless @thread.alive?
|
88
|
+
unless @stream.stopped?
|
89
|
+
@stream.stop
|
90
|
+
events.trigger(:toggle, @active)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def toggle
|
96
|
+
if @active
|
97
|
+
stop_stream
|
98
|
+
else
|
99
|
+
start_stream
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def load(files)
|
104
|
+
files = [] << files unless Array === files
|
105
|
+
files.each {|file| queue file }
|
106
|
+
set_current_song
|
107
|
+
end
|
108
|
+
|
109
|
+
def song_loaded?
|
110
|
+
!@mp3.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
def set_current_song
|
114
|
+
@mp3 = song_list.shift
|
115
|
+
start_thread
|
116
|
+
end
|
117
|
+
|
118
|
+
def queue song
|
119
|
+
@song_list << Mpg123.new(song)
|
120
|
+
end
|
121
|
+
|
122
|
+
def queued_songs
|
123
|
+
@song_list.map {|s| File.basename s.file }
|
124
|
+
end
|
125
|
+
|
126
|
+
def songs_in_queue?
|
127
|
+
!@song_list.empty?
|
128
|
+
end
|
129
|
+
|
130
|
+
def time_per_frame
|
131
|
+
@mp3.tpf
|
132
|
+
end
|
133
|
+
|
134
|
+
def samples_per_frame
|
135
|
+
@mp3.spf
|
136
|
+
end
|
137
|
+
|
138
|
+
def tell
|
139
|
+
@mp3 ? @mp3.tell : 0
|
140
|
+
end
|
141
|
+
|
142
|
+
def length
|
143
|
+
@mp3 ? @mp3.length : 0
|
144
|
+
end
|
145
|
+
|
146
|
+
def seconds_to_frames(seconds)
|
147
|
+
seconds / time_per_frame
|
148
|
+
end
|
149
|
+
|
150
|
+
def seconds_to_samples(seconds)
|
151
|
+
seconds_to_frames(seconds) * samples_per_frame
|
152
|
+
end
|
153
|
+
|
154
|
+
def samples_to_seconds(samples)
|
155
|
+
samples_to_frames(samples) * time_per_frame
|
156
|
+
end
|
157
|
+
|
158
|
+
def samples_to_frames(samples)
|
159
|
+
samples / samples_per_frame
|
160
|
+
end
|
161
|
+
|
162
|
+
def seek(seconds)
|
163
|
+
if @mp3
|
164
|
+
samples = seconds_to_samples(seconds)
|
165
|
+
|
166
|
+
if (0..length).include?(samples)
|
167
|
+
@mp3.seek(samples)
|
168
|
+
events.trigger(:position_change, position)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def position
|
174
|
+
samples_to_seconds(tell)
|
175
|
+
end
|
176
|
+
|
177
|
+
def length_in_seconds
|
178
|
+
samples_to_seconds(length)
|
179
|
+
end
|
180
|
+
|
181
|
+
def rewind(seconds = 2)
|
182
|
+
seek(position - seconds)
|
183
|
+
end
|
184
|
+
|
185
|
+
def forward(seconds = 2)
|
186
|
+
seek(position + seconds)
|
187
|
+
end
|
188
|
+
end
|
data/spec/30sec.mp3
ADDED
Binary file
|
data/spec/audite_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'audite'
|
2
|
+
|
3
|
+
describe Audite do
|
4
|
+
it "loads and reports on an mp3" do
|
5
|
+
player = Audite.new
|
6
|
+
player.load(File.dirname(__FILE__)+'/30sec.mp3')
|
7
|
+
player.length.should eq(1326351)
|
8
|
+
player.length_in_seconds.should eq(30.075986394557827)
|
9
|
+
end
|
10
|
+
it "generates an error on a non-existant mp3" do
|
11
|
+
player = Audite.new
|
12
|
+
expect {player.load('/tmp/file.mp3')}.to raise_error
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: audite-lib
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthias Georgi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-08-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Portable mp3 player built on mpg123 and portaudio
|
56
|
+
email: matti.georgi@gmail.com
|
57
|
+
executables: []
|
58
|
+
extensions:
|
59
|
+
- ext/mpg123/extconf.rb
|
60
|
+
- ext/portaudio/extconf.rb
|
61
|
+
extra_rdoc_files:
|
62
|
+
- README.md
|
63
|
+
files:
|
64
|
+
- ".gitignore"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- audite.gemspec
|
70
|
+
- ext/mpg123/extconf.rb
|
71
|
+
- ext/mpg123/mpg123.c
|
72
|
+
- ext/portaudio/extconf.rb
|
73
|
+
- ext/portaudio/portaudio.c
|
74
|
+
- lib/audite.rb
|
75
|
+
- spec/30sec.mp3
|
76
|
+
- spec/audite_spec.rb
|
77
|
+
homepage: http://georgi.github.com/audite
|
78
|
+
licenses: []
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 2.5.1
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: Portable mp3 player
|
100
|
+
test_files: []
|