Slide 1

Slide 1 text

Pure Intonation on Browser:
 Building a Sequencer with Ruby @nagachika

Slide 2

Slide 2 text

˒ @nagachika ˒ CRuby committer / CRuby stable branch maintainer (3.4) ˒ Write "ruby trunk changes” every day ˒ ANDPAD Inc ˒ Fukuoka.rb ˒ Sound Programmer Wannabe WHO AM I

Slide 3

Slide 3 text

˒ @nagachika ˒ CRuby committer / CRuby stable branch maintainer (3.4) ˒ Write "ruby trunk changes” every day ˒ ANDPAD Inc ˒ Fukuoka.rb ˒ Sound Programmer Wannabe ← Today’s Theme WHO AM I

Slide 4

Slide 4 text

˒ Introduction of “puri fi ed-synth” ˒ About Music Theory underlying puri fi ed-synth ˒ Interoperability of ruby.wasm on browser Agenda

Slide 5

Slide 5 text

Introduction
 of
 puri fi ed-synth

Slide 6

Slide 6 text

˒ Synthesizer + Sequencer based on … ˒ Pure Intonation (७ਖ਼཯) / Microtonal (ඍ෼Ի) ˒ Web Audio API + ruby.wasm ˒ Runs in the browser ˒ https://nagachika.github.io/ruby-wasm-puri fi ed-synth/ puri fi ed-synth

Slide 7

Slide 7 text

˒ Synthesizer ˒ Modular Synthesizer ˒ Oscillator, Filters, ADSR, Gain… ˒ 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

Slide 8

Slide 8 text

About Music Theory underlying puri fi ed-synth

Slide 9

Slide 9 text

˒ Modern Western music is entirely based on 12-EDO (12ฏۉ཯) 12-EDO (Equal Division of an octave) C D E F G A B Do Re Mi Fa Sol La Si C# 1 Octave = x2 [Hz] C:C# = C#:D = D:D# = D#:E = E:F =… x x C : C# = 1 :

Slide 10

Slide 10 text

˒ Music using intervals smaller than a standard semitone/half step. ˒ 31-EDO / 53-EDO (Dividing the octave into 31/53 steps) Microtonal Music cf. https://youtu.be/QUJ2oND3cdg Microtonal Keyboard: Terpstra Keyboard

Slide 11

Slide 11 text

