beeps 0.4.0 → 0.5.1
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.
- checksums.yaml +4 -4
- data/.doc/ext/beeps/analyser.cpp +1 -1
- data/.doc/ext/beeps/envelope.cpp +1 -1
- data/.doc/ext/beeps/file_in.cpp +1 -1
- data/.doc/ext/beeps/gain.cpp +1 -1
- data/.doc/ext/beeps/high_pass.cpp +1 -1
- data/.doc/ext/beeps/low_pass.cpp +1 -1
- data/.doc/ext/beeps/mic_in.cpp +1 -1
- data/.doc/ext/beeps/mixer.cpp +1 -1
- data/.doc/ext/beeps/oscillator.cpp +2 -2
- data/.doc/ext/beeps/pitch_shift.cpp +1 -1
- data/.doc/ext/beeps/processor.cpp +1 -1
- data/.doc/ext/beeps/reverb.cpp +1 -1
- data/.doc/ext/beeps/sequencer.cpp +1 -1
- data/.doc/ext/beeps/signals.cpp +1 -1
- data/.doc/ext/beeps/sound.cpp +6 -6
- data/.doc/ext/beeps/sound_player.cpp +5 -5
- data/.doc/ext/beeps/text_in.cpp +1 -1
- data/.doc/ext/beeps/time_stretch.cpp +1 -1
- data/.doc/ext/beeps/value.cpp +1 -1
- data/.github/workflows/release-gem.yml +2 -49
- data/.github/workflows/tag.yml +5 -28
- data/.github/workflows/test.yml +1 -37
- data/ChangeLog.md +21 -0
- data/Gemfile.lock +13 -8
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/beeps.gemspec +3 -3
- data/ext/beeps/analyser.cpp +1 -1
- data/ext/beeps/envelope.cpp +1 -1
- data/ext/beeps/file_in.cpp +1 -1
- data/ext/beeps/gain.cpp +1 -1
- data/ext/beeps/high_pass.cpp +1 -1
- data/ext/beeps/low_pass.cpp +1 -1
- data/ext/beeps/mic_in.cpp +1 -1
- data/ext/beeps/mixer.cpp +1 -1
- data/ext/beeps/oscillator.cpp +2 -2
- data/ext/beeps/pitch_shift.cpp +1 -1
- data/ext/beeps/processor.cpp +1 -1
- data/ext/beeps/reverb.cpp +1 -1
- data/ext/beeps/sequencer.cpp +1 -1
- data/ext/beeps/signals.cpp +1 -1
- data/ext/beeps/sound.cpp +6 -6
- data/ext/beeps/sound_player.cpp +5 -5
- data/ext/beeps/text_in.cpp +1 -1
- data/ext/beeps/time_stretch.cpp +1 -1
- data/ext/beeps/value.cpp +1 -1
- data/include/beeps/signals.h +2 -2
- data/include/beeps/sound.h +3 -3
- data/lib/beeps/beeps.rb +4 -4
- data/lib/beeps/sound.rb +1 -1
- data/src/mic_in.cpp +10 -9
- data/src/openal.cpp +66 -8
- data/src/openal.h +5 -1
- data/src/oscillator.cpp +1 -1
- data/src/signals.cpp +6 -6
- data/src/sound.cpp +347 -152
- data/src/win32/beeps.cpp +25 -2
- data/src/win32/signals.cpp +102 -21
- data/test/test_file_in.rb +38 -0
- data/test/test_sound.rb +6 -6
- data/test/test_sound_player.rb +86 -20
- metadata +7 -8
- data/.github/workflows/utils.rb +0 -127
data/src/win32/beeps.cpp
CHANGED
|
@@ -10,12 +10,27 @@ namespace Beeps
|
|
|
10
10
|
{
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
namespace global
|
|
14
|
+
{
|
|
15
|
+
|
|
16
|
+
static bool initialized = false;
|
|
17
|
+
|
|
18
|
+
static bool com_initialized = false;
|
|
19
|
+
|
|
20
|
+
}// global
|
|
21
|
+
|
|
22
|
+
|
|
13
23
|
void
|
|
14
24
|
Beeps_init ()
|
|
15
25
|
{
|
|
26
|
+
if (global::initialized)
|
|
27
|
+
beeps_error(__FILE__, __LINE__, "already initialized.");
|
|
28
|
+
global::initialized = true;
|
|
29
|
+
|
|
16
30
|
check_media_foundation_error(
|
|
17
|
-
CoInitializeEx(NULL,
|
|
31
|
+
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED),
|
|
18
32
|
__FILE__, __LINE__);
|
|
33
|
+
global::com_initialized = true;
|
|
19
34
|
|
|
20
35
|
check_media_foundation_error(
|
|
21
36
|
MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET),
|
|
@@ -25,11 +40,19 @@ namespace Beeps
|
|
|
25
40
|
void
|
|
26
41
|
Beeps_fin ()
|
|
27
42
|
{
|
|
43
|
+
if (!global::initialized)
|
|
44
|
+
beeps_error(__FILE__, __LINE__, "not initialized.");
|
|
45
|
+
global::initialized = false;
|
|
46
|
+
|
|
28
47
|
check_media_foundation_error(
|
|
29
48
|
MFShutdown(),
|
|
30
49
|
__FILE__, __LINE__);
|
|
31
50
|
|
|
32
|
-
|
|
51
|
+
if (global::com_initialized)
|
|
52
|
+
{
|
|
53
|
+
CoUninitialize();
|
|
54
|
+
global::com_initialized = false;
|
|
55
|
+
}
|
|
33
56
|
}
|
|
34
57
|
|
|
35
58
|
|
data/src/win32/signals.cpp
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#include "../signals.h"
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
#include <stdint.h>
|
|
4
5
|
#include <mfapi.h>
|
|
5
6
|
#include <mfidl.h>
|
|
6
7
|
#include <mfreadwrite.h>
|
|
7
8
|
#include <xot/noncopyable.h>
|
|
8
9
|
#include <xot/string.h>
|
|
9
10
|
#include <xot/windows.h>
|
|
11
|
+
#include <mmreg.h>// must be after windows.h
|
|
10
12
|
#include "exception.h"
|
|
11
13
|
|
|
12
14
|
|
|
@@ -64,25 +66,63 @@ namespace Beeps
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
static void
|
|
67
|
-
|
|
69
|
+
set_decoded_media_type (IMFMediaType* decoded, IMFSourceReader* source_reader)
|
|
68
70
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
ReleasePtr<IMFMediaType> pcm_media_type;
|
|
71
|
+
ReleasePtr<IMFMediaType> native;
|
|
72
72
|
check_media_foundation_error(
|
|
73
|
-
|
|
73
|
+
source_reader->GetNativeMediaType(
|
|
74
|
+
MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, &native.ptr),
|
|
74
75
|
__FILE__, __LINE__);
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
|
|
77
|
+
GUID subtype = {};// all zeros, as GUID_NULL, which needs libuuid
|
|
78
|
+
UINT32 bits = 0;
|
|
79
|
+
native->GetGUID(MF_MT_SUBTYPE, &subtype);
|
|
80
|
+
native->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &bits);
|
|
81
|
+
|
|
82
|
+
bool is_float = subtype == MFAudioFormat_Float;
|
|
83
|
+
if (is_float)
|
|
84
|
+
bits = 32;
|
|
85
|
+
else if (subtype != MFAudioFormat_PCM || (bits != 8 && bits != 24 && bits != 32))
|
|
86
|
+
bits = 16;
|
|
87
|
+
|
|
88
|
+
decoded->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
|
|
89
|
+
decoded->SetGUID(MF_MT_SUBTYPE, is_float ? MFAudioFormat_Float : MFAudioFormat_PCM);
|
|
90
|
+
decoded->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bits);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static bool
|
|
94
|
+
is_float_format (const WAVEFORMATEX& format)
|
|
95
|
+
{
|
|
96
|
+
if (format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT)
|
|
97
|
+
return true;
|
|
98
|
+
if (format.wFormatTag != WAVE_FORMAT_EXTENSIBLE)
|
|
99
|
+
return false;
|
|
100
|
+
|
|
101
|
+
const auto& extensible = (const WAVEFORMATEXTENSIBLE&) format;
|
|
102
|
+
return extensible.SubFormat.Data1 == WAVE_FORMAT_IEEE_FLOAT;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static void
|
|
106
|
+
load_bytes (
|
|
107
|
+
WAVEFORMATEX* format, bool* is_float, std::vector<BYTE>* bytes,
|
|
108
|
+
const char* path)
|
|
109
|
+
{
|
|
110
|
+
std::wstring wpath = Xot::String(path).to_wstr();
|
|
77
111
|
|
|
78
112
|
ReleasePtr<IMFSourceReader> source_reader;
|
|
79
113
|
check_media_foundation_error(
|
|
80
114
|
MFCreateSourceReaderFromURL(wpath.c_str(), NULL, &source_reader.ptr),
|
|
81
115
|
__FILE__, __LINE__);
|
|
82
116
|
|
|
117
|
+
ReleasePtr<IMFMediaType> decoded_media_type;
|
|
118
|
+
check_media_foundation_error(
|
|
119
|
+
MFCreateMediaType(&decoded_media_type.ptr),
|
|
120
|
+
__FILE__, __LINE__);
|
|
121
|
+
set_decoded_media_type(decoded_media_type.ptr, source_reader.ptr);
|
|
122
|
+
|
|
83
123
|
check_media_foundation_error(
|
|
84
124
|
source_reader->SetCurrentMediaType(
|
|
85
|
-
MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL,
|
|
125
|
+
MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, decoded_media_type.ptr),
|
|
86
126
|
__FILE__, __LINE__);
|
|
87
127
|
|
|
88
128
|
ReleasePtr<IMFMediaType> media_type;
|
|
@@ -96,9 +136,15 @@ namespace Beeps
|
|
|
96
136
|
MFCreateWaveFormatExFromMFMediaType(media_type.ptr, &format_.ptr, NULL),
|
|
97
137
|
__FILE__, __LINE__);
|
|
98
138
|
|
|
99
|
-
*format
|
|
100
|
-
|
|
139
|
+
*format = *format_.ptr;
|
|
140
|
+
*is_float = is_float_format(*format_.ptr);
|
|
141
|
+
if (
|
|
142
|
+
format->wFormatTag != WAVE_FORMAT_PCM &&
|
|
143
|
+
format->wFormatTag != WAVE_FORMAT_IEEE_FLOAT &&
|
|
144
|
+
format->wFormatTag != WAVE_FORMAT_EXTENSIBLE)
|
|
145
|
+
{
|
|
101
146
|
beeps_error(__FILE__, __LINE__, "'%s' is not a PCM file.", path);
|
|
147
|
+
}
|
|
102
148
|
|
|
103
149
|
while (true)
|
|
104
150
|
{
|
|
@@ -123,7 +169,7 @@ namespace Beeps
|
|
|
123
169
|
__FILE__, __LINE__);
|
|
124
170
|
|
|
125
171
|
bytes->resize(bytes->size() + size);
|
|
126
|
-
memcpy(
|
|
172
|
+
memcpy(bytes->data() + bytes->size() - size, data, size);
|
|
127
173
|
|
|
128
174
|
check_media_foundation_error(
|
|
129
175
|
buffer->Unlock(),
|
|
@@ -138,35 +184,70 @@ namespace Beeps
|
|
|
138
184
|
beeps_error(__FILE__, __LINE__, "'%s' not found.", path);
|
|
139
185
|
|
|
140
186
|
WAVEFORMATEX format = {0};
|
|
187
|
+
bool is_float = false;
|
|
141
188
|
std::vector<BYTE> bytes;
|
|
142
|
-
load_bytes(&format, &bytes, path);
|
|
189
|
+
load_bytes(&format, &is_float, &bytes, path);
|
|
143
190
|
if (bytes.empty())
|
|
144
191
|
beeps_error(__FILE__, __LINE__, "failed to read bytes: '%s'", path);
|
|
145
192
|
|
|
146
193
|
uint Bps = format.wBitsPerSample / 8;
|
|
147
194
|
uint nchannels = format.nChannels;
|
|
148
|
-
|
|
195
|
+
if (is_float ? Bps != 4 : (Bps < 1 || 4 < Bps))
|
|
196
|
+
{
|
|
197
|
+
beeps_error(
|
|
198
|
+
__FILE__, __LINE__, "'%s' has samples of %d bits.",
|
|
199
|
+
path, format.wBitsPerSample);
|
|
200
|
+
}
|
|
201
|
+
if (nchannels == 0)
|
|
202
|
+
beeps_error(__FILE__, __LINE__, "'%s' has no channels.", path);
|
|
203
|
+
|
|
204
|
+
uint nsamples = bytes.size() / Bps / nchannels;
|
|
149
205
|
Signals signals(nsamples, nchannels, format.nSamplesPerSec);
|
|
150
206
|
|
|
207
|
+
uint step = nchannels * Bps;
|
|
151
208
|
for (uint ch = 0; ch < nchannels; ++ch)
|
|
152
209
|
{
|
|
210
|
+
Sample* to_p = Signals_at(&signals, 0, ch);
|
|
211
|
+
const uchar* from_p = ((uchar*) bytes.data()) + ch * Bps;
|
|
153
212
|
switch (Bps)
|
|
154
213
|
{
|
|
155
214
|
case 1:
|
|
156
215
|
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += nchannels)
|
|
160
|
-
*to_p = (*to_p - 128) / 128.f;
|
|
216
|
+
for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += step)
|
|
217
|
+
*to_p = (*from_p - 128) / 128.f;
|
|
161
218
|
break;
|
|
162
219
|
}
|
|
163
220
|
|
|
164
221
|
case 2:
|
|
165
222
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
223
|
+
for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += step)
|
|
224
|
+
*to_p = *(const short*) from_p / 32768.f;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 3:
|
|
229
|
+
{
|
|
230
|
+
for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += step)
|
|
231
|
+
{
|
|
232
|
+
int32_t value = from_p[0] | (from_p[1] << 8) | (from_p[2] << 16);
|
|
233
|
+
if (value & 0x800000) value -= 0x1000000;// sign extension
|
|
234
|
+
*to_p = value / 8388608.f;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 4:
|
|
240
|
+
{
|
|
241
|
+
if (is_float)
|
|
242
|
+
{
|
|
243
|
+
for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += step)
|
|
244
|
+
*to_p = *(const float*) from_p;
|
|
245
|
+
}
|
|
246
|
+
else
|
|
247
|
+
{
|
|
248
|
+
for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += step)
|
|
249
|
+
*to_p = *(const int32_t*) from_p / 2147483648.f;
|
|
250
|
+
}
|
|
170
251
|
break;
|
|
171
252
|
}
|
|
172
253
|
}
|
data/test/test_file_in.rb
CHANGED
|
@@ -14,6 +14,22 @@ class TestFileIn < Test::Unit::TestCase
|
|
|
14
14
|
B::Oscillator.new(...)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def wav(tag, bits, rate, data)
|
|
18
|
+
align = bits / 8
|
|
19
|
+
fmt = [tag, 1, rate, rate * align, align, bits].pack('vvVVvv')
|
|
20
|
+
['RIFF', 36 + data.bytesize, 'WAVE', 'fmt ', 16].pack('a4Va4a4V') + fmt +
|
|
21
|
+
['data', data.bytesize].pack('a4V') + data
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def save_wav(filename, *wav_args, &block)
|
|
25
|
+
Tempfile.create ["#{filename}", '.wav'] do |file|
|
|
26
|
+
file.binmode
|
|
27
|
+
file.write wav(*wav_args)
|
|
28
|
+
file.close
|
|
29
|
+
block.call file.path
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
17
33
|
def test_load_file()
|
|
18
34
|
Tempfile.create 'tmp.wav' do |file|
|
|
19
35
|
file.close
|
|
@@ -26,6 +42,28 @@ class TestFileIn < Test::Unit::TestCase
|
|
|
26
42
|
end
|
|
27
43
|
end
|
|
28
44
|
|
|
45
|
+
def test_load_formats()
|
|
46
|
+
rate = B.sample_rate
|
|
47
|
+
values = (0...rate / 10).map {Math.sin(2 * Math::PI * _1 / 100) * 0.5}
|
|
48
|
+
{
|
|
49
|
+
pcm8: [1, 8, 0.01, values.map {(_1 * 128 + 128).round}.pack('C*')],
|
|
50
|
+
pcm16: [1, 16, 0.0001, values.map {(_1 * 32768).round}.pack('s<*')],
|
|
51
|
+
pcm24: [1, 24, 0.0001, values.map {(_1 * 8388608).round}
|
|
52
|
+
.flat_map {[_1, _1 >> 8, _1 >> 16]}
|
|
53
|
+
.map {_1 & 0xff}.pack('C*')],
|
|
54
|
+
pcm32: [1, 32, 0.0001, values.map {(_1 * 2147483648).round}.pack('l<*')],
|
|
55
|
+
float32: [3, 32, 0.0001, values.pack('e*')]
|
|
56
|
+
}.each do |name, (tag, bits, delta, data)|
|
|
57
|
+
save_wav name, tag, bits, rate, data do |path|
|
|
58
|
+
samples = B::Processor
|
|
59
|
+
.get_signals(filein(path), seconds: values.size.to_f / rate)
|
|
60
|
+
.to_a
|
|
61
|
+
assert_equal values.size, samples.size, name
|
|
62
|
+
assert_each_in_delta values, samples, delta, name
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
29
67
|
def test_file_not_found()
|
|
30
68
|
assert_raise(B::BeepsError) {B::FileIn.new 'nofile.wav'}
|
|
31
69
|
end
|
data/test/test_sound.rb
CHANGED
|
@@ -38,17 +38,17 @@ class TestSound < Test::Unit::TestCase
|
|
|
38
38
|
assert_nothing_raised {B::Sound.load PATH}
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def test_nchannels()
|
|
42
|
+
assert_equal 1, sound .nchannels
|
|
43
|
+
assert_equal 2, sound(nchannels: 2).nchannels
|
|
44
|
+
end
|
|
45
|
+
|
|
41
46
|
def test_sample_rate()
|
|
42
47
|
assert_in_epsilon 44100, sound .sample_rate
|
|
43
48
|
assert_in_epsilon 48000, sound(sample_rate: 48000).sample_rate
|
|
44
49
|
assert_in_epsilon 96000, sound(sample_rate: 96000).sample_rate
|
|
45
50
|
end
|
|
46
51
|
|
|
47
|
-
def test_nchannels()
|
|
48
|
-
assert_equal 1, sound .nchannels
|
|
49
|
-
assert_equal 2, sound(nchannels: 2).nchannels
|
|
50
|
-
end
|
|
51
|
-
|
|
52
52
|
def test_seconds()
|
|
53
53
|
assert_in_epsilon 0.2, sound(0.2).seconds
|
|
54
54
|
assert_in_epsilon (-1), sound(0) .seconds
|
|
@@ -90,8 +90,8 @@ class TestSound < Test::Unit::TestCase
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
s = B::Sound.load PATH
|
|
93
|
-
assert_in_epsilon 96000, s.sample_rate
|
|
94
93
|
assert_equal 2, s.nchannels
|
|
94
|
+
assert_in_epsilon 96000, s.sample_rate
|
|
95
95
|
assert_in_epsilon 0.1, s.seconds
|
|
96
96
|
assert_nothing_raised {s.play}
|
|
97
97
|
end
|
data/test/test_sound_player.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'tempfile'
|
|
1
2
|
require_relative 'helper'
|
|
2
3
|
|
|
3
4
|
|
|
@@ -5,23 +6,20 @@ class TestSoundPlayer < Test::Unit::TestCase
|
|
|
5
6
|
|
|
6
7
|
B = Beeps
|
|
7
8
|
|
|
8
|
-
PATH = 'test.wav'
|
|
9
|
-
|
|
10
9
|
def sound(seconds = 0.1, processor: B::Oscillator.new, **kwargs)
|
|
11
10
|
B::Sound.new processor >> B::Gain.new(gain: 0), seconds, **kwargs
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
def stream_sound(**kwargs)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
def stream_sound(seconds = 0.1, **kwargs)
|
|
14
|
+
Tempfile.create ['stream', '.wav'] do |file|
|
|
15
|
+
file.close
|
|
16
|
+
sound(seconds).save file.path
|
|
17
|
+
sound 0, processor: B::FileIn.new(file.path), **kwargs
|
|
18
|
+
end
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
def teardown()
|
|
23
22
|
B::SoundPlayer.stop_all
|
|
24
|
-
File.delete PATH if File.exist?(PATH)
|
|
25
23
|
end
|
|
26
24
|
|
|
27
25
|
def test_initialize()
|
|
@@ -66,7 +64,7 @@ class TestSoundPlayer < Test::Unit::TestCase
|
|
|
66
64
|
sleep sec * 2
|
|
67
65
|
assert_equal :stopped, p.state
|
|
68
66
|
|
|
69
|
-
s = stream_sound
|
|
67
|
+
s = stream_sound sec
|
|
70
68
|
p = s.play
|
|
71
69
|
assert_equal :playing, p.state
|
|
72
70
|
sleep sec * 2
|
|
@@ -87,6 +85,18 @@ class TestSoundPlayer < Test::Unit::TestCase
|
|
|
87
85
|
assert_equal :playing, p.state
|
|
88
86
|
end
|
|
89
87
|
|
|
88
|
+
def test_play_after_stop()
|
|
89
|
+
p = sound.play
|
|
90
|
+
p.stop
|
|
91
|
+
p.play
|
|
92
|
+
assert_equal :playing, p.state
|
|
93
|
+
|
|
94
|
+
p = stream_sound.play
|
|
95
|
+
p.stop
|
|
96
|
+
p.play
|
|
97
|
+
assert_equal :playing, p.state
|
|
98
|
+
end
|
|
99
|
+
|
|
90
100
|
def test_stop_after_pause()
|
|
91
101
|
p = sound.play
|
|
92
102
|
p.pause
|
|
@@ -102,6 +112,8 @@ class TestSoundPlayer < Test::Unit::TestCase
|
|
|
102
112
|
end
|
|
103
113
|
|
|
104
114
|
def test_state()
|
|
115
|
+
assert_equal :none, B::SoundPlayer.new.state
|
|
116
|
+
|
|
105
117
|
p = sound.play
|
|
106
118
|
assert_equal :playing, p.state
|
|
107
119
|
assert_equal [true, false, false], [p.playing?, p.paused?, p.stopped?]
|
|
@@ -155,32 +167,67 @@ class TestSoundPlayer < Test::Unit::TestCase
|
|
|
155
167
|
assert_equal 200, p.position
|
|
156
168
|
end
|
|
157
169
|
|
|
170
|
+
def test_play_from_position_after_stop()
|
|
171
|
+
p = sound(1).play
|
|
172
|
+
p.stop
|
|
173
|
+
p.position = 22050
|
|
174
|
+
assert_equal 22050, p.position
|
|
175
|
+
p.play
|
|
176
|
+
sleep 0.05
|
|
177
|
+
assert_equal :playing, p.state
|
|
178
|
+
assert_operator p.position, :>=, 22050
|
|
179
|
+
|
|
180
|
+
p = stream_sound(1).play
|
|
181
|
+
p.stop
|
|
182
|
+
p.position = 1000
|
|
183
|
+
assert_equal 1000, p.position
|
|
184
|
+
p.play
|
|
185
|
+
sleep 0.05
|
|
186
|
+
assert_equal :playing, p.state
|
|
187
|
+
assert_operator p.position, :>=, 1000
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_position_over_length()
|
|
191
|
+
p = sound.play
|
|
192
|
+
assert_raise(ArgumentError) {p.position = 100_000}
|
|
193
|
+
p.stop
|
|
194
|
+
assert_raise(ArgumentError) {p.position = 100_000}
|
|
195
|
+
p.play
|
|
196
|
+
assert_equal :playing, p.state
|
|
197
|
+
|
|
198
|
+
p = stream_sound.play
|
|
199
|
+
p.stop
|
|
200
|
+
p.position = 100_000
|
|
201
|
+
p.play
|
|
202
|
+
assert_equal :stopped, p.state
|
|
203
|
+
end
|
|
204
|
+
|
|
158
205
|
def test_time()
|
|
159
206
|
p = sound.play
|
|
160
207
|
p.pause
|
|
161
|
-
assert_in_epsilon 0,
|
|
208
|
+
assert_in_epsilon 0, p.time
|
|
162
209
|
|
|
163
|
-
p.time = 0.
|
|
210
|
+
p.time = 0.025
|
|
164
211
|
sleep 0.05
|
|
165
|
-
assert_in_epsilon 0.
|
|
212
|
+
assert_in_epsilon 0.025, p.time
|
|
166
213
|
|
|
167
|
-
p.time 0.
|
|
214
|
+
p.time 0.05
|
|
168
215
|
sleep 0.05
|
|
169
|
-
assert_in_epsilon 0.
|
|
216
|
+
assert_in_epsilon 0.05, p.time
|
|
170
217
|
end
|
|
171
218
|
|
|
172
219
|
def test_time_stream()
|
|
173
220
|
p = stream_sound.play
|
|
174
221
|
p.pause
|
|
175
|
-
assert_in_epsilon 0,
|
|
222
|
+
assert_in_epsilon 0, p.time
|
|
176
223
|
|
|
177
|
-
p.time = 0.
|
|
224
|
+
p.time = 0.025
|
|
178
225
|
sleep 0.05
|
|
179
|
-
assert_in_epsilon 0.
|
|
226
|
+
assert_in_epsilon 0.025, p.time
|
|
180
227
|
|
|
181
|
-
p.time 0.
|
|
228
|
+
p.time 0.05
|
|
182
229
|
sleep 0.05
|
|
183
|
-
assert_in_epsilon 0.
|
|
230
|
+
assert_in_epsilon 0.05, p.time
|
|
184
231
|
end
|
|
185
232
|
|
|
186
233
|
def test_time_scale()
|
|
@@ -228,4 +275,23 @@ class TestSoundPlayer < Test::Unit::TestCase
|
|
|
228
275
|
assert_true players.all? {|p| p.stopped?}
|
|
229
276
|
end
|
|
230
277
|
|
|
278
|
+
def test_players_over_limit()
|
|
279
|
+
s = sound 1
|
|
280
|
+
|
|
281
|
+
paused = s.play
|
|
282
|
+
paused.pause
|
|
283
|
+
paused.time = 0.5
|
|
284
|
+
|
|
285
|
+
players = 300.times.map {s.play}
|
|
286
|
+
assert_true players.none? {|p| p.state == :none}
|
|
287
|
+
assert_equal :paused, paused.state
|
|
288
|
+
assert_in_epsilon 0.5, paused.time
|
|
289
|
+
|
|
290
|
+
paused.play
|
|
291
|
+
assert_equal :playing, paused.state
|
|
292
|
+
|
|
293
|
+
players.first.play
|
|
294
|
+
assert_equal :playing, players.first.state
|
|
295
|
+
end
|
|
296
|
+
|
|
231
297
|
end# TestSoundPlayer
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: beeps
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- xordog
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: xot
|
|
@@ -16,28 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.
|
|
19
|
+
version: 0.5.0
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.
|
|
26
|
+
version: 0.5.0
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: rucy
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.
|
|
33
|
+
version: 0.5.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.
|
|
40
|
+
version: 0.5.0
|
|
41
41
|
description: Synthesize and play beep sounds.
|
|
42
42
|
email: xordog@gmail.com
|
|
43
43
|
executables: []
|
|
@@ -93,7 +93,6 @@ files:
|
|
|
93
93
|
- ".github/workflows/release-gem.yml"
|
|
94
94
|
- ".github/workflows/tag.yml"
|
|
95
95
|
- ".github/workflows/test.yml"
|
|
96
|
-
- ".github/workflows/utils.rb"
|
|
97
96
|
- CLAUDE.md
|
|
98
97
|
- CONTRIBUTING.md
|
|
99
98
|
- ChangeLog.md
|
|
@@ -233,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
233
232
|
- !ruby/object:Gem::Version
|
|
234
233
|
version: '0'
|
|
235
234
|
requirements: []
|
|
236
|
-
rubygems_version: 3.
|
|
235
|
+
rubygems_version: 3.5.22
|
|
237
236
|
signing_key:
|
|
238
237
|
specification_version: 4
|
|
239
238
|
summary: Plays beep sound.
|
data/.github/workflows/utils.rb
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
require 'shellwords'
|
|
2
|
-
|
|
3
|
-
ALL_REPO = 'xord/all'
|
|
4
|
-
ALL_DIR = '../all'
|
|
5
|
-
ALL_FETCH_DEPTH = 100
|
|
6
|
-
|
|
7
|
-
RENAMES = {reflex: 'reflexion'}
|
|
8
|
-
|
|
9
|
-
def sh(cmd)
|
|
10
|
-
puts cmd
|
|
11
|
-
system cmd
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def setup_dependencies(only: nil)
|
|
15
|
-
gemspec_path = `git ls-files`.lines(chomp: true).find {|l| l =~ /\.gemspec$/}
|
|
16
|
-
return unless gemspec_path
|
|
17
|
-
|
|
18
|
-
gemspec = File.read gemspec_path
|
|
19
|
-
name = File.basename gemspec_path, '.gemspec'
|
|
20
|
-
|
|
21
|
-
exts = File.readlines('Rakefile')
|
|
22
|
-
.map {|l| l[%r|^\s*require\W+([\w\-\_]+)/extension\W+$|, 1]}
|
|
23
|
-
.compact
|
|
24
|
-
.reject {|ext| ext == name}
|
|
25
|
-
exts = exts & [only].flatten.map(&:to_s) if only
|
|
26
|
-
return if exts.empty?
|
|
27
|
-
|
|
28
|
-
unless setup_dependencies_via_monorepo(exts)
|
|
29
|
-
setup_dependencies_via_each_repo_by_version(gemspec, exts)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
exts.each {|ext| sh %( cd ../#{ext} && rake ext )}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def setup_dependencies_via_monorepo(exts)
|
|
36
|
-
return false unless checkout_monorepo
|
|
37
|
-
exts.each {|ext| sh %( ln -snf all/#{ext} ../#{ext} )}
|
|
38
|
-
true
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def checkout_monorepo()
|
|
42
|
-
uuid = `git log -1 --format=%B`[/^\[\[([0-9a-fA-F-]+)\]\]$/, 1]
|
|
43
|
-
return false unless uuid
|
|
44
|
-
|
|
45
|
-
commit = setup_monorepo uuid
|
|
46
|
-
return false unless commit
|
|
47
|
-
|
|
48
|
-
Dir.chdir(ALL_DIR) {sh %( git checkout -q #{commit} )}
|
|
49
|
-
true
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def setup_monorepo(uuid)
|
|
53
|
-
unless File.directory? ALL_DIR
|
|
54
|
-
url = "https://github.com/#{ALL_REPO}.git"
|
|
55
|
-
sh %( git clone --no-tags --depth #{ALL_FETCH_DEPTH} #{url} #{ALL_DIR} )
|
|
56
|
-
end
|
|
57
|
-
loop do
|
|
58
|
-
commit = find_monorepo_commit uuid
|
|
59
|
-
return commit if commit
|
|
60
|
-
|
|
61
|
-
deepened = Dir.chdir ALL_DIR do
|
|
62
|
-
before = `git rev-list --count HEAD`.to_i
|
|
63
|
-
sh %( git fetch --deepen #{ALL_FETCH_DEPTH} )
|
|
64
|
-
`git rev-list --count HEAD`.to_i > before
|
|
65
|
-
end
|
|
66
|
-
return nil unless deepened
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def find_monorepo_commit(uuid)
|
|
71
|
-
Dir.chdir ALL_DIR do
|
|
72
|
-
out = `git log origin/HEAD -F --grep="[[#{uuid}]]" --format=%H -1`.strip
|
|
73
|
-
out.empty? ? nil : out
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def setup_dependencies_via_each_repo_by_version(gemspec, exts)
|
|
78
|
-
exts.each do |ext|
|
|
79
|
-
gem = RENAMES[ext.to_sym].then {|s| s || ext}
|
|
80
|
-
ver = gemspec[/add_dependency.*['"]#{gem}['"].*['"]\s*~>\s*([\d\.]+)\s*['"]/, 1]
|
|
81
|
-
opts = '-c advice.detachedHead=false --depth 1'
|
|
82
|
-
clone = "git clone #{opts} https://github.com/xord/#{ext}.git ../#{ext}"
|
|
83
|
-
|
|
84
|
-
# 'rake subtree:push' pushes all subrepos, so cloning by new tag
|
|
85
|
-
# often fails before tagging each new tag
|
|
86
|
-
sh %( #{clone} --branch v#{ver} || #{clone} )
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def tag_versions()
|
|
91
|
-
changes = changelogs
|
|
92
|
-
tags = `git tag`.lines chomp: true
|
|
93
|
-
vers = `git log --oneline ./VERSION`
|
|
94
|
-
.lines(chomp: true)
|
|
95
|
-
.map {|line| line.split.first[/^\w+$/]}
|
|
96
|
-
.map {|sha| [`git cat-file -p #{sha}:./VERSION 2>/dev/null`[/[\d\.]+/], sha]}
|
|
97
|
-
.select {|ver, sha| ver && sha}
|
|
98
|
-
.reverse
|
|
99
|
-
.to_h
|
|
100
|
-
|
|
101
|
-
vers.to_a.reverse.each do |ver, sha|
|
|
102
|
-
tag = "v#{ver}"
|
|
103
|
-
break if tags.include?(tag)
|
|
104
|
-
sh %( git tag -a -m \"#{changes[tag]&.gsub '"', '\\"'}\" #{tag} #{sha} )
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def release(*paths)
|
|
109
|
-
tag = ENV['GITHUB_REF']&.sub(%r|^refs/tags/|, '') || raise('GITHUB_REF tag not set')
|
|
110
|
-
notes = (changelogs[tag] || '').shellescape
|
|
111
|
-
paths = paths.flatten.join ' '
|
|
112
|
-
|
|
113
|
-
sh(%( gh release create #{tag} #{paths} --notes #{notes} )) ||
|
|
114
|
-
sh(%( gh release upload #{tag} #{paths} --clobber )) ||
|
|
115
|
-
raise('failed to upload to releases')
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def changelogs()
|
|
119
|
-
File.read('ChangeLog.md')
|
|
120
|
-
.split(/^\s*##\s*\[\s*(v[\d\.]+)\s*\].*$/)
|
|
121
|
-
.slice(1..)
|
|
122
|
-
.each_slice(2)
|
|
123
|
-
.to_h
|
|
124
|
-
.transform_values(&:strip!)
|
|
125
|
-
rescue Errno::ENOENT
|
|
126
|
-
raise 'failed to get changelogs'
|
|
127
|
-
end
|