@rolandnsharp/signal
v1.0.3
Published
Minimal, composable audio synthesis for live coding.
Readme
Signal API - Pure Mathematical Audio Synthesis
Minimal, composable audio synthesis for live coding.
What Makes Signal Different?
Most JavaScript live coding libraries (like Tidal Cycles, Gibber, Strudel) and tools like Sonic Pi act as frontends that send commands to SuperCollider or other synthesis engines. You're essentially scripting a synthesizer, not programming sound itself.
Signal is different: you're writing the mathematical wave function directly. When you write:
signal('tone', t => Math.sin(2 * Math.PI * 432 * t) * 0.2)You're not calling a synthesizer - you're computing the actual sample values that become the sound wave. You're learning and interacting with the wave function itself, understanding exactly how sound works at the sample level.
This makes Signal ideal for:
- Learning digital signal processing fundamentals
- Understanding how synthesis actually works
- Direct mathematical control over every sample
- Building your own synthesis techniques from first principles
Quick Start
const signal = require('@rolandnsharp/signal');
// Create a sine wave - audio starts automatically!
signal('tone', t => Math.sin(2 * Math.PI * 432 * t) * 0.2);Installation
npm install -g @rolandnsharp/signalLive Coding
signal sessions/example-session.jsEdit example-session.js and save - changes apply immediately!
Core API
Signal Creation
The first parameter is just an arbitrary name for tracking the signal (used for hot reload). It doesn't determine the sound - that's defined by what you chain after it.
// Builder style (preferred)
// 'tone' is just a name - the actual sound is sin(432)
signal('tone').sin(432).gain(0.2)
// You could call it anything:
signal('mySound').sin(432).gain(0.2)
signal('x').sin(432).gain(0.2)
// Custom function - builder style
signal('custom').fn(t => Math.sin(2 * Math.PI * 432 * t) * 0.2)
// Custom function - direct style
signal('custom', t => Math.sin(2 * Math.PI * 432 * t) * 0.2)
// Manual sine wave with amplitude modulation (no chainable methods)
signal('tremolo', t => {
const carrier = Math.sin(2 * Math.PI * 440 * t) // 440 Hz carrier
const lfo = Math.sin(2 * Math.PI * 5 * t) // 5 Hz LFO
const modulator = 0.5 + 0.5 * lfo // Scale LFO to 0-1
return carrier * modulator * 0.2 // Apply modulation
})
// Stereo signal
signal('stereo', {
left: t => signal.sin(432).eval(t),
right: t => signal.sin(435).eval(t)
})Helper Generators
// Builder style (named signal)
// Note: 'bass', 'pad', etc. are just arbitrary names, not presets!
signal('tone').sin(432) // Name: 'tone', Sound: sine wave at 432 Hz
signal('bass').square(110) // Name: 'bass', Sound: square wave at 110 Hz
signal('pad').saw(220) // Name: 'pad', Sound: saw wave at 220 Hz
signal('lead').tri(880) // Name: 'lead', Sound: triangle wave at 880 Hz
signal('noise').noise() // Name: 'noise', Sound: white noise
// Unnamed style (for modulators, internal use)
const lfo = signal.sin(5)
const carrier = signal.square(440)Chainable Methods
// Chain methods after any signal generator
signal('tone').sin(432)
.gain(0.5) // Amplitude
.offset(0.1) // DC offset
.clip(0.8) // Hard clipping
.fold(0.7) // Wavefolder
.modulate(lfo) // AM/RM
.fx(sample => Math.tanh(sample * 3)) // Custom effect
.fx((sample, t) => sample * Math.sin(t)) // Time-varying
.play() // Start playing (auto-starts by default)
.stop() // Stop playing instantly
.stop(2) // Fade out over 2 secondsPlay/Stop Control
// Signals auto-play by default
const bass = signal('bass').sin(110).gain(0.3) // Playing immediately
// Create signal but don't play yet
const melody = signal('melody').sin(440).gain(0.2).stop()
// Play it later
setTimeout(() => melody.play(), 4000)
// Stop and restart
bass.stop() // Instant stop
bass.play() // Unmute
// Fade out smoothly (perfect for live performances)
bass.stop(3) // Fade out over 3 seconds
// Imperative composition
const layers = [
signal('layer1').sin(100).gain(0.2).stop(),
signal('layer2').sin(200).gain(0.15).stop(),
signal('layer3').sin(300).gain(0.1).stop()
]
// Bring in layers over time
layers.forEach((layer, i) => {
setTimeout(() => layer.play(), i * 2000)
})Mixing
const bass = signal.sin(110).gain(0.3)
const harmony = signal.sin(165).gain(0.15)
// Module-level
signal.mix(bass, harmony)
// Chainable
bass.mix(harmony)Stereo
const left = signal.sin(432)
const right = signal.sin(435)
// Module-level
signal.stereo(left, right)
// Chainable
left.stereo(right)Helper Utilities
Rhythm
const signal = require('@rolandnsharp/signal');
const { step, euclidean } = signal;
// Beat/phase info
const { beat, index, phase } = step(t, 120, 16) // 120 BPM, 16th notes
// Euclidean rhythm
const pattern = euclidean(5, 16) // 5 pulses in 16 stepsMelody
const signal = require('@rolandnsharp/signal');
const { freq, mtof, ftom, scales } = signal;
// Scale degree to frequency
freq(432, scales.major, 2) // => 486 Hz (major third)
// MIDI conversions
mtof(69) // => 440 Hz
ftom(440) // => 69Scales
const signal = require('@rolandnsharp/signal');
const { scales } = signal;
scales.major // [0, 2, 4, 5, 7, 9, 11, 12]
scales.minor // [0, 2, 3, 5, 7, 8, 10, 12]
scales.pentatonic // [0, 2, 4, 7, 9, 12]
scales.blues // [0, 3, 5, 6, 7, 10, 12]
// ... and moreEnvelopes
const signal = require('@rolandnsharp/signal');
const { env } = signal;
env.exp(phase, 5) // Exponential decay
env.ramp(phase, 0, 1) // Linear ramp
env.adsr(phase, duration, a, d, s, r) // ADSR envelopeImperative Programming
Signal API works great with loops, arrays, and imperative logic:
Generate Chord with Loop
const signal = require('@rolandnsharp/signal');
const { freq, scales } = signal;
const chordDegrees = [0, 4, 7, 11]; // Major 7th
for (let i = 0; i < chordDegrees.length; i++) {
const f = freq(200, scales.major, chordDegrees[i]);
signal(`chord-${i}`).sin(f).gain(0.1);
}Create Multiple Layers with a Loop
// Create 8 detuned oscillators for a rich pad sound
const baseFreq = 220;
const detuneAmount = 5; // Hz
for (let i = 0; i < 8; i++) {
const detune = (Math.random() * 2 - 1) * detuneAmount; // Random detune
const freq = baseFreq + detune;
signal(`pad-${i}`).sin(freq).gain(0.02);
}Generate Drum Pattern with Nested Loops
const signal = require('@rolandnsharp/signal');
const { step, env } = signal;
// Create multiple kick drums with different patterns
const patterns = [
[1, 0, 0, 0, 1, 0, 0, 0], // Four on the floor
[1, 0, 0, 1, 0, 0, 1, 0], // Syncopated
[1, 0, 1, 0, 1, 0, 1, 0], // Eighth notes
];
for (let i = 0; i < patterns.length; i++) {
const pattern = patterns[i];
signal(`kick-${i}`, t => {
const { index, phase } = step(t, 120, 8);
if (!pattern[index % pattern.length]) return 0;
if (phase > 0.3) return 0;
const pitchEnv = 50 + 80 * Math.exp(-15 * phase);
return Math.sin(2 * Math.PI * pitchEnv * t) * Math.exp(-8 * phase) * 0.3;
}).stop(); // Start stopped, enable manually
}
// Enable just one pattern
// patterns[0] is active by default when you call .play()Build Harmonics
const fundamental = 110;
const harmonicSignals = [];
for (let n = 1; n <= 6; n++) {
const harmonicFreq = fundamental * n;
const amplitude = 1 / n; // Decay
harmonicSignals.push(signal.sin(harmonicFreq).gain(amplitude));
}
signal('rich-tone', signal.mix(...harmonicSignals).gain(0.15));Array Methods
// Generate frequencies
const frequencies = [100, 150, 200, 250, 300];
// Filter to odd harmonics
const odd = frequencies.filter((f, i) => i % 2 === 0);
// Map to signals
const layers = odd.map((f, i) =>
signal.sin(f).gain(0.05 / (i + 1))
);
signal('texture', signal.mix(...layers));Conditional Logic
signal('evolving', t => {
const { beat, index, phase } = step(t, 120, 8);
// Change pattern every 8 beats
let pattern;
if (Math.floor(beat / 8) % 2 === 0) {
pattern = [0, 2, 4, 5]; // Major
} else {
pattern = [0, 3, 5, 7]; // Minor
}
const degree = pattern[index % pattern.length];
const f = freq(330, scales.major, degree);
return signal.sin(f).eval(t) * env.exp(phase, 6) * 0.15;
});Polyrhythms with Loop
const rhythms = [
{ period: 3, freq: 200 },
{ period: 5, freq: 300 },
{ period: 7, freq: 400 }
];
for (const rhythm of rhythms) {
signal(`poly-${rhythm.period}`, t => {
const phase = (t / rhythm.period) % 1;
if (phase > 0.05) return 0;
return signal.sin(rhythm.freq).eval(t) * env.exp(phase * 20, 10) * 0.2;
});
}Programmatic Pattern Generation
function generatePattern(pulses, steps) {
const pattern = [];
for (let i = 0; i < steps; i++) {
const bucket = Math.floor(i * pulses / steps);
const next = Math.floor((i + 1) * pulses / steps);
pattern.push(bucket !== next ? 1 : 0);
}
return pattern;
}
const kickPattern = generatePattern(5, 16);
signal('kick', t => {
const { index, phase } = step(t, 128, 16);
if (!kickPattern[index % kickPattern.length]) return 0;
const f = 50 + 80 * env.exp(phase, 20);
return signal.sin(f).eval(t) * env.exp(phase, 10) * 0.4;
});See imperative-session.js for more examples.
Examples
Tremolo
const lfo = signal.sin(3).gain(0.5).offset(0.5)
signal('tremolo').sin(432).modulate(lfo).gain(0.2)Distorted Bass
signal('bass').sin(110)
.fx(sample => Math.tanh(sample * 3))
.gain(0.3)Melodic Sequencer
const signal = require('@rolandnsharp/signal');
const { step, freq, env, scales } = signal;
signal('melody', t => {
const { index, phase } = step(t, 120, 8) // 8th notes
const pattern = [0, 2, 4, 2, 5, 4, 2, 0]
const degree = pattern[index % pattern.length]
const f = freq(432, scales.major, degree)
const envelope = env.exp(phase, 5)
return signal.sin(f).eval(t) * envelope * 0.2
})Euclidean Kick
const signal = require('@rolandnsharp/signal');
const { step, euclidean, env } = signal;
signal('kick', t => {
const { index, phase } = step(t, 120, 16)
const pattern = euclidean(5, 16)
if (!pattern[index % pattern.length]) return 0
const pitchEnv = 100 * env.exp(phase, 15)
const f = 50 + pitchEnv
return signal.sin(f).eval(t) * env.exp(phase, 8) * 0.4
})Signal Management
signal.clear() // Remove all signals
signal.remove('name') // Remove specific signal
signal.list() // List all signal names
signal.stopAudio() // Stop audio outputPhilosophy
- Pure functions - Signals are
Time → Samplefunctions - Explicit math - You write the DSP, helpers just reduce boilerplate
- Composable - Chain operations, mix signals
- Live coding - Hot reload, named signals
- Minimal API - Only what you can't easily write yourself
Files
index.js- Core Signal APIrhythm.js- Beat and pattern helpersmelody.js- Frequency and scale helpersscales.js- Scale definitionsenvelopes.js- Envelope shapesrunner.js- Hot reload runnerbuilder-session.js- Builder style examples (recommended)example-session.js- Live coding exampleperformance-session.js- Live performance layer controlimperative-session.js- Imperative programming examplestest-session.js- API teststest-builder.js- Builder syntax teststest-start-stop.js- Start/stop functionality tests
