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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +204 -22
  4. data/exe/wavify +6 -0
  5. data/lib/wavify/adapters.rb +81 -0
  6. data/lib/wavify/audio.rb +1066 -120
  7. data/lib/wavify/cli.rb +237 -0
  8. data/lib/wavify/codecs/aiff.rb +388 -87
  9. data/lib/wavify/codecs/base.rb +98 -5
  10. data/lib/wavify/codecs/flac.rb +660 -170
  11. data/lib/wavify/codecs/ogg_vorbis.rb +515 -155
  12. data/lib/wavify/codecs/raw.rb +235 -45
  13. data/lib/wavify/codecs/registry.rb +272 -10
  14. data/lib/wavify/codecs/wav.rb +453 -74
  15. data/lib/wavify/core/duration.rb +54 -2
  16. data/lib/wavify/core/format.rb +96 -11
  17. data/lib/wavify/core/sample_buffer.rb +648 -60
  18. data/lib/wavify/core/stream.rb +590 -34
  19. data/lib/wavify/dsl.rb +712 -100
  20. data/lib/wavify/dsp/automation.rb +109 -0
  21. data/lib/wavify/dsp/effects/auto_pan.rb +71 -0
  22. data/lib/wavify/dsp/effects/bitcrusher.rb +74 -0
  23. data/lib/wavify/dsp/effects/chorus.rb +15 -4
  24. data/lib/wavify/dsp/effects/compressor.rb +133 -23
  25. data/lib/wavify/dsp/effects/delay.rb +46 -1
  26. data/lib/wavify/dsp/effects/distortion.rb +3 -2
  27. data/lib/wavify/dsp/effects/effect_base.rb +69 -2
  28. data/lib/wavify/dsp/effects/effect_chain.rb +94 -0
  29. data/lib/wavify/dsp/effects/envelope_controlled_effect.rb +93 -0
  30. data/lib/wavify/dsp/effects/eq.rb +102 -0
  31. data/lib/wavify/dsp/effects/expander.rb +90 -0
  32. data/lib/wavify/dsp/effects/flanger.rb +112 -0
  33. data/lib/wavify/dsp/effects/limiter.rb +259 -0
  34. data/lib/wavify/dsp/effects/mastering_chain.rb +31 -0
  35. data/lib/wavify/dsp/effects/noise_gate.rb +72 -0
  36. data/lib/wavify/dsp/effects/phaser.rb +112 -0
  37. data/lib/wavify/dsp/effects/podcast_chain.rb +32 -0
  38. data/lib/wavify/dsp/effects/reverb.rb +100 -14
  39. data/lib/wavify/dsp/effects/soft_limiter.rb +55 -0
  40. data/lib/wavify/dsp/effects/stereo_widener.rb +73 -0
  41. data/lib/wavify/dsp/effects/tremolo.rb +66 -0
  42. data/lib/wavify/dsp/effects/vibrato.rb +102 -0
  43. data/lib/wavify/dsp/effects.rb +106 -0
  44. data/lib/wavify/dsp/envelope.rb +90 -19
  45. data/lib/wavify/dsp/filter.rb +149 -8
  46. data/lib/wavify/dsp/headroom.rb +57 -0
  47. data/lib/wavify/dsp/lfo.rb +65 -0
  48. data/lib/wavify/dsp/loudness_meter.rb +281 -0
  49. data/lib/wavify/dsp/oscillator.rb +261 -22
  50. data/lib/wavify/dsp/processor.rb +90 -0
  51. data/lib/wavify/errors.rb +2 -2
  52. data/lib/wavify/sequencer/engine.rb +405 -89
  53. data/lib/wavify/sequencer/note_sequence.rb +45 -7
  54. data/lib/wavify/sequencer/pattern.rb +121 -12
  55. data/lib/wavify/sequencer/track.rb +149 -10
  56. data/lib/wavify/version.rb +1 -1
  57. data/lib/wavify.rb +18 -0
  58. data/sig/audio.rbs +86 -0
  59. data/sig/codecs.rbs +77 -0
  60. data/sig/core.rbs +108 -0
  61. data/sig/dsl.rbs +32 -0
  62. data/sig/dsp.rbs +87 -0
  63. data/sig/effects.rbs +129 -0
  64. data/sig/public_api.yml +111 -0
  65. data/sig/sequencer.rbs +126 -0
  66. data/sig/stream.rbs +30 -0
  67. data/sig/wavify.rbs +24 -0
  68. metadata +44 -58
  69. data/.serena/.gitignore +0 -1
  70. data/.serena/memories/project_overview.md +0 -5
  71. data/.serena/memories/style_and_completion.md +0 -5
  72. data/.serena/memories/suggested_commands.md +0 -11
  73. data/.serena/project.yml +0 -126
  74. data/.simplecov +0 -18
  75. data/.yardopts +0 -4
  76. data/Rakefile +0 -190
  77. data/benchmarks/README.md +0 -46
  78. data/benchmarks/benchmark_helper.rb +0 -112
  79. data/benchmarks/dsp_effects_benchmark.rb +0 -46
  80. data/benchmarks/flac_benchmark.rb +0 -74
  81. data/benchmarks/streaming_memory_benchmark.rb +0 -94
  82. data/benchmarks/wav_io_benchmark.rb +0 -110
  83. data/examples/audio_processing.rb +0 -73
  84. data/examples/cinematic_transition.rb +0 -118
  85. data/examples/drum_machine.rb +0 -74
  86. data/examples/format_convert.rb +0 -81
  87. data/examples/hybrid_arrangement.rb +0 -165
  88. data/examples/streaming_master_chain.rb +0 -129
  89. data/examples/synth_pad.rb +0 -42
  90. data/tools/fixture_writer.rb +0 -85
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0573e324cb4eb544c7a82ed254dd7fcd473439747b5c675ab474457d5cad1fec
4
- data.tar.gz: a8659118673abb114e917ce23c5c8e332338962f87fe42eec9bab562628b4af9
3
+ metadata.gz: faee1fe4af50cc79c060be1ff707954c8569d099bc29e6e9ca00a3769902de55
4
+ data.tar.gz: 8cbec03ebfb36f2a90adda64ac7866b8b6c32d26eb7239c5cfff958033a23d58
5
5
  SHA512:
