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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.doc/ext/beeps/analyser.cpp +1 -1
  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 +1 -1
  10. data/.doc/ext/beeps/oscillator.cpp +2 -2
  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 +1 -1
  15. data/.doc/ext/beeps/signals.cpp +1 -1
  16. data/.doc/ext/beeps/sound.cpp +6 -6
  17. data/.doc/ext/beeps/sound_player.cpp +5 -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 +1 -1
  21. data/.github/workflows/release-gem.yml +2 -49
  22. data/.github/workflows/tag.yml +5 -28
  23. data/.github/workflows/test.yml +1 -37
  24. data/ChangeLog.md +21 -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 +1 -1
  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 +1 -1
  37. data/ext/beeps/oscillator.cpp +2 -2
  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 +1 -1
  42. data/ext/beeps/signals.cpp +1 -1
  43. data/ext/beeps/sound.cpp +6 -6
  44. data/ext/beeps/sound_player.cpp +5 -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 +1 -1
  48. data/include/beeps/signals.h +2 -2
  49. data/include/beeps/sound.h +3 -3
  50. data/lib/beeps/beeps.rb +4 -4
  51. data/lib/beeps/sound.rb +1 -1
  52. data/src/mic_in.cpp +10 -9
  53. data/src/openal.cpp +66 -8
  54. data/src/openal.h +5 -1
  55. data/src/oscillator.cpp +1 -1
  56. data/src/signals.cpp +6 -6
  57. data/src/sound.cpp +347 -152
  58. data/src/win32/beeps.cpp +25 -2
  59. data/src/win32/signals.cpp +102 -21
  60. data/test/test_file_in.rb +38 -0
  61. data/test/test_sound.rb +6 -6
  62. data/test/test_sound_player.rb +86 -20
  63. metadata +7 -8
  64. 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
 
@@ -64,25 +66,63 @@ namespace Beeps
64
66
  }
65
67
 
66
68
  static void
67
- load_bytes (WAVEFORMATEX* format, std::vector<BYTE>* bytes, const char* path)
69
+ set_decoded_media_type (IMFMediaType* decoded, IMFSourceReader* source_reader)
68
70
  {
69
- std::wstring wpath = Xot::String(path).to_wstr();
70
-
71
- ReleasePtr<IMFMediaType> pcm_media_type;
71
+ ReleasePtr<IMFMediaType> native;
72
72
  check_media_foundation_error(
73
- MFCreateMediaType(&pcm_media_type.ptr),
73
+ source_reader->GetNativeMediaType(
74
+ MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, &native.ptr),
74
75
  __FILE__, __LINE__);
75
- pcm_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
76
- pcm_media_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
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, pcm_media_type.ptr),
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 = *format_.ptr;
100
- 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
+ {
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(&(*bytes)[0] + bytes->size() - size, data, size);
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
- 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;
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
- Sample* to_p = Signals_at(&signals, 0, ch);
158
- const uchar* from_p = ((uchar*) &bytes[0]) + ch;
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
- Sample* to_p = Signals_at(&signals, 0, ch);
167
- const ushort* from_p = ((ushort*) &bytes[0]) + ch;
168
- for (uint i = 0; i < nsamples; ++i, to_p += nchannels, from_p += nchannels)
169
- *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
+ }
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
@@ -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.4.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-08-04 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.4.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.4.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.4.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.4.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.4.19
235
+ rubygems_version: 3.5.22
237
236
  signing_key:
238
237
  specification_version: 4
239
238
  summary: Plays beep sound.
@@ -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