Skip to content

tomara-x/quartz

Repository files navigation

quartz

"you africans, please listen to me as africans
and you non-africans, listen to me with open mind"

Screenshot_2024-04-28_19-32-55 Screenshot_2024-06-07_09-43-35 Screenshot_2024-07-12_06-12-55 Screenshot_2024-07-27_15-22-10

let's play

learning


building

to build from source:

git clone https://github.com/tomara-x/quartz.git
  • build it
cd quartz
cargo run --release

development happens on the main branch

alternatively you can download stable releases from: https://github.com/tomara-x/quartz/releases

there's an experimental wasm build here: https://tomara-x.github.io/quartz/ (no audio input, no file loading/saving, and no OSC)

wasm building

uncomment the commented out dependencies in cargo.toml, then:

RUSTFLAGS=--cfg=web_sys_unstable_apis cargo build --profile wasm-release --target wasm32-unknown-unknown

wasm-bindgen --out-name quartz --out-dir target --target web target/wasm32-unknown-unknown/wasm-release/quartz.wasm

more details: https://github.com/bevyengine/bevy/tree/main/examples#wasm


modes and navigation

when you open quartz, it will be an empty window. there's 3 modes:

  • edit: (default) interact with entities and execute commands (press e or esc)
  • draw: draw new circles (press d)
  • connect: connect circles (press c)
    • target: target an entity from another (hold t in connect mode)

hold space, then drag to pan the view, or scroll to zoom in and out


anatomy