˒ Harmonics based on simple integer frequency ratios (e.g., 3:2, 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

Slide 12

Slide 12 text

˒ Proposed by LΛMPLIGHT †1 ˒ Rede fi ning musical intervals as “Dimensions” ˒ 1D: Octave, 2D: Perfect 5th (3/2), 3D: Major 3rd (5/4) ˒ Expanding into higher dimensions (4D: 7/4, 5D: 11/4) Shasaf-Style Theory †1 https://x.com/lamplight_music

Slide 13

Slide 13 text

Shasaf-Style Harmony https://youtu.be/q8A7utFpS9E

Slide 14

Slide 14 text

˒ Chord Diagrams - Lattice - Chordnyms ˒ puri fi ed-synth adopt Lattice in chord editor Shasaf-Style Notation “Ahta”

Slide 15

Slide 15 text

˒ The Problem: Even in microtonal DTM, chords are often 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

Slide 16

Slide 16 text

Interoperability of
 ruby.wasm on browser

Slide 17

Slide 17 text

˒ WebAssembly port of CRuby with a WASI-compatible runtime. ˒ Provides libraries to bridge Ruby and JavaScript objects. ruby.wasm

Slide 18

Slide 18 text

˒ A graph-based modular audio synthesis framework ˒ Oscillators ˒ Filters/Effects ˒ Inputs/Outputs ˒ Accurate scheduling and timing Web Audio API Oscillator BiFilter Speaker 🔉

Slide 19

Slide 19 text

˒ Ruby wrapper for JavaScript AudioContext & AudioNode JavaScript AudioNodeWrapper Oscillator BiFilter Speaker 🔉 Ruby Oscillator BiFilter Speaker

Slide 20

Slide 20 text

˒ Ruby wrapper for JavaScript AudioContext & AudioNode AudioNodeWrapper class 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

Slide 21

Slide 21 text

˒ A Class in Web Audio API ˒ Automates parameters instead of using static constants JavaScript AudioParam Oscillator Gain Speaker 🔉 Exponential Amplitude

Slide 22

Slide 22 text

˒ Ruby wrapper for JavaScript AudioParam JavaScript AudioParamWrapper Ruby Gain Speaker 🔉 AudioParam frequency
 [Hz] Oscillator Gain Speaker AudioParam frequency
 [Hz] Oscillator

Slide 23

Slide 23 text

˒ JavaScript Object Proxy in ruby.wasm VM JS::Object obj = JS.global[:window] obj.is_a?(JS::Object) #=> true obj.class #=> ERROR: /bundle/gems/js-2.8.1/lib/js.rb:275:in `invoke_js_method': undefined method `class' for an instance of JS::Object (NoMethodError)

Slide 24

Slide 24 text

˒ Inherits BasicObject JS::Object JS::Object.superclass #=> BasicObject JS::Object.instance_methods(false) #=> [:typeof, :apply, :raise, :strictly_eql?, :==, :to_a, :to_s, :[], :[]=, :to_f, :to_i, :is_a?, :call, :nil?, :eql?, :respond_to?, :new, :hash , :await, :inspect, :method_missing]

Slide 25

Slide 25 text

˒ AudioNodeWrapper again JS::Object class AudioNodeWrapper def connect(destination) (…snip…) elsif destination.is_a?(JS::Object) @native_node.connect(destination) end end end

Slide 26

Slide 26 text

˒ Inherits BasicObject JS::Object JS::Object.superclass #=> BasicObject JS::Object.instance_methods(false) #=> [:typeof, :apply, :raise, :strictly_eql?, :==, :to_a, :to_s, :[], :[]=, :to_f, :to_i, :is_a?, :call, :nil?, :eql?, :respond_to?, :new, :hash , :await, :inspect, :method_missing]

Slide 27

Slide 27 text

˒ method_missing → invoke_js_method → call JS::Object def method_missing(sym, *args, &block) (...snip...) invoke_js_method(sym, *args, &block) end def invoke_js_method(sym, *args, &block) return self.call(sym, *args, &block) if self[sym].typeof == "function" https://github.com/ruby/ruby.wasm/blob/2.9.4/packages/gems/js/lib/js.rb

Slide 28

Slide 28 text

˒ JavaScript objects are wrapped in Ruby space ˒ Manual conversion required to get Ruby primitives ˒ Use #to_i, #to_f, #to_s, etc JS::Object num = JS.eval("42") puts num + 30 # => NoMethodError puts num.to_i + 30 # => 72

Slide 29

Slide 29 text

˒ vm.eval() ˒ call() ˒ vm.wrap() JavaScript → Ruby const ruby_obj = vm.eval(“Const1”); const result = ruby_obj.call(“meth1", vm.wrap(42));

Slide 30

Slide 30 text

JavaScript → Ruby

Slide 31

Slide 31 text

JS 㱻 Ruby trampoline module RubyTest def self.m1(obj) obj.js_func(JS::Object.wrap(self)) end def self.m2(obj) 42 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)); console.log(result); // => 42

Slide 32

Slide 32 text

JS 㱻 Ruby trampoline limitation module RubyTest def self.m1(obj) obj.call(js_func(JS::Object.wrap(self)) 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

Slide 33

Slide 33 text

JS 㱻 Ruby trampoline limitation Ruby APIs that may rewind 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

Slide 34

Slide 34 text

˒ Pass graph by JSON (future plan) Reduce JS 㱻 Ruby conversion JavaScript Oscillator BiFilter Speaker 🔉 Ruby Oscillator BiFilter Speaker JSON JSON Encode Decode

Slide 35

Slide 35 text

˒ JavaScript → Ruby: vm.eval(), call(), vm.wrap() ˒ Ruby → JavaScript: JS.global JS.eval, JS::Object#call ˒ Limitation about nested call stack ˒ JS::Object ˒ JS::Object#call, JS::Object#method_missing(js.gem) ˒ Explicit conversion required with to_i, to_f, to_s Ruby 㱻 JavaScript

Slide 36

Slide 36 text

ruby.wasm unlocks the door to hobby programming
 in the browser with Ruby

Slide 37

Slide 37 text

Web Audio API turns everyone into a creator of sound

Slide 38

Slide 38 text

Happy Hacking
 and 
 Happy Jamming!