beeps 0.3.16 → 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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.doc/ext/beeps/analyser.cpp +4 -4
  3. data/.doc/ext/beeps/envelope.cpp +1 -1
  4. data/.doc/ext/beeps/file_in.cpp +1 -1
  5. data/.doc/ext/beeps/gain.cpp +1 -1
  6. data/.doc/ext/beeps/high_pass.cpp +1 -1
  7. data/.doc/ext/beeps/low_pass.cpp +1 -1
  8. data/.doc/ext/beeps/mic_in.cpp +1 -1
  9. data/.doc/ext/beeps/mixer.cpp +2 -2
  10. data/.doc/ext/beeps/oscillator.cpp +3 -3
  11. data/.doc/ext/beeps/pitch_shift.cpp +1 -1
  12. data/.doc/ext/beeps/processor.cpp +1 -1
  13. data/.doc/ext/beeps/reverb.cpp +1 -1
  14. data/.doc/ext/beeps/sequencer.cpp +2 -2
  15. data/.doc/ext/beeps/signals.cpp +3 -3
  16. data/.doc/ext/beeps/sound.cpp +16 -8
  17. data/.doc/ext/beeps/sound_player.cpp +13 -5
  18. data/.doc/ext/beeps/text_in.cpp +1 -1
  19. data/.doc/ext/beeps/time_stretch.cpp +1 -1
  20. data/.doc/ext/beeps/value.cpp +2 -2
  21. data/.github/workflows/release-gem.yml +2 -40
  22. data/.github/workflows/tag.yml +5 -28
  23. data/.github/workflows/test.yml +1 -28
  24. data/ChangeLog.md +28 -0
  25. data/Gemfile.lock +13 -8
  26. data/Rakefile +1 -1
  27. data/VERSION +1 -1
  28. data/beeps.gemspec +3 -3
  29. data/ext/beeps/analyser.cpp +4 -4
  30. data/ext/beeps/envelope.cpp +1 -1
  31. data/ext/beeps/file_in.cpp +1 -1
  32. data/ext/beeps/gain.cpp +1 -1
  33. data/ext/beeps/high_pass.cpp +1 -1
  34. data/ext/beeps/low_pass.cpp +1 -1
  35. data/ext/beeps/mic_in.cpp +1 -1
  36. data/ext/beeps/mixer.cpp +2 -2
  37. data/ext/beeps/oscillator.cpp +3 -3
  38. data/ext/beeps/pitch_shift.cpp +1 -1
  39. data/ext/beeps/processor.cpp +1 -1
  40. data/ext/beeps/reverb.cpp +1 -1
  41. data/ext/beeps/sequencer.cpp +2 -2
  42. data/ext/beeps/signals.cpp +3 -3
  43. data/ext/beeps/sound.cpp +17 -8
  44. data/ext/beeps/sound_player.cpp +14 -5
  45. data/ext/beeps/text_in.cpp +1 -1
  46. data/ext/beeps/time_stretch.cpp +1 -1
  47. data/ext/beeps/value.cpp +2 -2
  48. data/include/beeps/defs.h +14 -4
  49. data/include/beeps/signals.h +2 -2
  50. data/include/beeps/sound.h +9 -3
  51. data/lib/beeps/beeps.rb +4 -4
  52. data/lib/beeps/extension.rb +1 -1
  53. data/lib/beeps/sound.rb +2 -2
  54. data/src/file_in.cpp +2 -0
  55. data/src/mic_in.cpp +10 -9
  56. data/src/openal.cpp +66 -8
  57. data/src/openal.h +5 -1
  58. data/src/oscillator.cpp +1 -1
  59. data/src/signals.cpp +6 -6
  60. data/src/sound.cpp +382 -160
  61. data/src/win32/beeps.cpp +25 -2
  62. data/src/win32/signals.cpp +103 -29
  63. data/test/test_file_in.rb +38 -0
  64. data/test/test_sound.rb +6 -6
  65. data/test/test_sound_player.rb +86 -20
  66. metadata +7 -8
  67. 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, COINIT_MULTITHREADED),
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
- CoUninitialize();
51
+ if (global::com_initialized)
52
+ {
53
+ CoUninitialize();
54
+ global::com_initialized = false;
55
+ }
33
56
  }
34
57
 
35
58
 
@@ -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
 
@@ -55,41 +57,72 @@ namespace Beeps
55
57
 
56
58
  static bool is_file_exist (const char* path)
57
59
  {
58
- DWORD attribs = GetFileAttributes(path);
60
+ if (!path) return false;
61
+
62
+ DWORD attribs = GetFileAttributesW(Xot::String(path).to_wstr().c_str());
59
63
  return
60
64
  attribs != INVALID_FILE_ATTRIBUTES &&
61
65
  !(attribs & FILE_ATTRIBUTE_DIRECTORY);
62
66
  }
63
67
 
64
- static std::wstring
65
- to_wcs (const char* mbs)
68
+ static void
69
+ set_decoded_media_type (IMFMediaType* decoded, IMFSourceReader* source_reader)
70
+ {
71
+ ReleasePtr<IMFMediaType> native;
72
+ check_media_foundation_error(
73
+ source_reader->GetNativeMediaType(
74
+ MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, &native.ptr),
75
+ __FILE__, __LINE__);
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)
66
95
  {
67
- size_t len = strlen(mbs);
68
- std::wstring wcs(len, L'#');
69
- mbstowcs(&wcs[0], mbs, len);
70
- return wcs;
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;
71
103
  }
72
104
 
73
105
  static void