6
- metadata.gz: 197cda12919d9acb3a80b17cb6526a15e2fc5b2c8c442144f299d938d0306f0b0bc332ce57da8ec98ebe87a04b11b18b3b157d2ea2bcfbf570d3266b7dde855d
7
- data.tar.gz: 96432c4aadea84b31a83a231678dde7faed327246e45fa7d34ca4a5cc16d0ab1da200faaef4ab7f0980ec6de33402bf1f5a82e628b7921c5447447c3793f5f41
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 [![Gem Version](https://badge.fury.io/rb/wavify.svg)](https://badge.fury.io/rb/wavify) [![Ruby](https://github.com/ydah/wavify/actions/workflows/ci.yml/badge.svg)](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
- - Native build environment for gems with C extensions (`ogg-ruby`, `vorbis`)
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(path, format: nil, codec_options: {})`
55
- - `Audio.stream(path_or_io, chunk_size: 4096, format: nil, codec_options: {})`
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`, `loop`, `apply`
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:)`, `peak_amplitude`, `rms_amplitude`, `duration`, `sample_frame_count`
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
- .pipe(Wavify::Effects::Chorus.new(rate: 0.5, depth: 0.2, mix: 0.15))
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, including extensible WAV |
84
- | AIFF | ✅ | ✅ | ✅ | ✅ | PCM only (AIFC unsupported) |
85
- | FLAC | ✅ | ✅ | ✅ | ✅ | Pure Ruby implementation |
86
- | OGG Vorbis | ✅ | ✅ | ✅ | ✅ | Backed by `ogg-ruby` + `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
- - `decode_mode: :strict` and `decode_mode: :placeholder` are accepted for API compatibility.
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 . . . C2 . . .", resolution: 16
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 (ADSR)
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
- bundle exec rake spec:coverage COVERAGE_MINIMUM=90
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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "wavify"
5
+
6
+ exit Wavify::CLI.run
@@ -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