terms:

  • circle1: an object that you create in draw mode (they're regular polygons)
  • hole: a connection object. these always come in (black hole - white hole) pairs
  • entity: i'll use that to refer to any object (circle or hole)

note: in this file i'll use square brackets [] in arguments to denote an optional argument

both circles and holes have:

  • position: x, y, z (z controls depth, what's in front of what)
  • color: in hsla (hue, saturation, lightness, alpha) (hue is in the range [0...360], the rest in the range [0...1])
  • radius: from zero to beeeg
  • vertices: the number of sides (3 or more)
  • rotation: in radians [-PI...PI]

a circle has other things in addition:

  • a number (32-bit float)
  • an op string: defining what that circle does
    • for example: sum, sine(), toggle, screenshot, lowpass()
  • an array of numbers: for different uses (empty by default)
  • an array of target entities that this circle controls in some way (empty by default too)
  • an order number: defining if/when that circle is processed
  • an audio node (defined by the op string) (empty by default)

holes store information about what they connect:

  • a white hole has:
    • id of the black hole it's connected to
    • id of the circle that has that black hole
    • link types
    • an open status (whether or not to read data from the black hole2)
  • a black hole has:
    • id of the white hole it's connected to
    • id of the circle that has that white hole

commands

there are 2 types of commands:

  1. return-terminated (type it then press enter) (you can separate them with ; to run more than one at once)
scene saving/loading

  • :e {file name} edit (open) a scene file (in the assets path) (no spaces)
  • :w {file name} write (save) a scene file (same)
:w moth.cute    // saves the current scene as the file "assets/moth.cute" (OVERWRITES)
:e soup.fun     // opens the file "assets/soup.fun" if it's there

(dragging and dropping scene files into a window also works)

set values

  • :set n [id] {float} set num value
  • :set r [id] {float} set radius (use rx or ry to set those independently)
  • :set x [id] {float} set x position
  • :set y [id] {float} set y position
  • :set z [id] {float} set z position (this controls depth. what's in front of what)
  • :set h [id] {float} set hue value [0...360]
  • :set s [id] {float} set saturation [0...1]
  • :set l [id] {float} set lightness [0...1]
  • :set a [id] {float} set alpha [0...1]
  • :set v [id] {float} set number of vertices (3 or higher)
  • :set o [id] {float} set rotation [-pi...pi] (:set rot and :set rotation also work)
  • :set op [id] {string} set the op string (use shortcut o)
  • :set ord[er] [id] {float} set order (use ] and [ to go up/down by one)
  • :set arr[ay] [id] {float float ...} set the array (space separated)
  • :set tar[gets] {id id ...} set targets (if nothing is selected, the first entity gets the rest of the list as its targets)
  • :tsel {id} target selected (:tsel 4v2 sets selected entities as targets of entity 4v2)
  • :push {float}/{id} push a number to the array, or an id to the targets array
  • :lt [id] {link type} set holes' link type (use shortcut l)
:set n 4v0 42  // will set the num of entity 4v0 to 42
:set n 42      // will set the num values of selected entities to 42

audio device selection

  • :od {index} {index} [sample rate] [buffer size] set the output audio device. first index is the host, second is the device index (use the commands ah and ao to get those) if sample rate and buffer size aren't given, the device defaults will be used
  • :id {index} {index} [sample rate] [buffer size] set the input audio device

notes:

  • if device has a sampling rate different from 44100, you need to connect your node to an sr() node (with matching sr) before connecting that to out() (unless you want mismatched sr)
  • after changing input device, you must re-set the op of the in() node. (because it only initializes internal stuff once the op is set and it depends on the selected device)
  • after changing output device, you must open the white hole connecting your node to the out() node (the new device will not automatically read it)

other

  • :nl set the maximum number of nodes a connective op (+, *, >>, etc) will allow (default 500) (saved in scene file)
  • :reset_bloom if you change bloom settings to the point where you can't see what's happening, reset them
  • :reset_cam in case you took it too far with the cam op (might need to set everything to order 0 first)
  • :dv {float} set default number of vertices of drawn circles
  • :dc {float} [float] [float] float] set default color of drawn circles (h s l a)
  • :ht {id} toggle open a white hole (by id)
  • :q exit (don't combine with other commands using ;)

note: the std constants, inf, -inf, nan are valid floats. e.g. :set op dc(-PI), :set n TAU, :set x inf

  1. immediate commands (these execute when you finish typing them)
drag modes

(what happens when dragging selected entities, or when arrow keys are pressed)

  • exclusive:
    • ee drag nothing (default)
    • et drag translation (move entity)
    • er drag radius
    • en drag number
    • eh drag hue
    • es drag saturation
    • el drag lightness
    • ea drag alpha
    • eo drag rotation
    • ev drag vertices
  • add a drag mode: (to drag multiple properties at the same time)
    • Et add translation
    • Er add radius
    • En add num
    • Eh add hue
    • Es add saturation
    • El add lightness
    • Ea add alpha
    • Eo add rotation
    • Ev add vertices

shortcuts

  • o shortcut for :set op
  • l shortcut for :lt

inspect commands

(information about the selected entities)

  • ii entity ids
  • in number values
  • ira radius values
  • ix x position
  • iy y position
  • iz z position
  • ihu hue value
  • is saturation
  • il lightness
  • ial alpha
  • iv vertices
  • iro rotation
  • ior order
  • iop op
  • iar array
  • iho holes
  • it targets
  • iL hole link type
  • iO white hole open status

audio node info

  • ni number of inputs
  • no number of outputs
  • np info about the node

audio hosts/devices

  • ah list available audio hosts
  • ao list output devices
  • ai list input devices

selection

  • sa select all
  • sA deselect all
  • sc select all circles
  • sC deselect circles
  • sh select all holes
  • sH deselect holes
  • sv select visible entities (in view)
  • sV deselect visible entities
  • sg select holes of the selected circles
  • st select targets of the selected circles
  • <delete> delete selected entities
  • yy copy selection to clipboard
  • p paste copied

notes:

  • holding shift, then clicking an entity, or dragging across and area will add to the selection
  • when drag-selecting, holding alt will only select circles (ignores holes), holding ctrl will only select holes (ignores circles)
  • ctrl+clicking a selected entity will deselect it

visibility

  • vc toggle circle visibility
  • vb toggle black hole visibility
  • vw toggle white hole visibility
  • va toggle arrow visibility
  • vt toggle info texts
  • vT toggle id in info texts (flick vt after changing this)
  • vv show all

other

  • <F11> toggle fullscreen
  • ht toggle white hole open status
  • F freeze the command line (press esc, Enter, or Backspace to reactivate it)
  • quartz shhh!
  • version print version
  • awa awawawa


link types

any connection links 2 circles together in some way. the black hole is taking some data from the source circle, and the white hole is getting that data and feeding it to the sink circle. the link type determines what that data is.

  • n or -1 : num
  • r or -2 : radius
  • x or -3 : x position
  • y or -4 : y position
  • z or -5 : z position
  • h or -6 : hue
  • s or -7 : saturation
  • l or -8 : lightness
  • a or -9 : alpha
  • -10 : order (only with distro)
  • v or -11 : vertices
  • o or -12 : rotation
  • A or -13 : array
  • T or -14 : targets
  • positive numbers: used to denote "input number x"
  • 0 can mean audio node, op string, or nothing (it depends on the op) a 0 -> 0 connection does nothing (except for connective ops, seq, and select)

use }/{ to go up/down by one. or use l with selected holes to set a specific link type


order

every circle has an order (0 or higher). things in order 0 do nothing.

each frame, every circle with a positive order gets processed (does whatever its op defines) this processing happens.. you guessed it, in order

we process things breadth-first. so when a circle is processed, all its inputs must have their data ready (they processed in this frame) to make sure that's the case, their order has to be lower than that of the circle reading their data...

lower order processes first, and the higher the order, the later that circle processes (within the same frame)

unless...


targets

a circle has an array of "targets" those are just entity ids. so it's like a "pointer" to another circles or hole. think of it as a one-way wireless connection. some ops make a circle do things to its targets. like process, del_targets, spin_target, distro, reorder, (see ops for full list)

(they allow some things that aren't easy through normal processing. since circles read their input when they process, while targets are written to when the controller circle is processed instead)


ops

targets

  • process
    • this circle will process its targets in the order they appear in the targets array. it doesn't matter what order those targets are. even if they're at order 0 (it's preferable they are at 0 so you don't cause unexpected things). so for every frame a circle with a process op is processed, it processes all of its targets in order.
    • you can't nest them. so if a process has another process in its targets, that won't process the second one (to avoid blowing up computers)
  • select_target
    • input: n -> 1
    • select the targets when input is non-zero, deselect them when it's zero
  • open_target
    • inputs: n -> 1
    • open target white holes when input is non-zero
  • close_target
    • inputs: n -> 1
    • close target white holes when input is non-zero
  • open_nth
    • inputs: n -> 1
    • open nth target once if it's a white hole
  • del_target
    • inputs: n -> 1
    • delete targets and clear targets array when input is non-zero
  • spin_target
    • inputs: n, n -> 1
    • rotate targets around this circle by n (radians) when input is non-zero
  • reorder
    • inputs: n -> 1
    • set target circles' order to input n
  • spawn
    • inputs: n -> 1
    • spawn a new circle similar to this one when input is non-zero. the new circle is added to this circle's targets. only the color, vertices, and transform (ish) are copied (z depth is increased with each one)
  • distro
    • inputs: A -> n/r/x/y/z/r/o/v/h/s/l/a/-10 (any number of those)
    • distribute values from input array among targets
  • connect_target
    • inputs: n -> 1, [T -> 2]
    • remove holes from targets array, then connect each target circle to the next. if array contains 2 numbers they will be used as the connection type (otherwise 0 -> 0) if second input is provided, the white holes created will be added as targets to that circle
  • isolate_target
    • inputs: n -> 1
    • delete all connections target has when input is non-zero
  • target_lt
    • inputs: n -> 1
    • for hole targets, set their link type to input num
  • repeat
    • inputs: n, T -> 1
    • repeat input targets array n times

arrays

  • zip
    • inputs: A -> 1, A -> 2
    • zip array 1 and array 2
  • unzip
    • inputs: A -> 1
    • unzip input array (one side remains in input array, the other side is in this circle)
  • push
    • inputs: n -> 1
    • push input num to this circle's array
  • pop
    • inputs: n -> 1
    • pop the last number in the array and set this circle's num to it when input is non-zero
  • len
    • inputs: A -> 1 or T -> 1
    • length of array (or targets array)
  • append
    • inputs: A -> 1
    • copy input array and append it to the end of this circle's array
  • slice
    • inputs: n, A -> 1
    • slice input array at index n, [0..n] remain in input array, [n..len] are moved to this circle's array
  • resize
    • inputs: n -> 1
    • resize this circle's array, discards numbers when shrinking, and adds zeros when growing
  • contains
    • inputs: A -> 1, n -> 2
    • outputs 1 when input array contains input num, 0 otherwise
  • set
    • inputs: n -> 1, n -> 2
    • first input is index, second is value. sets the value of the given index of this circle's array
  • get
    • inputs: A -> 1, n -> 2
    • get the value at index of the input array
  • collect
    • inputs: n -> {non-negative} (any number of those)
    • collect all connected nums and add them in order to this circle's array

settings

  • clear_color
    • when color changes (drag h/s/l), sets the background color (the clear color)
  • draw_verts
    • when vertices change, set the default drawing vertices for future circles
  • draw_color
    • when color changes, set the default drawing color
  • highlight_color
    • when color changes, set the highlight color (the outline around selected entities)
  • indicator_color
    • when color changes, set the color of the selecting/drawing/connecting indicator
  • connection_color
    • when color changes, set the color of connection arrows
  • connection_width
    • when this circle's num changes, set the width of the connection arrows
  • command_color
    • when color changes, set color of the command line text
  • text_size
    • when this circle's num changes, set the font size of info texts
  • tonemapping
    • inputs: n -> 1
    • input num sets the tonemapping mode. 0 = None, 1 = Reinhard, 2 = ReinhardLuminance, 3 = AcesFitted, 4 = AgX, 5 = SomewhatBoringDisplayTransform, 6 = TonyMcMapface, 7 = BlenderFilmic (default: 6 tony)
  • bloom
    • control bloom parameters
    • inputs:
      • n -> 1 : intensity (default: 0.2)
      • n -> 2 : low frequency boost (default: 0.6)
      • n -> 3 : low frequency boost curvature (default: 0.4)
      • n -> 4 : high pass frequency (default: 1)
      • n -> 5 : composite mode (if n > 0 Additive else EnergyConserving) (default: additive)
      • n -> 6 : prefilter threshold (default: 0)
      • n -> 7 : prefilter threshold softness (default: 0)

all of these except for the tonemapping and bloom settings are saved inside the scene file (so deleting the circle after changing that setting is fine) but for persistent change to bloom/tonemapping you have to leave the circles with the input values attached to them in the scene

utils

  • cam
    • n -> 1 camera x position
    • n -> 2 camera y position
    • n -> 3 camera z position (can be useful if you're playing with extremes in depth)
    • n -> 4 camera rotation
    • n -> 5 zoom
  • update_rate
    • inputs: n -> 1, n -> 2
    • by default quartz will respond (as fast as possible) to any mouse input/movement, or keyboard input, or if the refresh duration has elapsed. that duration is by default 1/60 of a second (60fps) when the window is in focus, and 30fps when out of focus. first input is the refresh rate (in hz) for focused mode, second input is unfocused rate
  • command
    • inputs: 0 -> 1 (op string to first input)
    • when the white hole is open set the command line text to the string of the input circle
  • screenshot
    • inputs: n -> 1
    • when input num is non-zero, take a screenshot and save it as screenshots/{time in ms since 1970}.png (make sure that folder exists)
  • osc
    • set the settings of osc sender and receiver
    • n -> 1 receiver port (needs to be specified for receiving to work)
    • 0 -> 2 op string of the input sets the host ip (ip to send to) (defaults to 127.0.0.1 (localhost))
    • n -> 3 sender port (defaults to 1729)
  • osc_r_{osc address}
    • receive osc messages into the array of this circle. the osc op must be present in this patch and is processing for this to work. the osc messages must be sent to the given osc address and contain floats. you can receive from multiple addresses
    • e.g. osc_r /gyroscope, osc_r /touch1 /touch3
  • osc_s_{osc address}
    • inputs: A -> 1
    • send the input array as an osc message with the given address (to the host and port set by the osc op)
    • e.g. osc_s /space

for more info about osc: https://opensoundcontrol.stanford.edu/spec-1_0.html

input

  • mouse
    • array stores mouse position (in world coordinates) [x, y]
  • lmb_pressed
    • num = 1 if left mouse button is pressed, 0 otherwise
  • mmb_pressed
    • num = 1 if middle mouse button is pressed, 0 otherwise
  • rmb_pressed
    • num = 1 if right mouse button is pressed, 0 otherwise
  • butt
    • num = 1 when clicked, 0 otherwise
  • toggle
    • num = 1 when clicked, 0 when clicked again (kinda)
  • key
    • pressed keyboard keys are added to this circle's array and removed when released. for keys corresponding to an ascii character that's their decimal ascii code, for other keys it's an arbitrary convention that i put together in 5 minutes:
      • Control: 128, Shift: 129, Alt: 130, Super: 131, Fn: 132
      • CapsLock: 133, NumLock: 134, ScrollLock: 135
      • End: 136, Home: 137, PageUp: 138, PageDown: 139
      • Insert: 140, ContextMenu: 141
      • ArrowUp: 200, ArrowDown: 201, ArrowLeft: 202, ArrowRight: 203
      • F1: -1, F2: -2 .. F12: -12
  • pressed_{one or more characters}
    • e.g. pressed Hi this circle's num will be set to 1 when either H or i is pressed, zero otherwise

data management xD

  • apply
    • inputs: 0 -> 1 (input audio node), A -> 2 (input array)
    • process the input array as input to the given audio node (array length must match the number of input channels the node has) output of the node is written to this circle's array (process one audio frame)
  • render
    • inputs: n, 0 -> 1 (input node), n -> 2 (trigger)
    • render n samples from the given audio node into the array when the second input is non-zero (node must have 0 ins, and 1 out). can process a maximum of 10 million samples at a time (a limit to avoid causing memory issues and excessive cpu usage)
  • store
    • inputs: n -> 1
    • store the input num into this circle's num, but doesn't open the white holes reading nums like usual
  • num_push
    • inputs: n -> 1
    • output this circle's num (open all white holes reading it) when the input num in non-zero
  • worm[string]
    • multiple circles with the same op string (starting with worm) will mirror the same number value. when one's num changes, the others follow. their order matters, as value flows from lower to higher order
  • sum
    • inputs: n -> 1 (any number of those)
    • convenience op for adding numbers together
  • product
    • inputs: n -> 1 (any number of those)
    • multiply numbers together

audio node management

refer to the fundsp readme, and docs for more details (sometimes)

  • + or SUM
    • inputs: 0 -> {non-negative} (any number of those), n (repetitions)
    • sum given nodes together. their number of outputs must match, their inputs are stacked together in the order they appear in connections
  • * or PRO
    • inputs: 0 -> {non-negative} (any number of those), n (repetitions)
    • multiply given nodes together. their number of outputs must match, their inputs are stacked together in the order they appear in connections
  • - or SUB
    • inputs: 0 -> 1, 0 -> 2
    • node 1 - node 2 (number of outputs of those nodes must match)
  • >> or PIP
    • inputs: 0 -> {non-negative} (any number of those), n (repetitions)
    • pipe nodes though each other. if outputs of node 1 matches inputs of node 2 they're piped together, and so on
  • | or STA
    • inputs: 0 -> {non-negative} (any number of those), n (repetitions)
    • stack inputs and outputs of given nodes
  • & or BUS
    • inputs: 0 -> {non-negative} (any number of those), n (repetitions)
    • bus given nodes together. number of inputs and outputs must match. input is passed through each node and output from them is mixed at output
  • ^ or BRA
    • inputs: 0 -> {non-negative} (any number of those), n (repetitions)
    • branch given nodes together (same inputs are passed to each node, but their outputs are kept separate)
  • ! or THR
    • inputs: 0 -> 1
    • pass extra inputs through
  • branch()
    • inputs: A -> 1, 0 -> 2
    • create as many nodes as the input array has values, replacing the "#" in the second input's op with each value, all branched together. e.g. array: [1, 2, 3] and op string "lowpass(1729, #)" creates the node lowpass(1729, 1) ^ lowpass(1729, 2) ^ lowpass(1729, 3)
  • bus()
    • same as branch() but bus nodes together instead
  • pipe()
    • same as branch() but pipe nodes together
  • stack()
    • same as branch() but stack nodes
  • sum()
    • same as branch() but sum
  • product()
    • same as branch() but
  • swap()
    • inputs: 0 -> 1
    • swap the node without resetting the graph if arity is identical. connecting a different arity will reset the graph
  • out() or dac()
    • inputs: 0 -> 1
    • output given node to speakers (node must have 1 or 2 outputs)
  • in() or adc()
    • node with 2 outputs corresponding to the quartz input device (mic input and the like)
  • var()
    • node: 0 ins, 1 out
    • create a shared variable audio node. its output is the value of this circle's num. must have an order >= 1
  • monitor()
    • node: 1 in, 1 out (it passes audio through)
    • create a monitor node. sets the value of this circle's num to the latest sample that passed through this node. must have an order >= 1
  • timer()
    • when stacked with another node, this will maintain the current time of that node in this circle's number. must have an order >= 1
  • buffin() and buffout()
    • these 2 are useful when you need to get samples from a point in the audio graph but not use them in audio (like when you want to use render to visualize audio). this is a channel with a buffer size (determined by the number of the buffin() circle. then this circle will have an audio node that passes audio through but sends it to the buffout(). connect them like this to link them: buffin() 0 -> 1 bufout() then whenever the audio node of buffout() processes (using apply or render) it will receive the samples sent from the audio graph. buffer can have a capacity of 1..100000. these nodes are basically like monitor(), but buffered instead of receiving only the latest sample. (see the assets/scope and assets/circular-scope scenes for examples)
  • get()
    • node: 1 in (index), 1 out (value)
    • copies this circle's array into node so it can be indexed at audio-rate. input is index, output is the value at that index
  • quantize()
    • inputs: A -> 1 (array of steps to quantize to. must have at least 2 different values)
    • node: 1 in, 1 out
    • quantize input to the nearest value in the given steps
  • feedback()
    • inputs: 0 -> 1 (input node), [n -> 2] (optional delay)
    • mixes outputs of given node back into its inputs (number of node ins/outs must match)
    • node: ins and outs are the same as the input node
  • kr()
    • inputs: n, 0 -> 1 (input node)
    • node: ins and outs are the same as the given node
    • process the input node once every n samples (this can be used to optimize things that don't need to be processed sample-by-sample)
  • s()
    • same as kr() but since kr() will process the node less frequently than it would normally, that has an effect on how it behaves (times and frequencies get stretched) s() avoids that by setting the sr of the given node to sr/n in order to preserve the time of that node
  • sr()
    • inputs: n, 0 -> 1 (input node)
    • set the sample rate for the input node
  • reset()
    • inputs: n, 0 -> 1 (input node (must have 0 ins, and 1 out))
    • node: 0 ins, 1 out
    • process the input node, but reset it every n seconds (rounded to nearest sample)
  • reset_v()
    • inputs: 0 -> 1 (input node (must have 0 ins, and 1 out))
    • node: 1 in, 1 out
    • process the input node but reset it every n seconds. n is specified by the input to this node
  • trig_reset()
    • inputs: 0 -> 1 (input node (must have 0 ins, and 1 out))
    • node: 1 in, 1 out
    • reset the given node whenever the input is non-zero
  • seq()
    • inputs: 0 -> {non-negative} (any number of those)
    • node: 4 ins (trig, node index, delay, duration), 1 out (output from sequenced nodes)
    • sequences the given nodes and mixes their outputs at output (valid input nodes must have no inputs, and only one output). for every sample trig is non-zero, add an event for the node at index with the given delay and duration (in seconds, rounded to nearest sample)
    • indexes are collected. e.g. if circle has three connections: 0 -> 1 0 -> 5 0 -> 8 this is gonna be a sequencer node that accepts indexes 0, 1, and 2. the node at 1 has index 0, node at 5 has index 1, and node at 8 has index 2. and only valid nodes are added.
  • select()
    • inputs: 0 -> {non-negative} (any number of those)
    • node: 1 in (index of selected node), 1 out (output from that node)
    • create a node that switches between input nodes based on index
  • wave()
    • inputs: A -> 1
    • node: 0 ins, 1 out
    • create a wave player from the input array

audio nodes

refer to the fundsp readme, and docs for more details (in some cases)

sources

  • sine([float]) e.g. sine(440) has no inputs, and outputs a sine wave at 440Hz. sine() takes 1 input (frequency) and outputs sine wave
  • saw([float]) (same)
  • square([float]) (same)
  • triangle([float]) (same)
  • organ([float]) (same)
  • hammond([float]) (same)
  • soft_saw([float]) (same)
  • dsf_saw([float]) dsf_saw() takes 2 inputs (frequency, and roughness [0...1]), dsf_saw(0.5) takes only a freq input.
  • dsf_square([float]) (same)
  • pulse() pulse wave oscillator (frequency, and duty cycle [0...1])
  • brown() brown noise
  • pink() pink noise
  • white() noise() white noise
  • zero() silence
  • impulse() one sample impulse
  • lorenz()
  • rossler()
  • constant(float) dc(float)
  • pluck(float, float, float) (frequency, gain per sec, high freq damping) input is string excitation signal
  • mls([float])
  • ramp() ramp from 0 to 1 at input freq (phasor)

filters

  • allpole([float]) delay in samples. if not provided the node takes 2 inputs (signal, delay)
  • pinkpass()
  • allpass([float], [float]) if 1 param is given, that's the q, and the node takes 2 input channels (signal, and hz) if 2 are given, that's the hz and q and the node only takes input signal
  • bandpass([float], [float]) (same as allpass)
  • bandrez([float], [float]) (same)
  • bell([float, float], [float]) if 2 params are given, they're (q, gain), if 3 are give, they're (hz, q, gain), if none, the node takes 4 channels (input, hz, q, gain)
  • biquad(float, float, float, float, float)
  • butterpass([float]) e.g. butterpass() takes 2 inputs (signal, and hz). butterpass(1729) takes 1 input
  • dcblock([float]) if no param is provided, the cutoff is 10Hz
  • fir(float [float], [float], ...) (up to 10 weights)
  • fir3(float) param is gain at nyquist
  • follow(float, [float]) attack and release response times
  • highpass([float], [float]) (same as allpass)
  • highpole([float]) (same as butterpass)
  • highshelf([float, float], [float]) (same as bell)
  • lowpass([float], [float]) (same as allpass)
  • lowpole([float]) (same as butterpass)
  • lowrez([float], [float]) (same as allpass)
  • lowshelf([float, float], [float]) (same as bell)
  • moog([float], [float]) (same as allpass)
  • morph([float, float, float]) (hz, q, morph [-1...1] (-1 = lowpass, 0 = peak, 1 = highpass)) if not provided, the node takes 4 inputs (signal, hz, q, morph)
  • notch([float], [float]) (same as allpass)
  • peak([float], [float]) (same)
  • resonator([float, float]) (hz, bandwidth) if not provided the node takes 3 inputs (signal, hz, bandwidth)

channels

  • sink() eats an input channel
  • pass() takes an input channel and passes it unchanged
  • chan(float, float,...) shortcut for stacking pass/sink nodes. e.g. chan(0,1,0,1,2) is the same as sink() | pass() | sink() | pass() | pass() (non-zero is pass)
  • pan([float]) e.g. pan(0) pan input (mono to stereo) pan() takes 2 inputs (signal, and pan [-1...1])
  • join(float) float can be [2...8] e.g. join(8) takes 8 inputs and averages them into 1 output
  • split(float) float can be [2...8] split(8) takes 1 input and copies it into 8 outputs
  • reverse(float) float can be [2...8] reverse the order of channels

envelopes (all subsampled at ~2 ms)

  • adsr(float, float, float, float)
  • xd([float]) (this is just an exp(-t*input))
  • xD([float], [float]) e.g. xD() takes 2 inputs (time and curvature) xD(5) takes 1 input specifying the decay time with a curvature of 5. xD(10, 0.5) is a decay over 10 seconds with a curvature of 0.5.
  • ar([float, float], [float, float]) if there are no params it takes 4 inputs, if there are 2 params they are the curvature of attack and release and the node takes 2 inputs specifying the times, if there are 4 params they are (attack time, attack curvature, release time, release curvature)

other

  • tick() one sample delay
  • shift_reg() 2 ins (input signal, trigger signal), 8 outs (outputs of the shift register)
  • snh() sample and hold node. 2 ins (input signal, trigger signal), 1 out (held signal)
  • meter(peak/rms, float) e.g. meter(rms, 0.5) rms(peak, 2)
  • chorus(float, float, float, float) (seed, separation, variation, mod frequency)
  • declick([float]) e.g. declick() 10ms fade in, declick(2) 2 second fade in
  • delay(float) e.g. delay(2) 2 second delay
  • hold(float, [float]) e.g. hold(0.5) takes 2 inputs (signal, and sampling frequency) with variability 0.5, hold(150, 0) takes one input and samples it at 150Hz with variability 0
  • limiter(float, float) look ahead limiter. first param is attack time, second is release time (in seconds)
  • limiter_stereo(float, float) (same)
  • reverb_stereo(float, [float], [float]) (room size, reverberation time, damping) when damping isn't provided it defaults to 1, time defaults to 5
  • reverb_mono(float, [float], [float]) same but input is passed to both channels and output is joined into one
  • tap(float, float) (min delay time, max delay time)
  • tap_linear(float, float) (same)
  • samp_delay(float) argument is max delay time (in samples), node takes 2 inputs (signal, delay time in samples)

math

  • add(float, [float], [float], ...) (up to 8 params)
  • sub(float, [float], [float], ...) (same)
  • mul(float, [float], [float], ...) (same)
  • div(float, [float], [float], ...) (same)
  • rotate(float, float)
  • t() time since the node started processing (subsampled every ~2 ms)
  • rise() one sample trigger when there's a rise in input
  • fall() same but fall
  • >([float]) e.g. >() takes 2 inputs and compares them. >(3) takes one input and compares against 3
  • <([float]) (same from this one...)
  • ==([float])
  • !=([float])
  • >=([float])
  • <=([float])
  • min([float])
  • max([float])
  • pow([float])
  • mod([float]) or rem([float])
  • log([float])
  • bitand([float])
  • bitor([float])
  • bitxor([float])
  • shl([float])
  • shr([float]) (.. all the way to this one)
  • clip([float, float]) e.g. clip() takes 1 input and clips to [-1...1], clip(-5, 5) clips to [-5...5]
  • wrap(float, [float]) wrap between 2 numbers (or between 0 and x if only one number is given)
  • mirror(float, float) mirror (wave fold) between two values
  • lerp([float, float]) e.g. lerp() takes 3 inputs (a, b, t) lerp(3,5) takes one input (t)
  • lerp11([float, float]) (same..)
  • delerp([float, float])
  • delerp11([float, float])
  • xerp([float, float])
  • xerp11([float, float])
  • dexerp([float, float])
  • dexerp11([float, float]) (.. same)
  • abs()
  • signum()
  • floor()
  • fract()
  • ceil()
  • round()
  • sqrt()
  • exp()
  • exp2()
  • exp10()
  • exp_m1()
  • ln_1p()
  • ln()
  • log2()
  • log10()
  • sin()
  • cos()
  • tan()
  • asin()
  • acos()
  • atan()
  • sinh()
  • cosh()
  • tanh()
  • asinh()
  • acosh()
  • atanh()
  • atan2()
  • hypot() distance between the origin and a point (x, y)
  • rfft(n, start) n is the size of the analysis window, a power of two between 2 and 32768 (inclusive). start determines the offset within the window to begin writing to (so we can do correct overlap)
  • ifft(n, start) same
  • pol() give it cartesian, get polar
  • car() give it polar, get cartesian
  • deg() give it radians, get degrees
  • rad() give it degrees, get radians
  • recip() reciprocate. give it x, get 1/x
  • squared()
  • cubed()
  • dissonance()
  • dissonance_max()
  • db_amp()
  • amp_db()
  • a_weight()
  • m_weight()
  • spline()
  • spline_mono()
  • soft_sign()
  • soft_exp()
  • soft_mix()
  • smooth3()
  • smooth5()
  • smooth7()
  • smooth9()
  • uparc()
  • downarc()
  • sine_ease()
  • sin_hz()
  • cos_hz()
  • sqr_hz()
  • tri_hz()
  • semitone_ratio()
  • rnd()
  • rnd2()
  • spline_noise()
  • fractal_noise()
  • normal() filter nan, inf, and -inf

i hope that everyone will become friends

thanks

license

quartz is free and open source. all code in this repository is dual-licensed under either:

at your option.

your contributions

unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

devlog

https://www.youtube.com/playlist?list=PLW3qKRjtGsGZC7V4eKU_tNwszVZAYvKow

Footnotes

  1. in early development everything was actually circular, there wasn't a vertices control.. and the word stuck

  2. when there is a change on the black hole side, this gets opened automatically. then it gets closed once the data is read. in some ops this is ignored and the op just reads data even if the white hole isn't open