74
- load_bytes (WAVEFORMATEX* format, std::vector<BYTE>* bytes, const char* path)
106
+ load_bytes (
107
+ WAVEFORMATEX* format, bool* is_float, std::vector<BYTE>* bytes,
108
+ const char* path)
75
109
  {
76
- std::wstring wpath = to_wcs(path);
110
+ std::wstring wpath = Xot::String(path).to_wstr();
77
111
 
78
- ReleasePtr<IMFMediaType> pcm_media_type;
112
+ ReleasePtr<IMFSourceReader> source_reader;
79
113
  check_media_foundation_error(
80
- MFCreateMediaType(&pcm_media_type.ptr),
114
+ MFCreateSourceReaderFromURL(wpath.c_str(), NULL, &source_reader.ptr),
81
115
  __FILE__, __LINE__);
82
- pcm_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
83
- pcm_media_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
84
116
 
85
- ReleasePtr<IMFSourceReader> source_reader;
117
+ ReleasePtr<IMFMediaType> decoded_media_type;
86
118
  check_media_foundation_error(
87
- MFCreateSourceReaderFromURL(wpath.c_str(), NULL, &source_reader.ptr),
119
+ MFCreateMediaType(&decoded_media_type.ptr),
88
120
  __FILE__, __LINE__);
121
+ set_decoded_media_type(decoded_media_type.ptr, source_reader.ptr);
89
122
 
90
123
  check_media_foundation_error(
91
124
  source_reader->SetCurrentMediaType(
92
- MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, pcm_media_type.ptr),
125
+ MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, decoded_media_type.ptr),
93
126
  __FILE__, __LINE__);
94
127
 
95
128
  ReleasePtr<IMFMediaType> media_type;
@@ -103,9 +136,15 @@ namespace Beeps
103
136
  MFCreateWaveFormatExFromMFMediaType(media_type.ptr, &format_.ptr, NULL),
104
137
  __FILE__, __LINE__);
105
138
 
106
- *format = *format_.ptr;
107
- if (format->wFormatTag != WAVE_FORMAT_PCM)
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
+ {
108
146
  beeps_error(__FILE__, __LINE__, "'%s' is not a PCM file.", path);
147
+ }
109
148
 
110
149
  while (true)
111
150
  {
@@ -130,7 +169,7 @@ namespace Beeps
130
169
  __FILE__, __LINE__);
131
170
 
132
171
  bytes->resize(bytes->size() + size);
133
- memcpy(&(*bytes)[0] + bytes->size() - size, data, size);
172
+ memcpy(bytes->data() + bytes->size() - size, data, size);
134
173
 
135
174
  check_media_foundation_error(
136
175
  buffer->Unlock(),
@@ -145,35 +184,70 @@ namespace Beeps
145
184
  beeps_error(__FILE__, __LINE__, "'%s' not found.", path);
146
185
 
147
186
  WAVEFORMATEX format = {0};
187
+ bool is_float = false;
148
188
  std::vector<BYTE> bytes;
149
- load_bytes(&format, &bytes, path);
189
+ load_bytes(&format, &is_float, &bytes, path);
150
190
  if (bytes.empty())
151
191
  beeps_error(__FILE__, __LINE__, "failed to read bytes: '%s'", path);
152
192
 
153
193
  uint Bps = format.wBitsPerSample / 8;
154
194
  uint nchannels = format.nChannels;
155
- uint nsamples = bytes.size() / Bps / nchannels;
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;
156
205
  Signals signals(nsamples, nchannels, format.nSamplesPerSec);
157
206
 
207
+ uint step = nchannels * Bps;
158
208
  for (uint ch = 0; ch < nchannels; ++ch)
159
209
  {
210
+ Sample* to_p = Signals_at(&signals, 0, ch);
211
+ const uchar* from_p = ((uchar*) bytes.data()) + ch * Bps;
160
212
  switch (Bps)
161
213
  {
162
214
  case 1:
163
215
  {
164
- Sample* to_p = Signals_at(&signals, 0, ch);
165
- const uchar* from_p = ((uchar*) &bytes[0]) + ch;
166
- for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += nchannels)
167
- *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;
168
218
  break;
169
219
  }
170
220
 
171
221
  case 2:
172
222
  {
173
- Sample* to_p = Signals_at(&signals, 0, ch);
174
- const ushort* from_p = ((ushort*) &bytes[0]) + ch;
175
- for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += nchannels)
176
- *to_p = *from_p / 32768.f;
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
+ }
177
251
  break;
178
252
  }
179
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
@@ -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
- sound 0, processor: B::FileIn.new(PATH), **kwargs
16
- end
17
-
18
- def setup()
19
- sound.save PATH
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, p.time
208
+ assert_in_epsilon 0, p.time
162
209
 
163
- p.time = 0.05
210
+ p.time = 0.025
164
211
  sleep 0.05
165
- assert_in_epsilon 0.05, p.time
212
+ assert_in_epsilon 0.025, p.time
166
213
 
167
- p.time 0.1
214
+ p.time 0.05
168
215
  sleep 0.05
169
- assert_in_epsilon 0.1, p.time
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, p.time
222
+ assert_in_epsilon 0, p.time
176
223
 
177
- p.time = 0.05
224
+ p.time = 0.025
178
225
  sleep 0.05
179
- assert_in_epsilon 0.05, p.time
226
+ assert_in_epsilon 0.025, p.time
180
227
 
181
- p.time 0.1
228
+ p.time 0.05
182
229
  sleep 0.05
183
- assert_in_epsilon 0.1, p.time
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.3.16
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-06-22 00:00:00.000000000 Z
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.3.15
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.3.15
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.3.15
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.3.15
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.4.19
235
+ rubygems_version: 3.5.22
237
236
  signing_key:
238
237
  specification_version: 4
239
238
  summary: Plays beep sound.