beeps 0.3.11 → 0.3.13
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/sound_player.cpp +57 -0
- data/.github/workflows/release-gem.yml +5 -16
- data/.github/workflows/utils.rb +88 -17
- data/ChangeLog.md +20 -0
- data/README.md +148 -5
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/beeps.gemspec +3 -4
- data/ext/beeps/extconf.rb +1 -1
- data/ext/beeps/sound_player.cpp +64 -1
- data/include/beeps/defs.h +2 -0
- data/include/beeps/filter.h +6 -0
- data/include/beeps/generator.h +8 -2
- data/include/beeps/processor.h +10 -8
- data/include/beeps/ruby.h +2 -2
- data/include/beeps/signals.h +14 -0
- data/include/beeps/sound.h +12 -0
- data/include/beeps.h +2 -2
- data/lib/beeps/extension.rb +8 -2
- data/lib/beeps/sound.rb +1 -1
- data/src/analyser.cpp +10 -3
- data/src/envelope.cpp +2 -5
- data/src/file_in.cpp +7 -1
- data/src/gain.cpp +7 -0
- data/src/mixer.cpp +16 -3
- data/src/oscillator.cpp +7 -1
- data/src/osx/signals.mm +4 -4
- data/src/osx/text_in.mm +1 -1
- data/src/processor.cpp +51 -38
- data/src/processor.h +1 -5
- data/src/reverb.cpp +2 -3
- data/src/sequencer.cpp +4 -4
- data/src/signals.cpp +178 -161
- data/src/signals.h +2 -15
- data/src/sound.cpp +154 -5
- data/src/time_stretch.cpp +2 -2
- data/src/value.cpp +8 -2
- data/src/win32/signals.cpp +9 -9
- data/src/x_pass.h +2 -3
- data/test/test_sound_player.rb +70 -0
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be0112d74491238712ca27ab7da557b8dbffac3075e2ac1eb13efa7ca91b4f18
|
|
4
|
+
data.tar.gz: 935312ec040f4dc78ab97135d8ff5035ee5f8712a5e1019bb720aead83e7bb90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c83043583de20a32cd345f29cf01c3e56cebbbfc6dc960d9a2032e1b2d179c8fcf5013f67f0a775807e5084b00c8e7d8674f464da2e8383ce4bf5e46e8d9c804
|
|
7
|
+
data.tar.gz: aabb022c116a6c942b2b4c253e0caed24ed7f62320190b768dfd76d331da2acb6d28fb2f4ddd939db577b4bfce625af968baadba095de66a2a1fd2ba9667cc58
|
|
@@ -61,6 +61,57 @@ VALUE get_state(VALUE self)
|
|
|
61
61
|
return value(THIS->state());
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
static
|
|
65
|
+
VALUE set_position(VALUE self, VALUE position)
|
|
66
|
+
{
|
|
67
|
+
CHECK;
|
|
68
|
+
|
|
69
|
+
THIS->set_position(to<uint>(position));
|
|
70
|
+
return position;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static
|
|
74
|
+
VALUE get_position(VALUE self)
|
|
75
|
+
{
|
|
76
|
+
CHECK;
|
|
77
|
+
|
|
78
|
+
return value(THIS->position());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static
|
|
82
|
+
VALUE set_time(VALUE self, VALUE time)
|
|
83
|
+
{
|
|
84
|
+
CHECK;
|
|
85
|
+
|
|
86
|
+
THIS->set_time(to<float>(time));
|
|
87
|
+
return time;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static
|
|
91
|
+
VALUE get_time(VALUE self)
|
|
92
|
+
{
|
|
93
|
+
CHECK;
|
|
94
|
+
|
|
95
|
+
return value(THIS->time());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static
|
|
99
|
+
VALUE set_time_scale(VALUE self, VALUE scale)
|
|
100
|
+
{
|
|
101
|
+
CHECK;
|
|
102
|
+
|
|
103
|
+
THIS->set_time_scale(to<float>(scale));
|
|
104
|
+
return scale;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static
|
|
108
|
+
VALUE get_time_scale(VALUE self)
|
|
109
|
+
{
|
|
110
|
+
CHECK;
|
|
111
|
+
|
|
112
|
+
return value(THIS->time_scale());
|
|
113
|
+
}
|
|
114
|
+
|
|
64
115
|
static
|
|
65
116
|
VALUE set_gain(VALUE self, VALUE gain)
|
|
66
117
|
{
|
|
@@ -116,6 +167,12 @@ Init_beeps_sound_player ()
|
|
|
116
167
|
rb_define_method(cSoundPlayer, "rewind", RUBY_METHOD_FUNC(rewind), 0);
|
|
117
168
|
rb_define_method(cSoundPlayer, "stop", RUBY_METHOD_FUNC(stop), 0);
|
|
118
169
|
rb_define_method(cSoundPlayer, "state", RUBY_METHOD_FUNC(get_state), 0);
|
|
170
|
+
rb_define_method(cSoundPlayer, "position=", RUBY_METHOD_FUNC(set_position), 1);
|
|
171
|
+
rb_define_method(cSoundPlayer, "position", RUBY_METHOD_FUNC(get_position), 0);
|
|
172
|
+
rb_define_method(cSoundPlayer, "time=", RUBY_METHOD_FUNC(set_time), 1);
|
|
173
|
+
rb_define_method(cSoundPlayer, "time", RUBY_METHOD_FUNC(get_time), 0);
|
|
174
|
+
rb_define_method(cSoundPlayer, "time_scale=", RUBY_METHOD_FUNC(set_time_scale), 1);
|
|
175
|
+
rb_define_method(cSoundPlayer, "time_scale", RUBY_METHOD_FUNC(get_time_scale), 0);
|
|
119
176
|
rb_define_method(cSoundPlayer, "gain=", RUBY_METHOD_FUNC(set_gain), 1);
|
|
120
177
|
rb_define_method(cSoundPlayer, "gain", RUBY_METHOD_FUNC(get_gain), 0);
|
|
121
178
|
rb_define_method(cSoundPlayer, "loop=", RUBY_METHOD_FUNC(set_loop), 1);
|
|
@@ -4,6 +4,9 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
tags: ['v[0-9]*']
|
|
6
6
|
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
7
10
|
jobs:
|
|
8
11
|
release:
|
|
9
12
|
runs-on: macos-latest
|
|
@@ -33,23 +36,9 @@ jobs:
|
|
|
33
36
|
echo path=$(ruby -e 'print Dir.glob("*.gem").first') >> $GITHUB_OUTPUT
|
|
34
37
|
|
|
35
38
|
- name: create github release
|
|
36
|
-
id: release
|
|
37
|
-
uses: actions/create-release@v1
|
|
38
39
|
env:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
tag_name: ${{ github.ref }}
|
|
42
|
-
release_name: ${{ github.ref }}
|
|
43
|
-
|
|
44
|
-
- name: upload to github release
|
|
45
|
-
uses: actions/upload-release-asset@v1
|
|
46
|
-
env:
|
|
47
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
48
|
-
with:
|
|
49
|
-
upload_url: ${{ steps.release.outputs.upload_url }}
|
|
50
|
-
asset_path: ./${{ steps.gem.outputs.path }}
|
|
51
|
-
asset_name: ${{ steps.gem.outputs.path }}
|
|
52
|
-
asset_content_type: application/zip
|
|
40
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
41
|
+
run: ruby -I.github/workflows -rutils -e 'release(*ARGV)' ./${{ steps.gem.outputs.path }}
|
|
53
42
|
|
|
54
43
|
- name: upload to rubygems
|
|
55
44
|
env:
|
data/.github/workflows/utils.rb
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
require 'shellwords'
|
|
2
|
+
|
|
3
|
+
ALL_REPO = 'xord/all'
|
|
4
|
+
ALL_DIR = '../all'
|
|
5
|
+
ALL_FETCH_DEPTH = 100
|
|
6
|
+
|
|
1
7
|
RENAMES = {reflex: 'reflexion'}
|
|
2
8
|
|
|
3
9
|
def sh(cmd)
|
|
@@ -5,7 +11,7 @@ def sh(cmd)
|
|
|
5
11
|
system cmd
|
|
6
12
|
end
|
|
7
13
|
|
|
8
|
-
def setup_dependencies(
|
|
14
|
+
def setup_dependencies(only: nil)
|
|
9
15
|
gemspec_path = `git ls-files`.lines(chomp: true).find {|l| l =~ /\.gemspec$/}
|
|
10
16
|
return unless gemspec_path
|
|
11
17
|
|
|
@@ -13,44 +19,109 @@ def setup_dependencies(build: true, only: nil)
|
|
|
13
19
|
name = File.basename gemspec_path, '.gemspec'
|
|
14
20
|
|
|
15
21
|
exts = File.readlines('Rakefile')
|
|
16
|
-
.map {|l| l[%r|^\s*require\W+(\w+)/extension\W+$|, 1]}
|
|
22
|
+
.map {|l| l[%r|^\s*require\W+([\w\-\_]+)/extension\W+$|, 1]}
|
|
17
23
|
.compact
|
|
18
24
|
.reject {|ext| ext == name}
|
|
19
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
|
|
20
76
|
|
|
77
|
+
def setup_dependencies_via_each_repo_by_version(gemspec, exts)
|
|
21
78
|
exts.each do |ext|
|
|
22
79
|
gem = RENAMES[ext.to_sym].then {|s| s || ext}
|
|
23
|
-
ver = gemspec[/add_dependency.*['"]#{gem}['"].*['"]\s
|
|
80
|
+
ver = gemspec[/add_dependency.*['"]#{gem}['"].*['"]\s*~>\s*([\d\.]+)\s*['"]/, 1]
|
|
24
81
|
opts = '-c advice.detachedHead=false --depth 1'
|
|
25
82
|
clone = "git clone #{opts} https://github.com/xord/#{ext}.git ../#{ext}"
|
|
26
83
|
|
|
27
84
|
# 'rake subtree:push' pushes all subrepos, so cloning by new tag
|
|
28
85
|
# often fails before tagging each new tag
|
|
29
86
|
sh %( #{clone} --branch v#{ver} || #{clone} )
|
|
30
|
-
sh %( cd ../#{ext} && rake ext )
|
|
31
87
|
end
|
|
32
88
|
end
|
|
33
89
|
|
|
34
90
|
def tag_versions()
|
|
35
|
-
|
|
36
|
-
|
|
91
|
+
changes = changelogs
|
|
92
|
+
tags = `git tag`.lines chomp: true
|
|
93
|
+
vers = `git log --oneline ./VERSION`
|
|
37
94
|
.lines(chomp: true)
|
|
38
95
|
.map {|line| line.split.first[/^\w+$/]}
|
|
39
|
-
.map {|
|
|
40
|
-
.select {|ver,
|
|
96
|
+
.map {|sha| [`git cat-file -p #{sha}:./VERSION 2>/dev/null`[/[\d\.]+/], sha]}
|
|
97
|
+
.select {|ver, sha| ver && sha}
|
|
41
98
|
.reverse
|
|
42
99
|
.to_h
|
|
43
100
|
|
|
44
|
-
|
|
45
|
-
.split(/^\s*##\s*\[\s*v([\d\.]+)\s*\].*$/)
|
|
46
|
-
.slice(1..-1)
|
|
47
|
-
.each_slice(2)
|
|
48
|
-
.to_h
|
|
49
|
-
.transform_values(&:strip!)
|
|
50
|
-
|
|
51
|
-
vers.to_a.reverse.each do |ver, hash|
|
|
101
|
+
vers.to_a.reverse.each do |ver, sha|
|
|
52
102
|
tag = "v#{ver}"
|
|
53
103
|
break if tags.include?(tag)
|
|
54
|
-
sh %( git tag -a -m \"#{changes[
|
|
104
|
+
sh %( git tag -a -m \"#{changes[tag]&.gsub '"', '\\"'}\" #{tag} #{sha} )
|
|
55
105
|
end
|
|
56
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
|
data/ChangeLog.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
# beeps ChangeLog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [v0.3.13] - 2026-05-17
|
|
5
|
+
|
|
6
|
+
- Rewrite README.md
|
|
7
|
+
- CI: Migrate release-gem.yml from actions/create-release to gh release create
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [v0.3.12] - 2026-05-10
|
|
11
|
+
|
|
12
|
+
- Support WebAssembly
|
|
13
|
+
- Add adaptive stream buffering to prevent audio underruns
|
|
14
|
+
- Add position(), time(), time_scale() to SoundPlayer and seekable() to Processor
|
|
15
|
+
- Add constructors, clear(), and append() to Signals as public API
|
|
16
|
+
- Move set_buffering_seconds() from Filter to Processor
|
|
17
|
+
- Rename max_segment_size_for_process() to get_max_segment_size_for_process()
|
|
18
|
+
- Bump AudioFile from 1.1.1 to 1.1.4 to fix Windows CI
|
|
19
|
+
- Remove deprecated has_rdoc= from gemspecs
|
|
20
|
+
|
|
21
|
+
- Fix SignalSamples copying channel 0 data to all channels
|
|
22
|
+
|
|
23
|
+
|
|
4
24
|
## [v0.3.11] - 2026-04-17
|
|
5
25
|
|
|
6
26
|
- Update dependencies
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Beeps -
|
|
1
|
+
# Beeps - Audio synthesis and playback library
|
|
2
2
|
|
|
3
3
|
[](https://deepwiki.com/xord/beeps)
|
|
4
4
|

|
|
@@ -21,18 +21,37 @@ Thanks for your support! 🙌
|
|
|
21
21
|
|
|
22
22
|
## 🚀 About
|
|
23
23
|
|
|
24
|
-
**Beeps** is a
|
|
24
|
+
**Beeps** is an audio library for Ruby. You build a processor graph — oscillators, file / mic input, filters, envelopes, effects — by wiring nodes together with the `>>` operator, then render the result to a `Sound` and play it back.
|
|
25
25
|
|
|
26
|
-
It is
|
|
26
|
+
It is part of the `xord/*` family and underlies the audio support in [Reflex](https://github.com/xord/reflex), [Processing](https://github.com/xord/processing), [RubySketch](https://github.com/xord/rubysketch), and [Reight](https://github.com/xord/reight). Like the rest of the family, it is primarily developed for our own use, but it works as a standalone audio synthesis gem.
|
|
27
|
+
|
|
28
|
+
## 📋 Requirements
|
|
29
|
+
|
|
30
|
+
- Ruby **3.0.0** or later
|
|
31
|
+
- A C++ compiler with C++20 support
|
|
32
|
+
- [Xot](https://rubygems.org/gems/xot) and [Rucy](https://rubygems.org/gems/rucy) (declared as runtime dependencies)
|
|
33
|
+
- Platform audio backend:
|
|
34
|
+
- **macOS** — OpenAL and AVFoundation (bundled with the OS)
|
|
35
|
+
- **Windows** — OpenAL (`MINGW_PACKAGE_PREFIX-openal`) and Media Foundation
|
|
36
|
+
- **Linux** — `libopenal-dev`
|
|
37
|
+
|
|
38
|
+
The following third-party DSP libraries are cloned from GitHub and statically linked while the native extension is being built, so you do not need to install them separately:
|
|
39
|
+
|
|
40
|
+
| Library | Role |
|
|
41
|
+
| ----------------------------------------------------------------------------- | ----------------------------------- |
|
|
42
|
+
| [STK](https://github.com/thestk/stk) | Core synthesis primitives |
|
|
43
|
+
| [AudioFile](https://github.com/adamstark/AudioFile) | WAV / AIFF file I/O |
|
|
44
|
+
| [r8brain-free-src](https://github.com/avaneev/r8brain-free-src) | High-quality sample-rate conversion |
|
|
45
|
+
| [signalsmith-stretch](https://github.com/Signalsmith-Audio/signalsmith-stretch) | Time stretching and pitch shifting |
|
|
27
46
|
|
|
28
47
|
## 📦 Installation
|
|
29
48
|
|
|
30
49
|
Add this line to your Gemfile:
|
|
31
50
|
```ruby
|
|
32
|
-
|
|
51
|
+
gem 'beeps'
|
|
33
52
|
```
|
|
34
53
|
|
|
35
|
-
Then
|
|
54
|
+
Then install:
|
|
36
55
|
```bash
|
|
37
56
|
$ bundle install
|
|
38
57
|
```
|
|
@@ -42,7 +61,131 @@ Or install it directly:
|
|
|
42
61
|
$ gem install beeps
|
|
43
62
|
```
|
|
44
63
|
|
|
64
|
+
`require 'beeps'` automatically calls `Beeps.init!` and registers `Beeps.fin!` at exit. Set `$BEEPS_NOAUTOINIT = true` before requiring if you want to manage the lifetime yourself.
|
|
65
|
+
|
|
66
|
+
## 📚 What's Included
|
|
67
|
+
|
|
68
|
+
### Generators (signal sources)
|
|
69
|
+
|
|
70
|
+
| Class | Purpose |
|
|
71
|
+
| -------------------- | ------------------------------------------------------------------------ |
|
|
72
|
+
| `Beeps::Oscillator` | Sine / triangle / square / sawtooth / noise / sample-playback oscillator |
|
|
73
|
+
| `Beeps::Value` | Constant or linearly-interpolated control value over time |
|
|
74
|
+
| `Beeps::Sequencer` | Schedule processors at given offsets and durations |
|
|
75
|
+
| `Beeps::FileIn` | Stream audio from a WAV / AIFF / other supported file |
|
|
76
|
+
| `Beeps::MicIn` | Capture audio from a microphone |
|
|
77
|
+
| `Beeps::TextIn` | Synthesize speech from text |
|
|
78
|
+
|
|
79
|
+
### Filters (processors that take an input)
|
|
80
|
+
|
|
81
|
+
| Class | Purpose |
|
|
82
|
+
| -------------------- | ----------------------------------------------------------------------- |
|
|
83
|
+
| `Beeps::Gain` | Multiply the signal by a gain coefficient |
|
|
84
|
+
| `Beeps::Mixer` | Sum multiple inputs |
|
|
85
|
+
| `Beeps::Envelope` | Attack / Decay / Sustain / Release envelope with `note_on` / `note_off` |
|
|
86
|
+
| `Beeps::LowPass` | Low-pass filter with cutoff frequency |
|
|
87
|
+
| `Beeps::HighPass` | High-pass filter with cutoff frequency |
|
|
88
|
+
| `Beeps::Reverb` | Reverb with `mix`, `room_size`, `damping` controls |
|
|
89
|
+
| `Beeps::TimeStretch` | Change duration without affecting pitch |
|
|
90
|
+
| `Beeps::PitchShift` | Change pitch without affecting duration |
|
|
91
|
+
| `Beeps::Analyser` | FFT analyser — exposes time-domain and spectrum readings |
|
|
92
|
+
|
|
93
|
+
### Playback
|
|
94
|
+
|
|
95
|
+
| Class | Purpose |
|
|
96
|
+
| -------------------- | --------------------------------------------------------------------------- |
|
|
97
|
+
| `Beeps::Sound` | A finite, renderable audio asset (created from a processor or `load_sound`) |
|
|
98
|
+
| `Beeps::SoundPlayer` | Returned by `Sound#play`; supports `pause`, `stop`, `position`, `loop`, ... |
|
|
99
|
+
|
|
100
|
+
## 💡 Usage
|
|
101
|
+
|
|
102
|
+
### Play a 440 Hz sine tone for one second
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
require 'beeps'
|
|
106
|
+
|
|
107
|
+
osc = Beeps::Oscillator.new(:sine, frequency: 440)
|
|
108
|
+
sound = Beeps::Sound.new(osc, 1)
|
|
109
|
+
player = sound.play
|
|
110
|
+
|
|
111
|
+
sleep 1 # keep the script alive while the sound plays
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Build a processor chain with `>>`
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
require 'beeps'
|
|
118
|
+
|
|
119
|
+
# oscillator -> half-volume -> low-pass at 800 Hz -> reverb
|
|
120
|
+
chain =
|
|
121
|
+
Beeps::Oscillator.new(:sawtooth, frequency: 220) >>
|
|
122
|
+
Beeps::Gain.new(0.5) >>
|
|
123
|
+
Beeps::LowPass.new(cutoff: 800) >>
|
|
124
|
+
Beeps::Reverb.new(mix: 0.3, room_size: 0.7)
|
|
125
|
+
|
|
126
|
+
Beeps::Sound.new(chain, 2).play
|
|
127
|
+
sleep 2
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`a >> b` calls `b.add_input(a)` and returns `b`, so the right-most node is the head of the chain you pass to `Sound.new`.
|
|
131
|
+
|
|
132
|
+
### ADSR envelope
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
require 'beeps'
|
|
136
|
+
|
|
137
|
+
osc = Beeps::Oscillator.new(:sine, frequency: 440)
|
|
138
|
+
env = Beeps::Envelope.new(0.05, 0.1, 0.7, 0.3, input: osc)
|
|
139
|
+
env.note_on
|
|
140
|
+
env.note_off 0.5 # release after 0.5 s
|
|
141
|
+
|
|
142
|
+
Beeps::Sound.new(env, 1).play
|
|
143
|
+
sleep 1
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Play and save a wave file
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
require 'beeps'
|
|
150
|
+
|
|
151
|
+
# play an existing file
|
|
152
|
+
player = Beeps.load_sound('drum.wav').play
|
|
153
|
+
|
|
154
|
+
# render a chain and save it to disk
|
|
155
|
+
sound = Beeps::Sound.new(Beeps::Oscillator.new(:square, frequency: 880), 0.5)
|
|
156
|
+
sound.save 'square.wav'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Capture from the microphone and analyse
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
require 'beeps'
|
|
163
|
+
|
|
164
|
+
mic = Beeps::MicIn.new(1) # 1 channel
|
|
165
|
+
analyser = Beeps::Analyser.new(1024, mic) # 1024-point FFT
|
|
166
|
+
mic.start
|
|
167
|
+
|
|
168
|
+
loop do
|
|
169
|
+
sleep 0.1
|
|
170
|
+
puts analyser.spectrum.first(8).map {|v| v.round 3 }.inspect
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 🛠️ Development
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
$ rake lib # build the native C++ library (libbeeps)
|
|
178
|
+
$ rake ext # build the Ruby C extension
|
|
179
|
+
$ rake test # run the test suite
|
|
180
|
+
$ rake doc # generate RDoc from C++ sources
|
|
181
|
+
$ rake # default: builds the extension
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
In the [`xord/all`](https://github.com/xord/all) monorepo you can scope by module, e.g. `rake beeps test`.
|
|
185
|
+
|
|
45
186
|
## 📜 License
|
|
46
187
|
|
|
47
188
|
**Beeps** is licensed under the MIT License.
|
|
48
189
|
See the [LICENSE](./LICENSE) file for details.
|
|
190
|
+
|
|
191
|
+
The third-party DSP libraries listed above retain their own licenses.
|
data/Rakefile
CHANGED
|
@@ -25,7 +25,7 @@ use_external_library 'https://github.com/thestk/stk',
|
|
|
25
25
|
excludes: %w[stk/src/include Tcp Udp Socket Thread Mutex InetWv /Rt]
|
|
26
26
|
|
|
27
27
|
use_external_library 'https://github.com/adamstark/AudioFile',
|
|
28
|
-
tag: '1.1.
|
|
28
|
+
tag: '1.1.4',
|
|
29
29
|
srcdirs: 'NOSRC',
|
|
30
30
|
excludes: %w[examples/ tests/]
|
|
31
31
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.13
|
data/beeps.gemspec
CHANGED
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
ext = Beeps::Extension
|
|
13
|
-
name = ext.name
|
|
13
|
+
name = ext.name true
|
|
14
14
|
rdocs = glob.call *%w[README .doc/ext/**/*.cpp]
|
|
15
15
|
|
|
16
16
|
s.name = name
|
|
@@ -25,14 +25,13 @@ Gem::Specification.new do |s|
|
|
|
25
25
|
s.platform = Gem::Platform::RUBY
|
|
26
26
|
s.required_ruby_version = '>= 3.0.0'
|
|
27
27
|
|
|
28
|
-
s.add_dependency 'xot', '~> 0.3.
|
|
29
|
-
s.add_dependency 'rucy', '~> 0.3.
|
|
28
|
+
s.add_dependency 'xot', '~> 0.3.13'
|
|
29
|
+
s.add_dependency 'rucy', '~> 0.3.13'
|
|
30
30
|
|
|
31
31
|
s.files = `git ls-files`.split $/
|
|
32
32
|
s.executables = s.files.grep(%r{^bin/}) {|f| File.basename f}
|
|
33
33
|
s.test_files = s.files.grep %r{^(test|spec|features)/}
|
|
34
34
|
s.extra_rdoc_files = rdocs.to_a
|
|
35
|
-
s.has_rdoc = true
|
|
36
35
|
|
|
37
36
|
s.metadata['msys2_mingw_dependencies'] = 'openal'
|
|
38
37
|
|
data/ext/beeps/extconf.rb
CHANGED
data/ext/beeps/sound_player.cpp
CHANGED
|
@@ -67,6 +67,63 @@ RUCY_DEF0(get_state)
|
|
|
67
67
|
}
|
|
68
68
|
RUCY_END
|
|
69
69
|
|
|
70
|
+
static
|
|
71
|
+
RUCY_DEF1(set_position, position)
|
|
72
|
+
{
|
|
73
|
+
CHECK;
|
|
74
|
+
|
|
75
|
+
THIS->set_position(to<uint>(position));
|
|
76
|
+
return position;
|
|
77
|
+
}
|
|
78
|
+
RUCY_END
|
|
79
|
+
|
|
80
|
+
static
|
|
81
|
+
RUCY_DEF0(get_position)
|
|
82
|
+
{
|
|
83
|
+
CHECK;
|
|
84
|
+
|
|
85
|
+
return value(THIS->position());
|
|
86
|
+
}
|
|
87
|
+
RUCY_END
|
|
88
|
+
|
|
89
|
+
static
|
|
90
|
+
RUCY_DEF1(set_time, time)
|
|
91
|
+
{
|
|
92
|
+
CHECK;
|
|
93
|
+
|
|
94
|
+
THIS->set_time(to<float>(time));
|
|
95
|
+
return time;
|
|
96
|
+
}
|
|
97
|
+
RUCY_END
|
|
98
|
+
|
|
99
|
+
static
|
|
100
|
+
RUCY_DEF0(get_time)
|
|
101
|
+
{
|
|
102
|
+
CHECK;
|
|
103
|
+
|
|
104
|
+
return value(THIS->time());
|
|
105
|
+
}
|
|
106
|
+
RUCY_END
|
|
107
|
+
|
|
108
|
+
static
|
|
109
|
+
RUCY_DEF1(set_time_scale, scale)
|
|
110
|
+
{
|
|
111
|
+
CHECK;
|
|
112
|
+
|
|
113
|
+
THIS->set_time_scale(to<float>(scale));
|
|
114
|
+
return scale;
|
|
115
|
+
}
|
|
116
|
+
RUCY_END
|
|
117
|
+
|
|
118
|
+
static
|
|
119
|
+
RUCY_DEF0(get_time_scale)
|
|
120
|
+
{
|
|
121
|
+
CHECK;
|
|
122
|
+
|
|
123
|
+
return value(THIS->time_scale());
|
|
124
|
+
}
|
|
125
|
+
RUCY_END
|
|
126
|
+
|
|
70
127
|
static
|
|
71
128
|
RUCY_DEF1(set_gain, gain)
|
|
72
129
|
{
|
|
@@ -126,7 +183,13 @@ Init_beeps_sound_player ()
|
|
|
126
183
|
cSoundPlayer.define_method("pause", pause);
|
|
127
184
|
cSoundPlayer.define_method("rewind", rewind);
|
|
128
185
|
cSoundPlayer.define_method("stop", stop);
|
|
129
|
-
cSoundPlayer.define_method("state",
|
|
186
|
+
cSoundPlayer.define_method("state", get_state);
|
|
187
|
+
cSoundPlayer.define_method("position=", set_position);
|
|
188
|
+
cSoundPlayer.define_method("position", get_position);
|
|
189
|
+
cSoundPlayer.define_method("time=", set_time);
|
|
190
|
+
cSoundPlayer.define_method("time", get_time);
|
|
191
|
+
cSoundPlayer.define_method("time_scale=", set_time_scale);
|
|
192
|
+
cSoundPlayer.define_method("time_scale", get_time_scale);
|
|
130
193
|
cSoundPlayer.define_method("gain=", set_gain);
|
|
131
194
|
cSoundPlayer.define_method("gain", get_gain);
|
|
132
195
|
cSoundPlayer.define_method("loop=", set_loop);
|
data/include/beeps/defs.h
CHANGED
data/include/beeps/filter.h
CHANGED
|
@@ -27,6 +27,8 @@ namespace Beeps
|
|
|
27
27
|
|
|
28
28
|
virtual float gain () const;
|
|
29
29
|
|
|
30
|
+
bool seekable () const override;
|
|
31
|
+
|
|
30
32
|
struct Data;
|
|
31
33
|
|
|
32
34
|
Xot::PImpl<Data> self;
|
|
@@ -70,6 +72,8 @@ namespace Beeps
|
|
|
70
72
|
|
|
71
73
|
virtual operator bool () const override;
|
|
72
74
|
|
|
75
|
+
bool seekable () const override;
|
|
76
|
+
|
|
73
77
|
struct Data;
|
|
74
78
|
|
|
75
79
|
Xot::PImpl<Data> self;
|
|
@@ -303,6 +307,8 @@ namespace Beeps
|
|
|
303
307
|
|
|
304
308
|
virtual operator bool () const override;
|
|
305
309
|
|
|
310
|
+
bool seekable () const override;
|
|
311
|
+
|
|
306
312
|
struct Data;
|
|
307
313
|
|
|
308
314
|
Xot::PImpl<Data> self;
|
data/include/beeps/generator.h
CHANGED
|
@@ -63,6 +63,8 @@ namespace Beeps
|
|
|
63
63
|
|
|
64
64
|
virtual const_iterator end () const;
|
|
65
65
|
|
|
66
|
+
bool seekable () const override;
|
|
67
|
+
|
|
66
68
|
struct Data;
|
|
67
69
|
|
|
68
70
|
Xot::PImpl<Data> self;
|
|
@@ -72,7 +74,7 @@ namespace Beeps
|
|
|
72
74
|
virtual void generate (
|
|
73
75
|
Context* context, Signals* signals, uint* offset) override;
|
|
74
76
|
|
|
75
|
-
virtual int
|
|
77
|
+
virtual int get_max_segment_size_for_process (
|
|
76
78
|
double sample_rate, uint nsamples) const override;
|
|
77
79
|
|
|
78
80
|
};// Value
|
|
@@ -140,6 +142,8 @@ namespace Beeps
|
|
|
140
142
|
|
|
141
143
|
virtual operator bool () const override;
|
|
142
144
|
|
|
145
|
+
bool seekable () const override;
|
|
146
|
+
|
|
143
147
|
struct Data;
|
|
144
148
|
|
|
145
149
|
Xot::PImpl<Data> self;
|
|
@@ -149,7 +153,7 @@ namespace Beeps
|
|
|
149
153
|
virtual void generate (
|
|
150
154
|
Context* context, Signals* signals, uint* offset) override;
|
|
151
155
|
|
|
152
|
-
virtual int
|
|
156
|
+
virtual int get_max_segment_size_for_process (
|
|
153
157
|
double sample_rate, uint nsamples) const override;
|
|
154
158
|
|
|
155
159
|
};// Oscillator
|
|
@@ -240,6 +244,8 @@ namespace Beeps
|
|
|
240
244
|
|
|
241
245
|
virtual operator bool () const override;
|
|
242
246
|
|
|
247
|
+
bool seekable () const override;
|
|
248
|
+
|
|
243
249
|
struct Data;
|
|
244
250
|
|
|
245
251
|
Xot::PImpl<Data> self;
|