Pure Intonation on Browser: Building a Sequencer with Ruby
The talk in RubyKaigi 2026.
Explore pure intonation, microtonal music theory, and ruby.wasm interoperability to create a modular synthesizer and sequencer running in the browser using Web Audio API and Ruby.
˒ Delay, Reverb ˒ Chord Editor ˒ Edit Chord based on Shasaf-Style Theory (I will introduce later) ˒ Sequencer ˒ Arbitrary number of tracks ˒ Mute and Volume adjustments per track ˒ Arpeggiator ˒ Rhythm pattern editor puri fi ed-synth
5:4) ˒ Achieves crystal clear chords with zero dissonance ˒ Pure resonance without the “beats” (interference) Pure Intonation Perfect Fifth 3:2 12-EDO: G:C = 27/12 ≈ 1.498 㱺 acoustic interference
quantized to N-EDO. ˒ The Mission: I want to play chords with exact frequency ratios. ˒ The Solution: As a programmer, I can built a tool to achieve it. Motivation: Real Harmony
AudioNodeWrapper def initialize(ctx, native_node) @ctx = ctx @native_node = native_node end def connect(destination) if destination.is_a?(AudioNodeWrapper) @native_node.connect(destination.native_node) elsif destination.is_a?(JS::Object) @native_node.connect(destination) end end def disconnect(destination = nil) if destination.is_a?(AudioNodeWrapper) @native_node.disconnect(destination.native_node) elsif destination.is_a?(JS::Object) @native_node.disconnect(destination) end end end More on this later
end def self.m2(obj) raise 'unhandled error' end end class J { js_func(obj) { return obj.call("m2", vm.wrap(this)); } } const test = vm.eval("RubyTest"); const obj = new J(); const result = test.call("m1", vm.wrap(obj)); // The fatal error occured
the VM stack are prohibited under nested VM operation (rb_wasm_handle_jmp_unwind) Nested VM operation means that the call stack has sandwitched JS frames like JS -> Ruby -> JS -> Ruby caused by something like `window.rubyVM.eval("JS.global[:rubyVM].eval('Fiber.yield')")` Please check your call stack and make sure that you are **not** doing any of the following inside the nested Ruby frame: 1. Switching fibers (e.g. Fiber#resume, Fiber.yield, and Fiber#transfer) Note that `evalAsync` JS API switches fibers internally 2. Raising uncaught exceptions Please catch all exceptions inside the nested operation 3. Calling Continuation APIs