Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Conversation

@felixroos
Copy link
Collaborator

@felixroos felixroos commented Jun 1, 2025

this is a poc for implementing a small part of superdough without ties to webaudio. it can be used like this:

doughsamples('github:eddyflux/crate')

all(x=>x.supradough())

$: s("crate_bd*[<4 [8 0]> 0] crate_rim")

$: s("<pink white>*8").dec(.07)
  .rarely(ply("2")).delay(.5)
.hpf(sine.range(200,2000).slow(4)).hpq(.2)

$: note("c,eb,g,bb").s("sine").press()
  .add(note(24))
  .fmi(3).fmh(5.1)
  .dec(.5)
  .delay(".6:<.12 .22>:.7")
  .jux(press).rarely(add(note("12")))
  .lpf(400).lpq(.2).lpd(.4).lpenv(3)
  .fmdecay(.2).fmenv(1)


$: chord("<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>").voicing()
  .s("<sine>").clip(1).rel(.4)
  .gain(.4)
  .fm(1.4).att(1).lpa(.5).lpf(200).lpenv(4)

the supradough method completely bypasses superdough and spawns an instance of the dough-processor worklet DoughVoice a for each event. DoughVoice spawning and despawning is handled by Dough, which runs in the long-lived dough-processor worklet.
the Dough class is self contained, so it could be used to render a strudel pattern in any environment that supports js (e.g. node.js) + it would be easy to prerender a strudel tune from a pattern, without the use of the web audio api. the dough.mjs contains ugens from kabelsalat. we don't really need a graph, as superdough is static.

superdough controls

sounds

  • sine
  • tri(angle)
  • saw(tooth)
  • zaw
  • square
  • supersaw
    • stereo
  • pulse
  • white
  • pink
  • brown
  • crackle, dust
  • samples
  •  bank
  • gain
  • postgain
  • orbit
  • velocity
  • n
  • pitch zones

fx

  •  distort:distortvol
  • penv, pattack, pdecay, psustain, prelease
  • fm(i) / fmh
  • fmenv, fmattack, fmdecay, fmsustain, fmrelease
  • cutoff (lpf) / resonance (lpq)
  •  lpenv, lpattack, lpdecay, lpsustain, lprelease
  •  hcutoff / hresonance
  •  hpenv, hpattack, hpdecay, hpsustain, hprelease
  •  bandf, bandq
  •  bpenv, bpattack, bpdecay, bpsustain, bprelease
  • vib / vibmod
  •  phaserrate, phasersweep, phasercenter, phaserdepth
  •  room, roomfade, roomlp, roomdim, roomsize, ir

more fx

  • delay
  • delayfeedback
  • delaytime
  • density -> dust
  •  coarse
  •  crush
  •  pan
  •  shape, shapevol
  •  vowel
  • ftype (filter model)
  • drive (for ladder filter)
  • fanchor

other

  • fft
  •  analyze
  • i (whats this?)

@felixroos felixroos requested a review from daslyfe June 1, 2025 17:17
@daslyfe
Copy link
Collaborator

daslyfe commented Jun 2, 2025

this looks excellent, I will try it out tomorrow morning

@daslyfe
Copy link
Collaborator

daslyfe commented Jun 3, 2025

Works pretty well! I noticed that with long release times it ends in a popping sound,
$: note("c eb g bb").add(note(12)) .lpf(sine.range(600,7000).slow(2)).lpq(0.2) .s("sawtooth") .att(.3).release(1) .supradough()

volume could also be lowered to match superdough volume. The filter sounds really tasty

@daslyfe
Copy link
Collaborator

daslyfe commented Jun 4, 2025

fixed the release envelope, volume, and clicking issue :)

@daslyfe
Copy link
Collaborator

daslyfe commented Jun 4, 2025

this rocks

@felixroos
Copy link
Collaborator Author

fixed the release envelope, volume, and clicking issue :)

