wavify 0.1.0 → 0.2.0
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/CHANGELOG.md +40 -1
- data/README.md +204 -22
- data/exe/wavify +6 -0
- data/lib/wavify/adapters.rb +81 -0
- data/lib/wavify/audio.rb +1066 -120
- data/lib/wavify/cli.rb +237 -0
- data/lib/wavify/codecs/aiff.rb +388 -87
- data/lib/wavify/codecs/base.rb +98 -5
- data/lib/wavify/codecs/flac.rb +660 -170
- data/lib/wavify/codecs/ogg_vorbis.rb +515 -155
- data/lib/wavify/codecs/raw.rb +235 -45
- data/lib/wavify/codecs/registry.rb +272 -10
- data/lib/wavify/codecs/wav.rb +453 -74
- data/lib/wavify/core/duration.rb +54 -2
- data/lib/wavify/core/format.rb +96 -11
- data/lib/wavify/core/sample_buffer.rb +648 -60
- data/lib/wavify/core/stream.rb +590 -34
- data/lib/wavify/dsl.rb +712 -100
- data/lib/wavify/dsp/automation.rb +109 -0
- data/lib/wavify/dsp/effects/auto_pan.rb +71 -0
- data/lib/wavify/dsp/effects/bitcrusher.rb +74 -0
- data/lib/wavify/dsp/effects/chorus.rb +15 -4
- data/lib/wavify/dsp/effects/compressor.rb +133 -23
- data/lib/wavify/dsp/effects/delay.rb +46 -1
- data/lib/wavify/dsp/effects/distortion.rb +3 -2
- data/lib/wavify/dsp/effects/effect_base.rb +69 -2
- data/lib/wavify/dsp/effects/effect_chain.rb +94 -0
- data/lib/wavify/dsp/effects/envelope_controlled_effect.rb +93 -0
- data/lib/wavify/dsp/effects/eq.rb +102 -0
- data/lib/wavify/dsp/effects/expander.rb +90 -0
- data/lib/wavify/dsp/effects/flanger.rb +112 -0
- data/lib/wavify/dsp/effects/limiter.rb +259 -0
- data/lib/wavify/dsp/effects/mastering_chain.rb +31 -0
- data/lib/wavify/dsp/effects/noise_gate.rb +72 -0
- data/lib/wavify/dsp/effects/phaser.rb +112 -0
- data/lib/wavify/dsp/effects/podcast_chain.rb +32 -0
- data/lib/wavify/dsp/effects/reverb.rb +100 -14
- data/lib/wavify/dsp/effects/soft_limiter.rb +55 -0
- data/lib/wavify/dsp/effects/stereo_widener.rb +73 -0
- data/lib/wavify/dsp/effects/tremolo.rb +66 -0
- data/lib/wavify/dsp/effects/vibrato.rb +102 -0
- data/lib/wavify/dsp/effects.rb +106 -0
- data/lib/wavify/dsp/envelope.rb +90 -19
- data/lib/wavify/dsp/filter.rb +149 -8
- data/lib/wavify/dsp/headroom.rb +57 -0
- data/lib/wavify/dsp/lfo.rb +65 -0
- data/lib/wavify/dsp/loudness_meter.rb +281 -0
- data/lib/wavify/dsp/oscillator.rb +261 -22
- data/lib/wavify/dsp/processor.rb +90 -0
- data/lib/wavify/errors.rb +2 -2
- data/lib/wavify/sequencer/engine.rb +405 -89
- data/lib/wavify/sequencer/note_sequence.rb +45 -7
- data/lib/wavify/sequencer/pattern.rb +121 -12
- data/lib/wavify/sequencer/track.rb +149 -10
- data/lib/wavify/version.rb +1 -1
- data/lib/wavify.rb +18 -0
- data/sig/audio.rbs +86 -0
- data/sig/codecs.rbs +77 -0
- data/sig/core.rbs +108 -0
- data/sig/dsl.rbs +32 -0
- data/sig/dsp.rbs +87 -0
- data/sig/effects.rbs +129 -0
- data/sig/public_api.yml +111 -0
- data/sig/sequencer.rbs +126 -0
- data/sig/stream.rbs +30 -0
- data/sig/wavify.rbs +24 -0
- metadata +44 -58
- data/.serena/.gitignore +0 -1
- data/.serena/memories/project_overview.md +0 -5
- data/.serena/memories/style_and_completion.md +0 -5
- data/.serena/memories/suggested_commands.md +0 -11
- data/.serena/project.yml +0 -126
- data/.simplecov +0 -18
- data/.yardopts +0 -4
- data/Rakefile +0 -190
- data/benchmarks/README.md +0 -46
- data/benchmarks/benchmark_helper.rb +0 -112
- data/benchmarks/dsp_effects_benchmark.rb +0 -46
- data/benchmarks/flac_benchmark.rb +0 -74
- data/benchmarks/streaming_memory_benchmark.rb +0 -94
- data/benchmarks/wav_io_benchmark.rb +0 -110
- data/examples/audio_processing.rb +0 -73
- data/examples/cinematic_transition.rb +0 -118
- data/examples/drum_machine.rb +0 -74
- data/examples/format_convert.rb +0 -81
- data/examples/hybrid_arrangement.rb +0 -165
- data/examples/streaming_master_chain.rb +0 -129
- data/examples/synth_pad.rb +0 -42
- data/tools/fixture_writer.rb +0 -85
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: faee1fe4af50cc79c060be1ff707954c8569d099bc29e6e9ca00a3769902de55
|
|
4
|
+
data.tar.gz: 8cbec03ebfb36f2a90adda64ac7866b8b6c32d26eb7239c5cfff958033a23d58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebd9f41c36d264872830a83c60e90efbc55a245378b0be1ab4f2d238bbbb1d822b7120df0b29a742986baba56aacc5bfa4caec229378b9422534990b51fbc4e6
|
|
7
|
+
data.tar.gz: 0da527d00c870300883e3dee77da574df351d93f8714b48668b3291171a01ae88e790b955f7b3cedcd46f0c9b4479861063780332d87b0ca9a9990441820884b
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-07-16
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added the `wavify` CLI with info, conversion, tone generation, processing, DSL rendering/timeline, format, and dependency-diagnostic commands.
|
|
14
|
+
- Expanded `Audio` with timeline editing, repeat, channel/bit-depth/sample-rate conversion, dither, sample/frame mapping, stereo controls, configurable mixing, value semantics, and peak/RMS/LUFS/true-peak analysis.
|
|
15
|
+
- Expanded streaming pipelines with named steps, duration windows, meters, progress, tees, materialization, dry runs, and reset/flush/latency handling for stateful processors.
|
|
16
|
+
- Added public codec metadata and reversible codec registries, strict magic/extension checks, filename hints for IO, codec-specific options, and atomic no-overwrite output.
|
|
17
|
+
- Added WAV LIST/INFO, cue, loop, BWF, RF64, valid-bit, and channel-layout metadata; AIFF-C, marker, and instrument-loop support; FLAC comments, stereo coding, LPC, and adaptive Rice encoding; and configurable raw PCM/float byte order and numeric domains.
|
|
18
|
+
- Added `Automation`, `LFO`, BS.1770 `LoudnessMeter`, `Processor`, and headroom helpers, plus limiter, gate, expansion, modulation, stereo, EQ, bitcrusher, and mastering/podcast chain effects.
|
|
19
|
+
- Expanded the sequencer and DSL with swing, velocity/probability/ratchet notation, note durations and ties, keys/scales, chord inversions and voicings, sample transforms, stems, repeatable tempo/meter sections, markers, explicit rests, timeline text/JSON, validation, and reproducible seeds.
|
|
20
|
+
- Added format valid-bit/channel-layout metadata (including explicit unknown layouts), lazy sample-buffer views, optional packed storage, cached value hashing, and thread-safe value equality.
|
|
21
|
+
- Added reversible effect registration and optional adapter discovery for external codec, MIDI, and analysis integrations.
|
|
22
|
+
- Added namespace-split RBS signatures, public API parity checks, documentation guides, expanded examples, randomized/property tests, and branch/per-file coverage gates.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Made OGG Vorbis an optional dependency; chained/interleaved streams now decode incrementally where possible, resample to a common rate, and spool multi-stream input outside the Ruby heap.
|
|
27
|
+
- Standardized codec read/write/stream/metadata contracts and processor `flush(format:)` behavior, including defined stream-source reuse semantics and isolated offline DSP runtimes.
|
|
28
|
+
- Kept mixing, automation, effect chains, and sequencer buses in floating-point workspaces until their output boundary, with signal-aware headroom and linked master limiting.
|
|
29
|
+
- Improved oscillator generation with polyBLEP waveforms and band-limited/interpolated wavetables, and improved FLAC compression with Levinson–Durbin LPC and adaptive Rice partition search.
|
|
30
|
+
- Reduced allocations in audio analysis, format conversion, slicing, streaming codecs, OGG demultiplexing, and sequencer voice rendering while bounding eager allocations and parser resource use.
|
|
31
|
+
- Moved general `InvalidParameterError` failures outside the DSP-specific error hierarchy.
|
|
32
|
+
- Made gem packaging independent of Git metadata, tied release links to the version tag, strengthened release installation checks, and made the default Rake task run the local quality gate.
|
|
33
|
+
- Expanded CI across supported Ruby/platform/dependency combinations, pinned actions/runners, strengthened lint and coverage checks, and added repeatable benchmark reports.
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
37
|
+
- Removed the no-op OGG `decode_mode:` option; OGG reads now always perform full decoding.
|
|
38
|
+
- Removed `Audio#loop`/`loop!`; use `repeat`/`repeat!` instead.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Fixed PCM endpoint conversion, resampling, duration parsing, zero-duration fades, clipping detection, metadata coordinate projection, packed-buffer concurrency, and destructive writes to caller-owned IO.
|
|
43
|
+
- Fixed WAV extensible/RF64/overflow handling, AIFF sample-width and bounded-read behavior, FLAC checksum validation, raw stream alignment, and OGG parser/tempfile cleanup and mixed-rate chaining.
|
|
44
|
+
- Fixed stream duration limits, reusable and non-rewindable source handling, processor error propagation, and cross-platform atomic path output.
|
|
45
|
+
- Fixed oscillator continuity/noise behavior, envelope release handling, filter stability, and state consistency across chunked DSP processing.
|
|
46
|
+
- Fixed sequencer note/chord release and rest timing, duplicate-track validation, chord parsing, and clipping across voices and simultaneous tracks.
|
|
47
|
+
|
|
9
48
|
## [0.1.0] - 2026-03-04
|
|
10
49
|
|
|
11
|
-
- Initial release
|
|
50
|
+
- Initial release
|
data/README.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
# Wavify
|
|
1
|
+
# Wavify [](https://badge.fury.io/rb/wavify) [](https://github.com/ydah/wavify/actions/workflows/ci.yml)
|
|
2
2
|
|
|
3
3
|
Wavify is a Ruby audio processing toolkit with immutable transforms, codec I/O, streaming pipelines, DSP effects, and a sequencing DSL.
|
|
4
4
|
|
|
5
|
+
Use it to:
|
|
6
|
+
|
|
7
|
+
- Read, inspect, transform, and write audio from Ruby scripts.
|
|
8
|
+
- Process large files with streaming pipelines and stateful DSP effects.
|
|
9
|
+
- Generate tones, small arrangements, and test fixtures without mandatory FFmpeg or SoX.
|
|
10
|
+
|
|
5
11
|
## Requirements
|
|
6
12
|
|
|
7
13
|
- Ruby `>= 3.1`
|
|
8
14
|
- Bundler
|
|
9
|
-
-
|
|
15
|
+
- Optional native build environment for OGG Vorbis gems (`ogg-ruby`, `vorbis`)
|
|
10
16
|
|
|
11
17
|
## Installation
|
|
12
18
|
|
|
@@ -28,6 +34,8 @@ Or install directly:
|
|
|
28
34
|
gem install wavify
|
|
29
35
|
```
|
|
30
36
|
|
|
37
|
+
WAV, AIFF, FLAC, and raw PCM work without OGG dependencies. Add `ogg-ruby` and `vorbis` to your Gemfile only when you need `.ogg` / `.oga` support.
|
|
38
|
+
|
|
31
39
|
## Quick Start
|
|
32
40
|
|
|
33
41
|
```ruby
|
|
@@ -42,7 +50,16 @@ audio = Wavify::Audio.tone(
|
|
|
42
50
|
format: format
|
|
43
51
|
)
|
|
44
52
|
|
|
45
|
-
audio.fade_in(0.02).fade_out(0.05).write("tone.wav")
|
|
53
|
+
audio.fade_in(0.02, curve: :exp).fade_out(0.05, curve: :log).with_bit_depth(16, dither: true).write("tone.wav")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Codec-specific write options are forwarded with `codec_options:`:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
audio.write("master.flac", codec_options: { block_size: 2048 })
|
|
60
|
+
audio.write("preview.ogg", codec_options: { quality: 0.5 })
|
|
61
|
+
audio.write("tagged.wav", codec_options: { info: { title: "Tone", artist: "Wavify" } })
|
|
62
|
+
audio.write("master.wav", overwrite: false)
|
|
46
63
|
```
|
|
47
64
|
|
|
48
65
|
## Core API
|
|
@@ -51,87 +68,195 @@ audio.fade_in(0.02).fade_out(0.05).write("tone.wav")
|
|
|
51
68
|
|
|
52
69
|
Main constructors:
|
|
53
70
|
|
|
54
|
-
- `Audio.read(
|
|
55
|
-
- `Audio.
|
|
71
|
+
- `Audio.read(path_or_io, format: nil, codec_options: {}, strict: false, filename: nil)`
|
|
72
|
+
- `Audio.metadata(path_or_io, format: nil, codec_options: {}, strict: false, filename: nil)`
|
|
73
|
+
- `Audio.info(path_or_io, format: nil, codec_options: {}, strict: false, filename: nil)`
|
|
74
|
+
- `Audio.stream(path_or_io, chunk_size: 4096, format: nil, codec_options: {}, strict: false, filename: nil)`
|
|
56
75
|
- `Audio.tone(frequency:, duration:, waveform:, format:)`
|
|
57
76
|
- `Audio.silence(duration_seconds, format:)`
|
|
58
|
-
- `Audio.mix(*audios)`
|
|
77
|
+
- `Audio.mix(*audios, strategy: :clip, gains: nil, align: :start, format: nil, work_format: nil, headroom_smoothing: 0.005)`
|
|
59
78
|
|
|
60
79
|
Immutable transforms (each also has `!` in-place variants):
|
|
61
80
|
|
|
62
|
-
- `gain`, `normalize`, `trim`, `fade_in`, `fade_out`, `pan`, `reverse`, `
|
|
81
|
+
- `gain`, `normalize`, `trim`, `fade_in`, `fade_out`, `pan`, `reverse`, `repeat`, `apply`
|
|
82
|
+
- `concat`, `append`, `prepend`, `overlay`, `crossfade`, `slice`, `crop`, `pad_start`, `pad_end`, `insert_silence`
|
|
83
|
+
- `to_mono`, `to_stereo`, `resample`, `with_bit_depth`, `map_samples`, `map_frames`
|
|
63
84
|
|
|
64
85
|
Utility methods:
|
|
65
86
|
|
|
66
|
-
- `convert`, `split(at:)`, `
|
|
87
|
+
- `convert`, `split(at:)`, `duration`, `sample_frame_count`, `channels`, `sample_rate`, `bit_depth`, `frames`, `each_frame`
|
|
88
|
+
- `peak_amplitude`, `rms_amplitude`, `peak_dbfs`, `rms_dbfs`, `lufs`, `stats`, `silent?`, `clipped?`, `dc_offset`, `zero_crossing_rate`
|
|
89
|
+
|
|
90
|
+
Mix strategies are `:clip` (default), `:normalize`, `:headroom`, `:soft_limit`, and `:none`. The `:none` strategy leaves the float workspace unclipped, but a PCM output `format:` still clamps during final conversion; select a float output format to retain values outside -1.0..1.0 for downstream limiting. Headroom is intended for brief overlaps, while sustained over-full-scale mixes should use `Wavify::DSP::Limiter`. `gains:` accepts one dB value per source, and `align:` can be `:start`, `:center`, or `:end`.
|
|
91
|
+
Normalize modes are `:peak`, `:rms`, and `:lufs`.
|
|
92
|
+
Use `with_bit_depth(16, dither: true)` when reducing PCM bit depth and you want simple TPDF dither.
|
|
93
|
+
For large immutable buffers that are not immediately transformed, pass `storage: :packed` to `SampleBuffer.new`; sequential enumeration stays packed until random sample access is requested.
|
|
94
|
+
|
|
95
|
+
Duration helpers:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
Wavify.ms(250)
|
|
99
|
+
Wavify.seconds(3)
|
|
100
|
+
Wavify::Core::Duration.parse("1:23.456")
|
|
101
|
+
```
|
|
67
102
|
|
|
68
103
|
### Streaming pipeline
|
|
69
104
|
|
|
70
105
|
```ruby
|
|
71
106
|
Wavify::Audio.stream("input.wav", chunk_size: 4096)
|
|
72
|
-
.pipe(Wavify::Effects::Compressor.new(threshold: -18, ratio: 3.0))
|
|
73
|
-
.
|
|
107
|
+
.pipe(Wavify::Effects::Compressor.new(threshold: -18, ratio: 3.0), name: :comp)
|
|
108
|
+
.map_chunks(name: :trim_preview) { |chunk| Wavify::Audio.new(chunk).trim(threshold: 0.005).buffer }
|
|
109
|
+
.meter { |stats| puts stats[:peak_dbfs] }
|
|
110
|
+
.tee("preview.wav")
|
|
74
111
|
.write_to("output.aiff", format: Wavify::Core::Format::CD_QUALITY)
|
|
75
112
|
```
|
|
76
113
|
|
|
77
114
|
`pipe` accepts processors that respond to `call`, `process`, or `apply`.
|
|
115
|
+
Stateful processors may implement `reset`, `flush(format:)`, and `tail_duration`. A processor that exposes `flush` must accept the `format:` keyword; Wavify invokes it exactly once at the end of a pass.
|
|
116
|
+
Use `take_duration`, `drop_duration`, `to_audio`, `progress`, `meter`, `tee`, and `pipeline_steps` for common chunk workflows and inspection.
|
|
117
|
+
Use `dry_run(format:)` to validate a stream pipeline without writing output.
|
|
118
|
+
|
|
119
|
+
`write_to` also accepts codec-specific output options:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
stream.write_to("output.flac", codec_options: { block_size_strategy: :fixed, block_size: 2048 })
|
|
123
|
+
```
|
|
78
124
|
|
|
79
125
|
## Format Support
|
|
80
126
|
|
|
81
127
|
| Format | Read | Write | Stream Read | Stream Write | Notes |
|
|
82
128
|
|--------|------|-------|-------------|--------------|-------|
|
|
83
|
-
| WAV | ✅ | ✅ | ✅ | ✅ | PCM + float WAV,
|
|
84
|
-
| AIFF | ✅ | ✅ | ✅ | ✅ | PCM
|
|
85
|
-
| FLAC | ✅ | ✅ | ✅ | ✅ | Pure Ruby implementation |
|
|
86
|
-
| OGG Vorbis | ✅ | ✅ | ✅ | ✅ |
|
|
129
|
+
| WAV | ✅ | ✅ | ✅ | ✅ | PCM + float WAV, extensible WAV, BWF metadata, RF64 read metadata |
|
|
130
|
+
| AIFF | ✅ | ✅ | ✅ | ✅ | PCM AIFF plus uncompressed AIFF-C `NONE` / `sowt` |
|
|
131
|
+
| FLAC | ✅ | ✅ | ✅ | ✅ | Pure Ruby implementation with comments, mid-side, and LPC write options |
|
|
132
|
+
| OGG Vorbis | ✅ | ✅ | ✅ | ✅ | Optional `ogg-ruby` + `vorbis` gems |
|
|
87
133
|
| Raw PCM/Float | ✅* | ✅ | ✅* | ✅ | `format:` is required for read/stream-read/metadata |
|
|
88
134
|
|
|
89
135
|
Raw example:
|
|
90
136
|
|
|
91
137
|
```ruby
|
|
92
138
|
raw_format = Wavify::Core::Format.new(channels: 2, sample_rate: 44_100, bit_depth: 16, sample_format: :pcm)
|
|
139
|
+
raw_float_format = raw_format.with(bit_depth: 32, sample_format: :float)
|
|
93
140
|
audio = Wavify::Audio.read("input.pcm", format: raw_format)
|
|
141
|
+
big_endian = Wavify::Audio.read(
|
|
142
|
+
"input.pcm",
|
|
143
|
+
format: raw_format,
|
|
144
|
+
codec_options: { endianness: :big, signed: true }
|
|
145
|
+
)
|
|
146
|
+
ieee_float = Wavify::Audio.read(
|
|
147
|
+
"samples.raw",
|
|
148
|
+
format: raw_float_format,
|
|
149
|
+
codec_options: { float_domain: :ieee }
|
|
150
|
+
)
|
|
94
151
|
audio.write("output.wav")
|
|
95
152
|
```
|
|
96
153
|
|
|
154
|
+
For IO objects without magic bytes, pass `filename:` as a codec hint:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
io = StringIO.new(raw_bytes)
|
|
158
|
+
audio = Wavify::Audio.read(io, filename: "input.raw", format: raw_format)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Metadata example:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
metadata = Wavify::Audio.metadata("input.wav")
|
|
165
|
+
metadata[:format].sample_rate
|
|
166
|
+
metadata[:duration]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Codec registry helpers:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Wavify::Codecs.supported_formats
|
|
173
|
+
Wavify::Codecs.available_formats
|
|
174
|
+
Wavify::Codecs.detect("input.wav")
|
|
175
|
+
Wavify::Codecs.register(".custom", MyCodec)
|
|
176
|
+
Wavify::Codecs.register(".custom", MyCodec, magic: "CSTM", priority: 10)
|
|
177
|
+
Wavify::Codecs.unregister(".custom")
|
|
178
|
+
Wavify::Adapters.known
|
|
179
|
+
```
|
|
180
|
+
|
|
97
181
|
### OGG Vorbis notes
|
|
98
182
|
|
|
99
183
|
- `read` / `stream_read` support sequential chained streams and interleaved multi-stream OGG.
|
|
184
|
+
- Single-logical-stream `stream_read` incrementally processes pages, packets, native synthesis, and output chunks.
|
|
185
|
+
- Chained/interleaved inputs demultiplex logical pages into bounded-memory temporary spools, then decode packets and emit mixed/resampled chunks incrementally.
|
|
100
186
|
- Interleaved multi-stream decode is mixed into one output stream.
|
|
101
187
|
- If interleaved streams have different sample rates, they are resampled to the first logical stream's sample rate before mix.
|
|
102
|
-
-
|
|
188
|
+
- OGG support is optional; `wavify doctor` reports whether the native gems are installed.
|
|
189
|
+
- MP3, AAC, FFmpeg, MIDI, and spectrogram support are adapter-gem boundaries; use `Wavify::Adapters.load(:ffmpeg)` after installing a matching adapter gem.
|
|
190
|
+
|
|
191
|
+
Native support matrix:
|
|
192
|
+
|
|
193
|
+
| Platform | Core codecs | OGG Vorbis native CI | Support status |
|
|
194
|
+
|----------|-------------|-----------------------|----------------|
|
|
195
|
+
| Linux (Ubuntu 24.04) | Tested | Tested with `libogg-dev` / `libvorbis-dev` | Supported |
|
|
196
|
+
| macOS 14 | Tested | Not run | Core supported; OGG native best-effort |
|
|
197
|
+
| Windows Server 2022 | Tested | Not run | Core supported; OGG native best-effort |
|
|
103
198
|
|
|
104
199
|
## Sequencer DSL
|
|
105
200
|
|
|
106
201
|
Use `Wavify.build` for one-shot rendering/writing, or `Wavify::DSL.build_definition` when you want timeline access.
|
|
107
202
|
|
|
108
203
|
```ruby
|
|
109
|
-
song = Wavify::DSL.build_definition(format: Wavify::Core::Format::CD_QUALITY, tempo: 116, default_bars: 2) do
|
|
204
|
+
song = Wavify::DSL.build_definition(format: Wavify::Core::Format::CD_QUALITY, tempo: 116, swing: 0.55, default_bars: 2) do
|
|
205
|
+
sample_folder "samples"
|
|
206
|
+
key :c, :minor
|
|
207
|
+
|
|
110
208
|
track :kick do
|
|
111
209
|
synth :sine
|
|
112
|
-
notes "C2
|
|
210
|
+
notes "C2/8. . C2/8t .", resolution: 16
|
|
113
211
|
envelope attack: 0.001, decay: 0.05, sustain: 0.0, release: 0.06
|
|
114
212
|
gain(-4)
|
|
115
213
|
end
|
|
116
214
|
|
|
215
|
+
track :pad do
|
|
216
|
+
chords ["Cmaj7/E@drop2"], voicing: :open
|
|
217
|
+
end
|
|
218
|
+
|
|
117
219
|
arrange do
|
|
118
|
-
section :intro, bars: 1, tracks: %i[kick]
|
|
220
|
+
section :intro, bars: 1, tracks: %i[kick], markers: [:start]
|
|
221
|
+
section :bridge, bars: 1, tracks: %i[kick pad], tempo: 92, beats_per_bar: 3, markers: [:bridge]
|
|
119
222
|
end
|
|
120
223
|
end
|
|
121
224
|
|
|
122
225
|
timeline = song.timeline
|
|
226
|
+
song.timeline_json
|
|
227
|
+
song.timeline_text
|
|
228
|
+
stems = song.render(stems: true)
|
|
123
229
|
mix = song.render
|
|
124
230
|
mix.write("song.wav")
|
|
125
231
|
```
|
|
126
232
|
|
|
233
|
+
Pattern steps support rests (`-`/`.`), normal triggers (`x`, velocity `0.8`), accents (`X`, velocity `1.0`), explicit velocity suffixes (`x0.5`), probability rolls (`x?50`), and ratchets (`x:3`). Pass `random_seed:` to the DSL entrypoint for reproducible probability rolls. Trigger patterns render audio on sample-backed DSL tracks; synth tracks use note or chord events.
|
|
234
|
+
Note tokens support fixed durations (`C4/8`), dotted values (`C4/8.`), triplets (`C4/8t`), and ties (`D4~ D4`).
|
|
235
|
+
Use `key :c, :minor` for simple scale quantization, slash chords for inversions, and `@drop2` / `@open` or `voicing:` for chord voicings.
|
|
236
|
+
Arrangement sections can carry `tempo:`, `beats_per_bar:`, and `markers:`.
|
|
237
|
+
Swing values start at `0.5` for straight timing; values such as `0.55` delay off-beat steps on even grids. Odd resolutions use straight timing because they do not form complete swing pairs.
|
|
238
|
+
Notes, ties, envelope releases, and effect tails may continue beyond a bar or section boundary. They are rendered with the tempo at note-on and continue even if the track is inactive in the following section.
|
|
239
|
+
Sample tracks can use `sample_folder`, per-sample `pitch:` semitones, `preset :lofi_drums`, and `Wavify::DSL.validate(deep: true)` for pre-render file/codec checks. Pass `safe_paths: true` to restrict sample paths to the configured sample folder.
|
|
240
|
+
|
|
127
241
|
## DSP
|
|
128
242
|
|
|
129
243
|
Built-in modules:
|
|
130
244
|
|
|
131
|
-
- Oscillator waveforms: `:sine`, `:square`, `:sawtooth`, `:triangle`, `:white_noise`, `:pink_noise`
|
|
132
|
-
- Envelope (
|
|
245
|
+
- Oscillator waveforms: `:sine`, `:square`, `:sawtooth`, `:triangle`, `:pulse`, `:white_noise`, `:pink_noise`
|
|
246
|
+
- Envelope (AHDSR with optional segment curves)
|
|
247
|
+
- Automation and LFO modulation helpers
|
|
133
248
|
- Biquad filters (lowpass/highpass/bandpass/notch/peaking/shelves)
|
|
134
|
-
- Effects: `Delay`, `Reverb`, `Chorus`, `Distortion`, `Compressor`
|
|
249
|
+
- Effects: `Delay`, `Reverb`, `Chorus`, `Vibrato`, `Flanger`, `Phaser`, `Distortion`, `Compressor`, `Limiter`, `SoftLimiter`, `NoiseGate`, `Expander`, `Tremolo`, `AutoPan`, `StereoWidener`, `Bitcrusher`, `EQ`
|
|
250
|
+
- Preset chains: `MasteringChain`, `PodcastChain`
|
|
251
|
+
|
|
252
|
+
Register custom processors for pipelines and DSL tracks:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
Wavify::Effects.register(:my_effect, MyEffect)
|
|
256
|
+
Wavify::DSL.effect(:my_effect, MyEffect)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`Envelope` supports `hold:` and `curve: :linear | :exp | :log`. `Reverb` supports `pre_delay:` for delaying only the wet path and `width:` for stereo wet width.
|
|
135
260
|
|
|
136
261
|
## Examples
|
|
137
262
|
|
|
@@ -141,6 +266,7 @@ Scripts in `examples/`:
|
|
|
141
266
|
- `examples/audio_processing.rb`
|
|
142
267
|
- `examples/synth_pad.rb`
|
|
143
268
|
- `examples/drum_machine.rb`
|
|
269
|
+
- `examples/chill_vibes.rb`
|
|
144
270
|
- `examples/hybrid_arrangement.rb`
|
|
145
271
|
- `examples/streaming_master_chain.rb`
|
|
146
272
|
- `examples/cinematic_transition.rb`
|
|
@@ -151,6 +277,50 @@ Run:
|
|
|
151
277
|
ruby examples/synth_pad.rb
|
|
152
278
|
```
|
|
153
279
|
|
|
280
|
+
See `examples/README.md` for the full list.
|
|
281
|
+
|
|
282
|
+
## CLI
|
|
283
|
+
|
|
284
|
+
The gem includes a small CLI for common scripting tasks:
|
|
285
|
+
|
|
286
|
+
`wavify render` and `wavify timeline` evaluate song files as Ruby. Only run trusted song files; this is not a sandbox for untrusted input.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
wavify info input.wav
|
|
290
|
+
wavify convert input.wav output.flac
|
|
291
|
+
wavify tone --freq 440 --duration 1 tone.wav
|
|
292
|
+
wavify normalize input.wav output.wav --target -1
|
|
293
|
+
wavify trim input.wav output.wav --threshold 0.01
|
|
294
|
+
wavify chain input.wav output.wav --gain -3 --fade-in 0.02 --fade-out 0.05
|
|
295
|
+
wavify render song.rb out.wav --tempo 120 --swing 0.55 --bars 4 --seed 123
|
|
296
|
+
wavify timeline song.rb --tempo 120 --bars 4 --seed 123
|
|
297
|
+
wavify formats
|
|
298
|
+
wavify doctor
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Documentation
|
|
302
|
+
|
|
303
|
+
- `docs/getting-started.md`
|
|
304
|
+
- `docs/codecs.md`
|
|
305
|
+
- `docs/dsp.md`
|
|
306
|
+
- `docs/streaming.md`
|
|
307
|
+
- `docs/sequencer.md`
|
|
308
|
+
- `docs/limitations.md`
|
|
309
|
+
- `docs/performance.md`
|
|
310
|
+
- `ROADMAP.md`
|
|
311
|
+
- YARD docs can be generated with `bundle exec rake docs:yard`.
|
|
312
|
+
|
|
313
|
+
## Limitations
|
|
314
|
+
|
|
315
|
+
- MP3, AAC, and M4A are not built into core.
|
|
316
|
+
- FFmpeg and SoX are not mandatory runtime dependencies.
|
|
317
|
+
- OGG Vorbis uses optional native gems (`ogg-ruby`, `vorbis`).
|
|
318
|
+
- Raw PCM/float requires `format:` for read, stream read, and metadata.
|
|
319
|
+
- Streaming writes for header-based formats require seekable output IO.
|
|
320
|
+
- Non-rewindable IO stream sources are single-use; path and rewindable IO sources can be enumerated repeatedly.
|
|
321
|
+
- Resampling defaults to linear interpolation; pass `resampler: :windowed_sinc` for higher-quality offline conversion.
|
|
322
|
+
- LUFS measurement and normalization use BS.1770 K-weighting with absolute and relative gating. `Limiter` provides stereo-linked lookahead peak control.
|
|
323
|
+
|
|
154
324
|
## Development
|
|
155
325
|
|
|
156
326
|
Install dependencies:
|
|
@@ -162,10 +332,16 @@ bundle install
|
|
|
162
332
|
Run tests:
|
|
163
333
|
|
|
164
334
|
```bash
|
|
335
|
+
bundle exec rake
|
|
165
336
|
bundle exec rspec
|
|
166
|
-
|
|
337
|
+
SIMPLECOV_BRANCH=1 COVERAGE_MINIMUM=90 COVERAGE_BRANCH_MINIMUM=70 \
|
|
338
|
+
COVERAGE_MINIMUM_PER_FILE=60 COVERAGE_BRANCH_MINIMUM_PER_FILE=40 \
|
|
339
|
+
bundle exec rake spec:coverage
|
|
340
|
+
bundle exec rake types:validate
|
|
167
341
|
```
|
|
168
342
|
|
|
343
|
+
The default `rake` task runs specs, lint, RBS/API parity validation, and the documentation gate.
|
|
344
|
+
|
|
169
345
|
Generate/check docs:
|
|
170
346
|
|
|
171
347
|
```bash
|
|
@@ -175,6 +351,8 @@ YARD_MINIMUM=85 bundle exec rake docs:check
|
|
|
175
351
|
bundle exec rake docs:all
|
|
176
352
|
```
|
|
177
353
|
|
|
354
|
+
`docs:examples` syntax-checks every example and smoke-runs the shorter scripts. Set `EXAMPLES=all` to run all examples end to end. `docs:yard` generates YARD output under `doc/`. `docs:check` enforces the configured YARD documentation percentage.
|
|
355
|
+
|
|
178
356
|
Benchmarks:
|
|
179
357
|
|
|
180
358
|
```bash
|
|
@@ -183,6 +361,8 @@ bundle exec rake bench:dsp
|
|
|
183
361
|
bundle exec rake bench:flac
|
|
184
362
|
bundle exec rake bench:stream
|
|
185
363
|
bundle exec rake bench:all
|
|
364
|
+
bundle exec rake bench:baseline
|
|
365
|
+
bundle exec rake bench:compare
|
|
186
366
|
```
|
|
187
367
|
|
|
188
368
|
Release checks:
|
|
@@ -191,6 +371,8 @@ Release checks:
|
|
|
191
371
|
bundle exec rake release:check
|
|
192
372
|
```
|
|
193
373
|
|
|
374
|
+
The release check builds the gem, installs it into an isolated `GEM_HOME`, then verifies `require "wavify"`, CLI help, and packaged public files.
|
|
375
|
+
|
|
194
376
|
## License
|
|
195
377
|
|
|
196
378
|
Wavify is released under the MIT License.
|
data/exe/wavify
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wavify
|
|
4
|
+
# Catalog and loader for optional adapter gems that live outside the core gem.
|
|
5
|
+
module Adapters
|
|
6
|
+
AdapterSpec = Struct.new(:name, :kind, :gem_name, :require_path, :formats, :summary, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
KNOWN = [
|
|
9
|
+
AdapterSpec.new(
|
|
10
|
+
name: :ffmpeg,
|
|
11
|
+
kind: :codec,
|
|
12
|
+
gem_name: "wavify-ffmpeg",
|
|
13
|
+
require_path: "wavify/ffmpeg",
|
|
14
|
+
formats: %w[mp3 aac m4a wav flac ogg],
|
|
15
|
+
summary: "FFmpeg-backed codec adapter for formats that should not be mandatory core dependencies."
|
|
16
|
+
),
|
|
17
|
+
AdapterSpec.new(
|
|
18
|
+
name: :mp3,
|
|
19
|
+
kind: :codec,
|
|
20
|
+
gem_name: "wavify-mp3",
|
|
21
|
+
require_path: "wavify/mp3",
|
|
22
|
+
formats: %w[mp3],
|
|
23
|
+
summary: "MP3 codec adapter."
|
|
24
|
+
),
|
|
25
|
+
AdapterSpec.new(
|
|
26
|
+
name: :midi,
|
|
27
|
+
kind: :sequencer,
|
|
28
|
+
gem_name: "wavify-midi",
|
|
29
|
+
require_path: "wavify/midi",
|
|
30
|
+
formats: %w[mid midi],
|
|
31
|
+
summary: "MIDI import/export adapter for sequencer timelines."
|
|
32
|
+
),
|
|
33
|
+
AdapterSpec.new(
|
|
34
|
+
name: :spectrogram,
|
|
35
|
+
kind: :analysis,
|
|
36
|
+
gem_name: "wavify-spectrogram",
|
|
37
|
+
require_path: "wavify/spectrogram",
|
|
38
|
+
formats: %w[png json],
|
|
39
|
+
summary: "Spectrogram and FFT analysis adapter."
|
|
40
|
+
)
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# @return [Array<AdapterSpec>]
|
|
45
|
+
def known
|
|
46
|
+
KNOWN.dup
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param name [String, Symbol]
|
|
50
|
+
# @return [AdapterSpec, nil]
|
|
51
|
+
def find(name)
|
|
52
|
+
normalized = normalize_name(name)
|
|
53
|
+
KNOWN.find { |adapter| adapter.name == normalized }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Requires an optional adapter gem and lets it register itself.
|
|
57
|
+
#
|
|
58
|
+
# @param name [String, Symbol]
|
|
59
|
+
# @return [true]
|
|
60
|
+
def load(name)
|
|
61
|
+
adapter = find(name)
|
|
62
|
+
raise InvalidParameterError, "unknown adapter: #{name.inspect}" unless adapter
|
|
63
|
+
|
|
64
|
+
require adapter.require_path
|
|
65
|
+
true
|
|
66
|
+
rescue LoadError
|
|
67
|
+
raise UnsupportedFormatError,
|
|
68
|
+
"adapter #{adapter.name} is not installed; add gem #{adapter.gem_name.inspect} " \
|
|
69
|
+
"and require #{adapter.require_path.inspect}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def normalize_name(name)
|
|
75
|
+
name.to_sym
|
|
76
|
+
rescue NoMethodError
|
|
77
|
+
raise InvalidParameterError, "adapter name must be String or Symbol"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|