nice! i failed to find the bug with my sleep deprived brain on the way back :D

i still have question marks on how to use this tactic to implement the rest of superdough, mainly:

  1. global fx
  2. samples
  3. ??

i see 2 potential ways:

  1. spawn only a single worklet and send haps as postMessages + do voice allocation in dough
  2. keep spawning one worklet per hap, implement fx in separate long living worklet + wire them together

imo 1 would be the "purest" approach, making it maximally portable. not sure what's more performant, but i guess we have to try

@felixroos
Copy link
Collaborator Author

ok i've implemented 1 and it seems to be working :) i think i like it. the Dough class could now handle global fx as well

@felixroos
Copy link
Collaborator Author

image

web audio glue is now super minimal :)

@felixroos
Copy link
Collaborator Author

i've added another poc (dough-export.mjs) to show how to use Dough in a node.js script to render the audio for a pattern

}

let externalWorklets = [];
export function registerWorklet(url) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've added this to be able to register worklets from the outside without depending on them from superdough

@felixroos
Copy link
Collaborator Author

still a bit of refactoring to do, to make sure stereo samples (and eventually stereo waveforms) are processed accordingly.. all the local effects ugens need to exist once for every channel i guess. envelopes are only needed once. also not sure how to best integrate the existing sample map loading logic

case 'decay': {
let time = curTime - this.startTime;
let curVal = lerp(time / decay, 1, susVal, -this.curve);
let curVal = lerp(time / decay, 1, susVal, -this.decayCurve);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious: why only decay and not attack / release ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be on the release and decay stage, the reason is that seems to be standard on most synthesizers so I played around with different curves and preferred the standard linear rate on attack, it was not scientific

@felixroos
Copy link
Collaborator Author

still a bit of refactoring to do, to make sure stereo samples (and eventually stereo waveforms) are processed accordingly.. all the local effects ugens need to exist once for every channel i guess. envelopes are only needed once.

fx now work channelwise! so stereo samples should now work

@felixroos
Copy link
Collaborator Author

also not sure how to best integrate the existing sample map loading logic

the doughsamples function can now be used to load a sample map:

doughsamples('github:eddyflux/crate')
all(x=>x.supradough())
$: s("crate_bd*[4 0] crate_rim, crate_hh*4")

for now, all samples are immediately loaded into memory! not sure if this is a good idea.. also n is not supported yet.. so far only the first sample for each key is loaded. also, pitch zones are not implemented yet

@felixroos
Copy link
Collaborator Author

felixroos commented Jun 9, 2025

new demo pattern:

doughsamples('github:eddyflux/crate')

all(x=>x.supradough())

$: s("crate_bd*[<4 [8 0]> 0] crate_rim")

$: s("<pink white>*8").dec(.07)
  .rarely(ply("2")).delay(.5)
.hpf(sine.range(200,2000).slow(4)).hpq(.2)

$: note("c,eb,g,bb").s("sine").press()
  .add(note(24))
  .fmi(3).fmh(5.1)
  .dec(.5)
  .delay(".6:<.12 .22>:.7")
  .jux(press).rarely(add(note("12")))
  .lpf(400).lpq(.2).lpd(.4).lpenv(3)
  .fmdecay(.2).fmenv(1)


$: chord("<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>").voicing()
  .s("<sine>").clip(1).rel(.4)
  .gain(.4)
  .fm(1.4).att(1).lpa(.5).lpf(200).lpenv(4)

@daslyfe
Copy link
Collaborator

daslyfe commented Jun 9, 2025 via email


return sl + sr
//TODO: make stereo
// return [sl, sr];
Copy link
Collaborator Author

@felixroos felixroos Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd go for a property on the class, rather than creating an array for each sample (avoids memory allocation (i think))

@daslyfe
Copy link
Collaborator

daslyfe commented Jun 10, 2025

Added delayspeed to create pitched and reversed delay effects, implementation inspired by this article

note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>") .supradough()

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants