├── .gitignore ├── API.md ├── LICENCE ├── Makefile ├── README.md ├── TODO ├── assets ├── font.png ├── font.png.dat └── spritesheet.png └── src ├── common.nim ├── core ├── basemachine.nim ├── chords.nim ├── delaybuffer.nim ├── envelope.nim ├── fft.nim ├── filter.nim ├── lfsr.nim ├── moddelay.nim ├── noise.nim ├── oscillator.nim ├── ringbuffer.nim ├── sample.nim └── scales.nim ├── jack ├── jack.nim ├── midiport.nim └── types.nim ├── machines ├── converters │ ├── a2e.nim │ ├── e2a.nim │ └── n2f.nim ├── fx │ ├── bitcrush.nim │ ├── compressor.nim │ ├── delay.nim │ ├── distortion.nim │ ├── eq.nim │ ├── flanger.nim │ ├── gate.nim │ ├── mod_amp.nim │ ├── mod_filter.nim │ ├── sandh.nim │ ├── stereo.nim │ └── svf.nim ├── generators │ ├── adsr.nim │ ├── basicfm.nim │ ├── clock.nim │ ├── eadsr.nim │ ├── fmsynth.nim │ ├── gbsynth.nim │ ├── granular.nim │ ├── kayoubi.nim │ ├── kit.nim │ ├── looper.nim │ ├── mod_lfsr.nim │ ├── noise.nim │ ├── organ.nim │ ├── osc.nim │ ├── sampler.nim │ ├── synth.nim │ ├── tb303.nim │ └── ym2612.nim ├── io │ ├── audioin.nim │ ├── filerec.nim │ └── keyboard.nim ├── master.nim ├── math │ ├── accumulator.nim │ └── operators.nim ├── ui │ ├── button.nim │ ├── knob.nim │ ├── value.nim │ └── xy.nim └── util │ ├── arp.nim │ ├── chord.nim │ ├── dc.nim │ ├── karp.nim │ ├── lfo.nim │ ├── paramlp.nim │ ├── paramrecorder.nim │ ├── probgate.nim │ ├── probpath.nim │ ├── probpick.nim │ ├── recorder.nim │ ├── scale.nim │ ├── sequencer.nim │ ├── spectrogram.nim │ ├── split.nim │ └── transposer.nim ├── main.nim ├── midi.nim ├── rtmidi.nim ├── testc.nim ├── ui ├── layoutview.nim ├── machineview.nim ├── menu.nim └── paramwindow.nim └── util.nim /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | Proposed Library API: 2 | 3 | nsLoadPatch(filename): Patch 4 | nsSavePatch(filename): bool 5 | 6 | nsCreateMachine(machineClass, name): Machine 7 | nsCreateMachineFromPatch(filename, name): Machine 8 | nsDestroyMachine(machine) 9 | 10 | nsConnectMachines(a,b, output, input) 11 | nsDisconnectMachines(a,b, output, input) 12 | 13 | nsGetInputs(machine): seq[Input] 14 | nsGetOutputs(machine): seq[Output] 15 | nsGetBindings(machine): seq[Binding] 16 | nsGetParameters(machine): seq[Parameter] 17 | 18 | nsBindParameter(a,b, binding, param): bool 19 | nsUnbindParameter(a,b, binding, param): bool 20 | 21 | nsSetParameterValue(machine: Machine, param: Parameter, value: float) 22 | nsGetParameterValue(machine: Machine, param: Parameter): float 23 | 24 | nsProcessAudio(nSamples: int, samples: pointer) 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES=$(shell find src -name '*.nim') 2 | DATE=$(shell date +%Y-%m-%d) 3 | NIMC=nim c 4 | OPTS=-p:src -d:nimNoLentIterators -d:audioInput 5 | 6 | JACK=0 7 | 8 | ifeq ($(JACK),1) 9 | JACK_FLAGS="-d:jack" 10 | else 11 | JACK_FLAGS= 12 | endif 13 | 14 | synth: $(SOURCES) 15 | ${NIMC} ${OPTS} -d:release -o:$@ src/main.nim 16 | 17 | synth-debug: $(SOURCES) 18 | ${NIMC} ${OPTS} -d:debug -o:$@ src/main.nim 19 | 20 | run: synth 21 | ./synth 22 | 23 | rund: synth-debug 24 | ./synth-debug 25 | 26 | .PHONY: web run rund osx windows 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NimSynth - Modular Synthesizer Workstation 2 | 3 | Goals: 4 | 5 | Cross platform, designed for performing live and in studio. 6 | Experimental audio creation. Easy to write your own modules. 7 | Build new things out of existing modules. 8 | 9 | Everything is controllable. 10 | 11 | 12 | 13 | Controls: 14 | 15 | Global: 16 | * F1/Ctrl+1: return to Layout View 17 | 18 | Layout View: 19 | * Connect machines with right-click drag (green lines) 20 | * Bind parameters with right-click drag (red lines) 21 | * Machines connected to master can make sound 22 | * CTRL+S: save 23 | * CTRL+O: open 24 | * CTRL+N: new project 25 | * CTRL+M: mute audio 26 | 27 | Machine View: 28 | 29 | Sequencer View: 30 | * Lower octave: z,s,x,d,c,v,g,b,h,n,m 31 | * Upper octave: q,2,w,3,e,r,5,t,6,y,u 32 | * 1 = Off Note 33 | * CTRL+PgDn: Double length of pattern 34 | * CTRL+PgUp: Half length of pattern 35 | * CTRL+L: toggle looping 36 | 37 | F2: Machine Settings (select a machine first) 38 | * UP/DOWN select parameter 39 | * LEFT/RIGHT adjust parameter (SHIFT for fine control, CTRL for coarse control) 40 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Mini Music Studio 2 | ----------------- 3 | 4 | Workspace similar to buzz where you lay out generator/effects/sequencers 5 | 6 | 7 | * SubSynth 8 | - 2 Oscs 9 | - 1 Filter 10 | - Amp Env 11 | - Filter Env 12 | - Glissando 13 | 14 | * FMSynth 15 | 16 | * Sampler 17 | - 8 channels 18 | - assign sfx to channel 19 | - volume 20 | 21 | * Looper 22 | - Records input and plays it back looped 23 | - Import external music file 24 | - Multiple loops 25 | - Can adjust speed/pitch/reverse/do glitchy stuff 26 | 27 | * Effects: 28 | - DONE: Delay 29 | - Distortion 30 | - Reverb 31 | - EQ 32 | - Chorus 33 | - DONE: Compressor (good excuse to implement multiple audio input/output for machines) 34 | - Flanger 35 | - Phaser 36 | - DONE: Gate 37 | - Pan 38 | - Frequency Splitter 39 | - Mixer (multi in, single out) 40 | 41 | * Random machines 42 | - Probability gate 43 | - Send trigger from pool 44 | 45 | * DONE: LFO 46 | - Binding: machine, param 47 | - Shape 48 | - Frequency 49 | - Amplitude 50 | 51 | * Sequencer 52 | - Add patterns, add columns that map to attributes of machine. 53 | - Tell it which pattern to play 54 | - You can sequence sequencers 55 | 56 | * handle importing layout into current layout 57 | * clear layout when loading layout by default 58 | 59 | Sequencer1 -> Sequencer2 (some notes) -> MonoSynth 60 | 61 | Each column of the sequencer maps to an attribute of a machine 62 | - Binding (machine: machineID, param: paramID, interpolation: ?) 63 | - Store as int 64 | - Note type (-1 = ...), (-2 = OFF), anything else is note value 65 | - Param type (-1 = ...), (0x00 = min value) (0xff = max value) 66 | - Show graph of value next to value 67 | 68 | Sequencer1 tells Sequencer2 which patterns to play, and Sequencer2 tells MonoSynth which notes to play 69 | A sequencer can control multiple different instruments 70 | 71 | Sequencer1 -> Sequencer2 -> MonoSynth1 72 | -> Sampler1 73 | -> Sequencer3 -> PolySynth1 74 | 75 | Notes from snowglobe: 76 | - DONE: count ticks from 0 77 | - DONE: space for transport control rather than return 78 | - shift and left drag for connections (for buzz natives) 79 | - DONE: delay length of 0 crashes 80 | - DONE: mouse in pattern view 81 | - DONE: remove bindings 82 | - knob dragging more obvious 83 | - save and load buttons 84 | - DONE: preset naming, save and load buttons 85 | - DONE: preset menu 86 | - presets store sample data, or links to it? 87 | - DONE: clone patterns 88 | - resample patterns (eg, change TPB from 4 to 8 inserting blanks in between, warn when removing information) 89 | - double click on box to show machine view 90 | - fix scrolling in sequencer and parameters 91 | - scrolling in menus 92 | - DONE: resizable window 93 | - DONE: sequencer loop toggle 94 | - save window position in config file 95 | - show window decorations 96 | -------------------------------------------------------------------------------- /assets/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftsf/nimsynth/bea79caa5bf156cc2f2094842a36357f6e6129f5/assets/font.png -------------------------------------------------------------------------------- /assets/font.png.dat: -------------------------------------------------------------------------------- 1 | !"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{}~ -------------------------------------------------------------------------------- /assets/spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftsf/nimsynth/bea79caa5bf156cc2f2094842a36357f6e6129f5/assets/spritesheet.png -------------------------------------------------------------------------------- /src/core/basemachine.nim: -------------------------------------------------------------------------------- 1 | import common 2 | 3 | import nico 4 | import nico/vec 5 | 6 | import ui/machineview 7 | import util 8 | 9 | import ui/menu 10 | 11 | 12 | {.this: self.} 13 | 14 | method getMachineView*(self: Machine): View {.base.} = 15 | return newMachineView(self) 16 | 17 | method getAABB*(self: Machine): AABB {.base.} = 18 | result.min.x = (pos.x.int - 16).float32 19 | result.min.y = (pos.y.int - 4).float32 20 | result.max.x = (pos.x.int + 16).float32 21 | result.max.y = (pos.y.int + 4).float32 22 | 23 | method drawBox*(self: Machine) {.base.} = 24 | if nInputs == 0 and nOutputs > 0: 25 | # generator 26 | setColor(3) 27 | elif nInputs == 0 and nOutputs == 0: 28 | # util 29 | setColor(2) 30 | else: 31 | # fx 32 | setColor(1) 33 | rrectfill(getAABB()) 34 | setColor(if bypass: 5 elif disabled: 1 else: 6) 35 | rrect(getAABB()) 36 | printc(name, pos.x, pos.y - 2) 37 | 38 | #method layoutUpdate*(self: Machine, layout: View, df: float) {.base.} = 39 | # discard 40 | 41 | method getParameterMenu*(self: Machine, mv: Vec2f, title: string, onselect: proc(paramId: int)): Menu {.base.} = 42 | var menu = newMenu(mv, title) 43 | for i in 0.. -1: $(voice+1) & ": " else: "") & param.name, proc() = 48 | onselect(paramId) 49 | ) 50 | if param.separator: 51 | menu.items.add(newMenuItem("")) 52 | menu.items.add(item) 53 | )() 54 | return menu 55 | 56 | method getBindParameterMenu*(self: Machine, mv: Vec2f, title: string, sourceMachine: Machine, binding: Binding, onselect: proc(paramId: int)): Menu {.base.} = 57 | var menu = newMenu(mv, title) 58 | var hadItemBeforeSep = false 59 | for i in 0.. -1: $(voice+1) & ": " else: "") & param.name, proc() = 75 | onselect(paramId) 76 | ) 77 | if alreadyBound: 78 | item.status = Warning 79 | if param.separator and hadItemBeforeSep: 80 | menu.items.add(newMenuItem("")) 81 | hadItemBeforeSep = false 82 | menu.items.add(item) 83 | )() 84 | return menu 85 | 86 | method getSlotMenu*(self: Machine, mv: Vec2f, onselect: proc(slotId: int)): Menu {.base.} = 87 | var menu = newMenu(mv, "select binding slot") 88 | for i in 0.. -1: ($(voice+1) & ": ") else: "") & param.name 96 | else: 97 | str = " - " 98 | 99 | menu.items.add( 100 | newMenuItem($(slotId+1) & " -> " & str) do(): 101 | onselect(slotId) 102 | ) 103 | )() 104 | return menu 105 | 106 | method getBindingMenu*(self: Machine, mv: Vec2f, targetMachine: Machine, slotId: int = -1, onselect: proc(slotId, paramId: int)): Menu {.base.} = 107 | if nBindings > 1 and slotId == -1: 108 | # let them select the slot first 109 | return self.getSlotMenu(mv) do(slotId: int): 110 | popMenu() 111 | pushMenu(self.getBindingMenu(mv, targetMachine, slotId, onselect)) 112 | 113 | let slotId = (if slotId == -1: 0 else: slotId) 114 | 115 | var menu = self.getParameterMenu(mv, "select param") do(paramId: int): 116 | onselect(slotId, paramId) 117 | 118 | return menu 119 | 120 | method getOutputMenu*(self: Machine, mv: Vec2f, onselect: proc(outputId: int)): Menu {.base.} = 121 | var menu = newMenu(mv, "select output") 122 | for i in 0..nOutputs-1: 123 | (proc() = 124 | let outputId = i 125 | menu.items.add(newMenuItem($outputId & ": " & self.getOutputName(outputId) ) do(): 126 | onselect(outputId) 127 | ) 128 | )() 129 | return menu 130 | 131 | method getInputMenu*(self: Machine, mv: Vec2f, onselect: proc(inputId: int)): Menu {.base.} = 132 | var menu = newMenu(mv, "select input") 133 | for i in 0.. 0: 156 | menu.items.add(newMenuItem("show bindings", proc() = 157 | self.hideBindings = not self.hideBindings 158 | popMenu() 159 | )) 160 | if nOutputs > 0: 161 | menu.items.add(newMenuItem("monitor", proc() = 162 | sampleMachine = self 163 | popMenu() 164 | )) 165 | if self.disabled: 166 | menu.items.add(newMenuItem("enable", proc() = 167 | self.disabled = not self.disabled 168 | popMenu() 169 | )) 170 | else: 171 | menu.items.add(newMenuItem("disable", proc() = 172 | self.disabled = not self.disabled 173 | popMenu() 174 | )) 175 | if nInputs > 0 and nOutputs > 0: 176 | menu.items.add(newMenuItem("bypass", proc() = 177 | self.bypass = not self.bypass 178 | popMenu() 179 | )) 180 | menu.items.add(newMenuItem("reset", proc() = 181 | self.reset() 182 | popMenu() 183 | )) 184 | menu.items.add(newMenuItem("remove", proc() = 185 | self.remove() 186 | popMenu() 187 | )) 188 | menu.items.add(newMenuItem("delete", proc() = 189 | self.delete() 190 | popMenu() 191 | )) 192 | return menu 193 | 194 | method drawParams*(self: Machine, x,y,w,h: int, favOnly: bool = false) {.base.} = 195 | let paramNameWidth = 64 196 | let sliderWidth = w - paramNameWidth - 6 197 | 198 | var i = 0 199 | var yv = y 200 | for voice,param in self.parameters(favOnly): 201 | if i < self.scroll: 202 | i += 1 203 | continue 204 | i += 1 205 | if param.separator: 206 | setColor(5) 207 | line(x,yv,x+paramNameWidth + sliderWidth,yv) 208 | yv += 4 209 | 210 | setColor(if i == self.currentParam: 8 elif param.fav: 10 else: 7) 211 | print((if voice > -1: $(voice+1) & ": " else: "") & param.name, x, yv) 212 | printr(param[].valueString(param.value), x + 63, yv) 213 | var range = (param.max - param.min) 214 | if range == 0.0: 215 | range = 1.0 216 | setColor(1) 217 | # draw slider background 218 | rectfill(x + paramNameWidth, yv, x + paramNameWidth + sliderWidth, yv+4) 219 | 220 | # draw slider fill 221 | setColor(if i == currentParam: 8 else: 6) 222 | 223 | let zeroX = x + paramNameWidth + (sliderWidth.float * clamp(invLerp(param.min, param.max, 0.0f), 0.0f, 1.0f)).int 224 | 225 | rectfill(zeroX, yv, x + paramNameWidth + (sliderWidth.float * invLerp(param.min, param.max, param.value)).int, yv+4) 226 | 227 | # draw default bar 228 | if param.kind != Note: 229 | setColor(7) 230 | let defaultX = x + paramNameWidth + (sliderWidth.float * invLerp(param.min, param.max, param.default)).int 231 | line(defaultX, yv, defaultX, yv+4) 232 | 233 | yv += 8 234 | i += 1 235 | 236 | if yv >= y + h: 237 | break 238 | 239 | method updateParams*(self: Machine, x,y,w,h: int, favOnly: bool = false) {.base.} = 240 | let paramNameWidth = 64 241 | let sliderWidth = w - paramNameWidth - 6 242 | 243 | var i = 0 244 | var yv = y 245 | for voice,param in self.parameters(favOnly): 246 | if i < self.scroll: 247 | i += 1 248 | continue 249 | i += 1 250 | if param.separator: 251 | yv += 4 252 | 253 | var range = (param.max - param.min) 254 | if range == 0.0: 255 | range = 1.0 256 | 257 | yv += 8 258 | i += 1 259 | 260 | if yv >= y + h: 261 | break 262 | -------------------------------------------------------------------------------- /src/core/chords.nim: -------------------------------------------------------------------------------- 1 | type Chord* = tuple[name: string, intervals: seq[int]] 2 | const chordList*: seq[Chord] = @[ 3 | ("oct", @[0]), 4 | ("maj", @[0,4,7]), 5 | ("min", @[0,3,7]), 6 | ("dim", @[0,3,6]), 7 | ("aug", @[0,4,8]), 8 | ("sus4", @[0,5,7]), 9 | ("sus2", @[0,2,7]), 10 | ("7", @[0,4,7,10]), 11 | ("maj7", @[0,4,7,11]), 12 | ("min7", @[0,3,7,10]), 13 | ("mmaj7", @[0,3,7,11]), 14 | ("hdim", @[0,3,6,10]), 15 | ("dim7", @[0,3,6,9]), 16 | ("7dim5", @[0,4,6,10]), 17 | ("maj7dim5", @[0,4,6,11]), 18 | ("maj7aug5", @[0,4,8,11]), 19 | ("7sus4", @[0,5,7,10]), 20 | ("maj7sus4", @[0,5,7,11]), 21 | ] 22 | 23 | proc instantiateChord*(chord: Chord, baseNote: int): Chord = 24 | result.name = chord.name 25 | result.intervals = chord.intervals 26 | for i in 0.. delay: 57 | state = Attack 58 | time -= delay 59 | if state == Attack: 60 | if released: 61 | state = Release 62 | releaseStartLevel = level 63 | time = 0.0 64 | elif a == 0.0: 65 | state = Decay 66 | targetLevel = velocity 67 | time = 0.0 68 | else: 69 | targetLevel = lerp(0.0, velocity, time / a) 70 | time += invSampleRate * speed 71 | if time > a: 72 | state = Decay 73 | time -= a 74 | if state == Decay: 75 | if released: 76 | state = Release 77 | releaseStartLevel = level 78 | time = 0.0 79 | elif d == 0.0: 80 | state = Sustain 81 | targetLevel = s * velocity 82 | time = 0.0 83 | elif time > d: 84 | state = Sustain 85 | time -= d 86 | else: 87 | case decayKind: 88 | of Linear: 89 | targetLevel = lerp(velocity, s * velocity, time / d) 90 | of Exponential: 91 | let x = time / d 92 | targetLevel = lerp(velocity, s * velocity, 1.0 - pow(1.0-x, decayExp)) 93 | time += invSampleRate * speed 94 | if state == Sustain: 95 | targetLevel = s * velocity 96 | if released: 97 | state = Release 98 | releaseStartLevel = level 99 | time = 0.0 100 | if state == Release: 101 | if r == 0.0: 102 | targetLevel = 0.0 103 | state = End 104 | time = 0.0 105 | else: 106 | if time > r: 107 | state = End 108 | else: 109 | targetLevel = lerp(releaseStartLevel, 0.0, time / r) 110 | time += invSampleRate * speed 111 | 112 | if abs(level - targetLevel) > 0.01: 113 | level = lerp(level, targetLevel, 0.05) 114 | else: 115 | level = targetLevel 116 | return level 117 | 118 | proc init*(self: var Envelope) = 119 | filter.kind = Lowpass 120 | filter.init() 121 | filter.setCutoff(exp(-4.0)) 122 | decayExp = 1.0 123 | speed = 1 124 | 125 | proc trigger*(self: var Envelope, vel = 1.0'f, speed = 1'f) = 126 | self.state = Delay 127 | self.time = 0.0 128 | self.velocity = vel 129 | self.released = false 130 | self.speed = max(speed, 0.0001'f) 131 | 132 | proc triggerIfReady*(self: var Envelope, vel = 1.0, speed = 1'f) = 133 | if self.state == End or self.released: 134 | self.state = Delay 135 | self.time = 0.0 136 | self.speed = max(speed, 0.0001'f) 137 | self.velocity = vel 138 | self.released = false 139 | 140 | proc release*(self: var Envelope) = 141 | self.released = true 142 | 143 | proc drawEnv*(a,d,dexp,s,r, length: float32, x,y,w,h: int) = 144 | ## draws the envelope to the screen 145 | 146 | #let len = a + d + 1.0 + r 147 | 148 | setColor(1) 149 | line(x, y + h - 1, x + w - 1, y + h - 1) 150 | line(x, y, x + w - 1, y) 151 | 152 | # grid line each second 153 | for i in 0.. 0: 178 | val = lerp(1.0, s, 1.0 - pow(1.0-decayTime, dexp)) 179 | else: 180 | val = lerp(1.0, s, decayTime) 181 | elif time <= a + d + 1.0: 182 | state = Sustain 183 | val = s 184 | elif time <= a + d + 1.0 + r: 185 | state = Release 186 | let releaseTime = (time - a - d - 1.0) / r 187 | if r == 0: 188 | val = 0 189 | else: 190 | val = clamp(lerp(s, 0.0, releaseTime), 0.0, 1.0) 191 | else: 192 | state = End 193 | val = 0 194 | 195 | # draw the line from last to current 196 | setColor(case state: 197 | of Delay: 1 198 | of Attack: 2 199 | of Decay: 4 200 | of Sustain: 3 201 | of Release: 5 202 | of End: 1 203 | ) 204 | line(x + i - 1, y + h - (last * h.float32).int, x + i, y + h - (val * h.float32).int) 205 | last = val 206 | 207 | proc drawEnvs*(envs: openarray[EnvelopeSettings], x,y,w,h: int) = 208 | var maxLength = 1.0'f 209 | for env in envs: 210 | var envLength = env.a + env.d + 1.0'f + env.r 211 | if envLength > maxLength: 212 | maxLength = envLength 213 | maxLength += 0.25'f 214 | 215 | var yv = y 216 | var eh = h div envs.len 217 | for env in envs: 218 | yv += eh 219 | drawEnv(env.a, env.d, env.decayExp, env.s, env.r, maxLength, x,yv,w,eh) 220 | yv += 4 221 | 222 | proc drawEnvs*(envs: openarray[Envelope], x,y,w,h: int) = 223 | var maxLength = 1.0'f 224 | for env in envs: 225 | var envLength = env.a + env.d + 1.0'f + env.r 226 | if envLength > maxLength: 227 | maxLength = envLength 228 | maxLength += 0.25'f 229 | 230 | var yv = y 231 | var eh = h div envs.len 232 | for env in envs: 233 | yv += eh 234 | drawEnv(env.a, env.d, env.decayExp, env.s, env.r, maxLength, x,yv,w,eh) 235 | yv += 4 236 | -------------------------------------------------------------------------------- /src/core/fft.nim: -------------------------------------------------------------------------------- 1 | import complex 2 | import math 3 | 4 | proc toComplex[T](x: T): Complex[T] = 5 | result.re = x 6 | 7 | proc fft[T](x: openarray[T]): seq[Complex[T]] = 8 | let n = x.len 9 | result = newSeq[Complex[T]]() 10 | if n <= 1: 11 | for v in x: result.add toComplex(v) 12 | return 13 | var evens,odds = newSeq[T]() 14 | for i,v in x: 15 | if i mod 2 == 0: evens.add(v) 16 | else: odds.add(v) 17 | var (even, odd) = (fft(evens), fft(odds)) 18 | 19 | for k in 0.. tape.high: 22 | writeHead = 0 23 | # readHead should be exactly delayTime behind writeHead 24 | readHead = (writeHead - floor(delayTime * sampleRate).int) %%/ tape.len 25 | let readHead1 = if readHead - 1 < 0: tape.high else: readHead - 1 26 | var alpha = (delayTime * sampleRate) mod 1.0 27 | result = lerp(tape[readHead], tape[readHead1], alpha) 28 | 29 | if readHead > tape.high: 30 | writeHead = 0 31 | 32 | tape[writeHead] = input + feedback * result 33 | -------------------------------------------------------------------------------- /src/core/noise.nim: -------------------------------------------------------------------------------- 1 | import bitops 2 | import math 3 | 4 | type Noise* = object 5 | value: uint32 6 | 7 | func extractBit[T: SomeInteger](v: T, bit: BitsRange[T]): T = 8 | v and (1.T shl bit) 9 | 10 | proc next*(self: var Noise): float32 = 11 | if self.value == 0: 12 | self.value = 0xdeadbeef'u32 13 | 14 | let b0 = extractBit(self.value, 0) 15 | let b1 = extractBit(self.value, 1) 16 | let b27 = extractBit(self.value, 27) 17 | let b28 = extractBit(self.value, 28) 18 | 19 | var b31 = b0 xor b1 xor b27 xor b28 20 | 21 | if b31 == 1'u32: 22 | b31 = 0x1000_0000'u32 23 | 24 | self.value = self.value shr 1'u32 25 | 26 | self.value = self.value or b31 27 | 28 | return self.value.float32 29 | 30 | import unittest 31 | 32 | suite "noise": 33 | test "noise": 34 | var n = Noise(value: 0xdeadbeef'u32) 35 | echo n.next() 36 | echo n.next() 37 | echo n.next() 38 | echo n.next() 39 | echo n.next() 40 | -------------------------------------------------------------------------------- /src/core/oscillator.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import core/noise 4 | from common import sampleRate, invSampleRate 5 | 6 | type 7 | OscKind* = enum 8 | Sin 9 | Tri 10 | Sqr 11 | Saw 12 | Noise 13 | FatSaw 14 | 15 | Osc* = object of RootObj 16 | kind*: OscKind 17 | phase*: float32 18 | freq: float32 19 | phaseIncrement*: float32 20 | pulseWidth*: float32 21 | cycled*: bool 22 | 23 | s,c: float32 24 | 25 | sinOut*: float32 26 | sawOut*: float32 27 | sqrOut*: float32 28 | triOut*: float32 29 | noiseOut*: float32 30 | fatSawOut*: float32 31 | 32 | LFOOsc* = object of RootObj 33 | kind*: OscKind 34 | phase*: float32 35 | freq: float32 36 | pulseWidth*: float32 37 | phaseIncrement*: float32 38 | 39 | sinOut*: float32 40 | sawOut*: float32 41 | sqrOut*: float32 42 | triOut*: float32 43 | noiseOut*: float32 44 | fatSawOut*: float32 45 | 46 | proc init*(self: var Osc) = 47 | self.c = 1'f 48 | self.s = 0'f 49 | 50 | proc `freq=`*(self: var Osc, freq: float32) = 51 | self.freq = freq 52 | self.phaseIncrement = (freq * invSampleRate) 53 | 54 | func freq*(self: Osc): float32 = 55 | return self.freq 56 | 57 | proc process*(self: var Osc, offset: float32 = 0'f): float32 {.inline.} = 58 | self.phase += self.phaseIncrement 59 | 60 | if self.phase > 1'f: 61 | self.phase -= 1'f 62 | self.cycled = true 63 | self.noiseOut = rand(2'f) - 1'f 64 | else: 65 | self.cycled = false 66 | 67 | let p = floorMod(self.phase + offset, 1'f) 68 | 69 | self.sawOut = floorMod(p, 1'f) * 2'f - 1'f 70 | self.sqrOut = if p > self.pulseWidth: -1'f else: 1'f 71 | self.triOut = abs(self.sawOut) * 2'f - 1'f 72 | self.sinOut = sin(p * TAU) 73 | self.fatSawOut = tanh(1'f*self.sawOut) / tanh(1'f) 74 | 75 | case self.kind: 76 | of Sin: 77 | return self.sinOut 78 | of Saw: 79 | return self.sawOut 80 | of Sqr: 81 | return self.sqrOut 82 | of Tri: 83 | return self.triOut 84 | of Noise: 85 | return self.noiseOut 86 | of FatSaw: 87 | return self.fatSawOut 88 | 89 | proc `freq=`*(self: var LFOOsc, freq: float32) {.inline.} = 90 | self.freq = freq 91 | self.phaseIncrement = (freq * invSampleRate) 92 | 93 | func freq*(self: LFOOsc): float32 = 94 | return self.freq 95 | 96 | proc process*(self: var LFOOsc): float32 {.inline.} = 97 | self.phase += self.phaseIncrement 98 | if self.phase > 1'f: 99 | self.phase -= 1'f 100 | self.sawOut = self.phase * 2'f - 1'f 101 | self.sqrOut = if self.phase > self.pulseWidth: -1'f else: 1'f 102 | self.triOut = abs(self.sawOut) * 2'f - 1'f 103 | self.sinOut = sin(self.phase * TAU) 104 | self.noiseOut = rand(2'f) - 1'f 105 | self.fatSawOut = tanh(1'f*self.sawOut) / tanh(1'f) 106 | case self.kind: 107 | of Sin: 108 | return self.sinOut 109 | of Saw: 110 | return self.sawOut 111 | of Sqr: 112 | return self.sqrOut 113 | of Tri: 114 | return self.triOut 115 | of Noise: 116 | return self.noiseOut 117 | of FatSaw: 118 | return self.fatSawOut 119 | 120 | proc peek*(self: LFOOsc, phase: float32): float32 {.inline.} = 121 | let phase = floorMod(phase, 1'f) 122 | let sawOut = phase * 2'f - 1'f 123 | let sqrOut = if phase > self.pulseWidth: -1'f else: 1'f 124 | let triOut = abs(sawOut) * 2'f - 1'f 125 | let sinOut = sin(phase * TAU) 126 | let noiseOut = rand(2'f) - 1'f 127 | let fatSawOut = tanh(0.5'f*sawOut) / tanh(0.5'f) 128 | 129 | case self.kind: 130 | of Sin: 131 | return sinOut 132 | of Saw: 133 | return sawOut 134 | of Sqr: 135 | return sqrOut 136 | of Tri: 137 | return triOut 138 | of Noise: 139 | return noiseOut 140 | of FatSaw: 141 | return fatSawOut 142 | 143 | -------------------------------------------------------------------------------- /src/core/ringbuffer.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | 3 | type 4 | RingBuffer*[T] = object 5 | data: seq[T] 6 | head*, tail*: int 7 | size*, length*: int 8 | 9 | 10 | proc newRingBuffer*[T](length: int): RingBuffer[T] = 11 | let s = newSeq[T](length) 12 | RingBuffer[T](data: s, head: 0, tail: -1, size: 0, length: length) 13 | 14 | template adjustHead(b: untyped): typed = 15 | b.head = (b.length + b.tail - b.size + 1) mod b.length 16 | 17 | template adjustTail(b, change: untyped): typed = 18 | b.tail = (b.tail + change) mod b.length 19 | 20 | proc add*[T](b: var RingBuffer[T], data: openArray[T]) = 21 | for item in data: 22 | b.adjustTail(1) 23 | b.data[b.tail] = item 24 | b.size = min(b.size + len(data), b.length) 25 | b.adjustHead() 26 | 27 | proc setLen*[T](b: var RingBuffer[T], newLen: int) = 28 | b.data.setLen(newLen) 29 | b.length = newLen 30 | 31 | proc `[]`*[T](b: RingBuffer[T], idx: int): T {.inline} = 32 | b.data[(idx + b.head) mod b.length] 33 | 34 | proc `[]=`*[T](b: var RingBuffer[T], idx: int, item: T) {.raises: [IndexError].} = 35 | ## Set an item at index (adjusted) 36 | if idx == b.size: inc(b.size) 37 | elif idx > b.size: raise newException(IndexError, "Index " & $idx & " out of bound") 38 | -------------------------------------------------------------------------------- /src/core/scales.nim: -------------------------------------------------------------------------------- 1 | type Scale* = object 2 | name*: string 3 | notes*: seq[int] 4 | 5 | type ConcreteScale* = object 6 | name*: string 7 | baseNote*: int 8 | notes*: seq[int] 9 | 10 | const scaleMajor* = Scale(name: "Major", notes: @[0, 2, 4, 5, 7, 9, 11]) 11 | const scaleMinor* = Scale(name: "Minor", notes: @[0, 2, 3, 5, 7, 8, 10]) 12 | const scaleMajorTriad* = Scale(name: "MajorTriad", notes: @[0, 4, 7, 11]) 13 | const scaleMinorTriad* = Scale(name: "MinorTriad", notes: @[0, 3, 7, 10]) 14 | const scaleDorian* = Scale(name: "Dorian", notes: @[0, 2, 3, 6, 7, 9, 10]) 15 | const scalePentatonic* = Scale(name: "Pentatonic", notes: @[0, 2, 5, 7, 9]) 16 | const scaleBlues* = Scale(name: "Blues", notes: @[0, 3, 5, 6, 7, 10]) 17 | const scaleChromatic* = Scale(name: "Chromatic", notes: @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) 18 | 19 | const scaleList* = [ 20 | scaleMajor, 21 | scaleMinor, 22 | scaleMajorTriad, 23 | scaleMinorTriad, 24 | scaleDorian, 25 | scalePentatonic, 26 | scaleBlues, 27 | scaleChromatic, 28 | ] 29 | 30 | proc instantiateScale*(scale: Scale, baseNote: int): ConcreteScale = 31 | result.name = scale.name 32 | result.baseNote = baseNote 33 | result.notes = scale.notes 34 | for i in 0.. value: 25 | value = attack * (value - sample) + sample 26 | else: 27 | value = release * (value - sample) + sample 28 | 29 | type Compressor = ref object of Machine 30 | threshold: float32 31 | ratio: float32 32 | invRatio: float32 33 | env: EnvDetector 34 | preGain,postGain: float32 35 | inputLevelL: float32 36 | inputLevelR: float32 37 | inputSample: float32 38 | reduction: float32 39 | rms: array[rmsSize, float32] 40 | 41 | 42 | method init(self: Compressor) = 43 | procCall init(Machine(self)) 44 | 45 | nInputs = 2 46 | nOutputs = 1 47 | stereo = true 48 | 49 | name = "comp" 50 | 51 | self.globalParams.add([ 52 | Parameter(name: "threshold", kind: Float, min: 0.0, max: 1.0, default: 1.00, onchange: proc(newValue: float32, voice: int) = 53 | self.threshold = newValue 54 | ), 55 | Parameter(name: "ratio", kind: Float, min: 1.0, max: 12.0, default: 3.00, onchange: proc(newValue: float32, voice: int) = 56 | self.ratio = newValue 57 | self.invRatio = 1.0/newValue 58 | ), 59 | Parameter(name: "attack", kind: Float, min: 0.0001, max: 0.5, default: 0.01, onchange: proc(newValue: float32, voice: int) = 60 | self.env.attack = exp(ln(0.01) / (newValue * sampleRate)) 61 | ), 62 | Parameter(name: "release", kind: Float, min: 0.01, max: 5.0, default: 0.05, onchange: proc(newValue: float32, voice: int) = 63 | self.env.release = exp(ln(0.01) / (newValue * sampleRate)) 64 | ), 65 | Parameter(name: "detector", kind: Int, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 66 | self.env.kind = newValue.EnvDetectorKind 67 | ), 68 | Parameter(name: "pre gain", kind: Float, min: 0.0, max: 2.0, default: 1.00, onchange: proc(newValue: float32, voice: int) = 69 | self.preGain = newValue 70 | ), 71 | Parameter(name: "post gain", kind: Float, min: 0.0, max: 2.0, default: 1.00, onchange: proc(newValue: float32, voice: int) = 72 | self.postGain = newValue 73 | ), 74 | ]) 75 | 76 | setDefaults() 77 | 78 | proc getReduction(self: Compressor, input: float32): float32 = 79 | let slope = 1.0 - invRatio 80 | return if input >= threshold: slope * (threshold - input) else: 0.0 81 | 82 | method process(self: Compressor) {.inline.} = 83 | # set input level 84 | if inputs.len == 0: 85 | inputLevelL = 0.0 86 | inputLevelR = 0.0 87 | outputSamples[0] = 0.0 88 | return 89 | 90 | let s = getInput() * preGain 91 | self.inputSample = s 92 | let s1 = if hasInput(1): getInput(1) * preGain else: s 93 | 94 | if sampleId mod 2 == 0: 95 | inputLevelL = abs(s1) 96 | else: 97 | inputLevelR = abs(s1) 98 | 99 | env.process(max(inputLevelL, inputLevelR)) 100 | 101 | reduction = getReduction(env.value) 102 | 103 | let compGain = 1.0 + reduction 104 | 105 | outputSamples[0] = s * compGain * postGain 106 | 107 | proc newCompressor(): Machine = 108 | var comp = new(Compressor) 109 | comp.init() 110 | return comp 111 | 112 | method drawExtraData(self: Compressor, x,y,w,h: int) = 113 | var yv = y 114 | setColor(11) 115 | rectfill(x, yv, x + (w-1).float32 * abs(inputSample), yv + 4) 116 | yv += 5 117 | setColor(10) 118 | rectfill(x + (w-1) - (w-1).float32 * abs(reduction), yv, x + (w-1), yv + 4) 119 | 120 | yv += 5 121 | # draw plot 122 | setColor(8) 123 | var xv = x 124 | while xv < x+w-1: 125 | let input = (xv - x).float32 / w.float32 126 | let output = clamp(input + getReduction(input), 0.0, 1.0) 127 | pset(xv, yv + w - (output * w.float32)) 128 | xv += 1 129 | setColor(9) 130 | xv = x 131 | while xv < x+(w-1).float32 * env.value: 132 | let input = (xv - x).float32 / w.float32 133 | let output = clamp(input + getReduction(input), 0.0, 1.0) 134 | line(xv, yv + w, xv, yv + w - (output * w.float32)) 135 | xv += 1 136 | 137 | method getInputName(self: Compressor, inputId: int): string = 138 | if inputId == 0: 139 | return "main" 140 | else: 141 | return "sidechain" 142 | 143 | 144 | registerMachine("compressor", newCompressor, "fx") 145 | -------------------------------------------------------------------------------- /src/machines/fx/distortion.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import common 3 | 4 | {.this:self.} 5 | 6 | type 7 | DistortionKind* = enum 8 | Foldback 9 | HardClip 10 | SoftClip 11 | Distortion* = object of RootObj 12 | kind*: DistortionKind 13 | preGain*: float32 14 | threshold*: float32 15 | postGain*: float32 16 | mix: float32 17 | feedback: float32 18 | 19 | proc process*(self: Distortion, sample: float32): float32 = 20 | var drySignal = sample * preGain 21 | var wetSignal = drySignal 22 | if abs(wetSignal) > threshold: 23 | case kind: 24 | of Foldback: 25 | wetSignal = abs(abs((wetSignal - threshold) mod (threshold * 4.0)) - threshold * 2.0) - threshold 26 | of HardClip: 27 | wetSignal = clamp(wetSignal, -threshold, threshold) 28 | of SoftClip: 29 | wetSignal = tanh(wetSignal * (1.0 / threshold)) 30 | result = (wetSignal * postGain * mix) + (drySignal * (1.0 - mix)) 31 | 32 | type 33 | DistortionMachine = ref object of Machine 34 | distortion: Distortion 35 | 36 | method init(self: DistortionMachine) = 37 | procCall init(Machine(self)) 38 | name = "dist" 39 | nInputs = 1 40 | nOutputs = 1 41 | stereo = true 42 | 43 | self.globalParams.add([ 44 | Parameter(name: "dist", kind: Int, min: DistortionKind.low.float32, max: DistortionKind.high.float32, default: HardClip.float32, onchange: proc(newValue: float32, voice: int) = 45 | self.distortion.kind = newValue.DistortionKind 46 | , getValueString: proc(value: float32, voice: int): string = 47 | return $value.DistortionKind 48 | ), 49 | Parameter(name: "pre", kind: Float, min: 0.0, max: 2.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 50 | self.distortion.preGain = newValue 51 | ), 52 | Parameter(name: "mix", kind: Float, min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 53 | self.distortion.mix = newValue 54 | ), 55 | Parameter(name: "threshold", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 56 | self.distortion.threshold = newValue 57 | ), 58 | Parameter(name: "post", kind: Float, min: 0.0, max: 2.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 59 | self.distortion.postGain = newValue 60 | ) 61 | ]) 62 | 63 | setDefaults() 64 | 65 | 66 | method process(self: DistortionMachine) {.inline.} = 67 | outputSamples[0] = getInput() 68 | outputSamples[0] = self.distortion.process(outputSamples[0]) 69 | 70 | proc newDistortionMachine(): Machine = 71 | result = new(DistortionMachine) 72 | result.init() 73 | 74 | registerMachine("distortion", newDistortionMachine, "fx") 75 | -------------------------------------------------------------------------------- /src/machines/fx/eq.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import common 4 | import util 5 | import nico 6 | 7 | import core/filter 8 | import core/fft 9 | 10 | 11 | {.this:self.} 12 | 13 | const nFilters = 3 14 | const bufferSize = 512 15 | 16 | type 17 | EQ = ref object of Machine 18 | filtersL: array[nFilters,BiquadFilter] 19 | filtersR: array[nFilters,BiquadFilter] 20 | filtersOn: array[nFilters,bool] 21 | inputBuffer: array[bufferSize, float32] 22 | outputBuffer: array[bufferSize, float32] 23 | writeHead: int 24 | 25 | method init(self: EQ) = 26 | procCall init(Machine(self)) 27 | name = "eq" 28 | nInputs = 1 29 | nOutputs = 1 30 | stereo = true 31 | 32 | for i in 0.. 0.0: 50 | case clockRateUnit: 51 | of PerSecond: 52 | clock += clockRate / sampleRate 53 | of PerBeat: 54 | clock += clockRate / sampleRate * beatsPerSecond() 55 | of EverySecond: 56 | clock += 1.0 / (clockRate * sampleRate) 57 | of EveryBeat: 58 | clock += 1.0 / (clockRate * sampleRate * beatsPerSecond()) 59 | 60 | if clock >= 1.0: 61 | triggered = true 62 | clock -= 1.0 63 | clock = clamp(clock, 0.0, 1.0) 64 | 65 | if triggered: 66 | if bindings[0].isBound(): 67 | var (voice,param) = bindings[0].getParameter() 68 | param.value = 1.0 69 | param.onchange(1.0, voice) 70 | triggered = false 71 | 72 | globalParams[2].value = clock 73 | 74 | proc newClock(): Machine = 75 | var m = new(Clock) 76 | m.init() 77 | return m 78 | 79 | registerMachine("clock", newClock, "generator") 80 | -------------------------------------------------------------------------------- /src/machines/generators/eadsr.nim: -------------------------------------------------------------------------------- 1 | import strutils 2 | import math 3 | import util 4 | 5 | import common 6 | 7 | import core.envelope 8 | 9 | 10 | # envelope 11 | 12 | {.this:self.} 13 | 14 | type 15 | EADSRMachine = ref object of Machine 16 | env: Envelope 17 | lastval: float32 18 | min: float32 19 | max: float32 20 | 21 | method init(self: EADSRMachine) = 22 | procCall init(Machine(self)) 23 | name = "eadsr" 24 | nInputs = 0 25 | nOutputs = 0 26 | stereo = false 27 | 28 | nBindings = 1 29 | bindings.setLen(1) 30 | 31 | env.init() 32 | 33 | globalParams.add([ 34 | Parameter(kind: Trigger, name: "trigger", min: 0, max: 1, onchange: proc(newValue: float32, voice: int) = 35 | if newValue == OffNote or newValue == 0: 36 | env.release() 37 | else: 38 | env.trigger() 39 | ), 40 | Parameter(kind: Float, name: "min", min: -10000'f, max: 10000.0'f, default: 0.0'f, onchange: proc(newValue: float32, voice: int) = 41 | self.min = newValue 42 | ), 43 | Parameter(kind: Float, name: "max", min: -10000'f, max: 10000.0'f, default: 1.0'f, onchange: proc(newValue: float32, voice: int) = 44 | self.max = newValue 45 | ), 46 | Parameter(name: "a", kind: Float, separator: true, min: 0.0, max: 5.0, default: 0.001, onchange: proc(newValue: float32, voice: int) = 47 | self.env.a = exp(newValue) - 1.0 48 | , getValueString: proc(value: float32, voice: int): string = 49 | return (exp(value) - 1.0).formatFloat(ffDecimal, 2) & " s" 50 | ), 51 | Parameter(name: "d", kind: Float, min: 0.0, max: 5.0, default: 0.1, onchange: proc(newValue: float32, voice: int) = 52 | self.env.d = exp(newValue) - 1.0 53 | , getValueString: proc(value: float32, voice: int): string = 54 | return (exp(value) - 1.0).formatFloat(ffDecimal, 2) & " s" 55 | ), 56 | Parameter(name: "ds", kind: Float, min: 0.1, max: 10.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 57 | self.env.decayExp = newValue 58 | ), 59 | Parameter(name: "s", kind: Float, min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 60 | self.env.s = newValue 61 | ), 62 | Parameter(name: "r", kind: Float, min: 0.0, max: 5.0, default: 0.01, onchange: proc(newValue: float32, voice: int) = 63 | self.env.r = exp(newValue) - 1.0 64 | , getValueString: proc(value: float32, voice: int): string = 65 | return (exp(value) - 1.0).formatFloat(ffDecimal, 2) & " s" 66 | ), 67 | ]) 68 | 69 | setDefaults() 70 | 71 | method createBinding*(self: EADSRMachine, slot: int, target: Machine, paramId: int) = 72 | procCall createBinding(Machine(self), slot, target, paramId) 73 | 74 | # match input to be the same as the target param 75 | var (voice,param) = target.getParameter(paramId) 76 | var inputParam = globalParams[1].addr 77 | inputParam.kind = param.kind 78 | inputParam.min = param.min 79 | inputParam.max = param.max 80 | inputParam.getValueString = param.getValueString 81 | 82 | inputParam = globalParams[2].addr 83 | inputParam.kind = param.kind 84 | inputParam.min = param.min 85 | inputParam.max = param.max 86 | inputParam.getValueString = param.getValueString 87 | 88 | method process(self: EADSRMachine) = 89 | var val = env.process() 90 | 91 | if bindings[0].isBound() and val != lastval: 92 | var (voice,param) = bindings[0].getParameter() 93 | param.value = lerp(self.min, self.max, val) 94 | param.onchange(param.value, voice) 95 | 96 | lastval = val 97 | 98 | proc newEADSRMachine(): Machine = 99 | var m = new(EADSRMachine) 100 | m.init() 101 | return m 102 | 103 | registerMachine("eadsr", newEADSRMachine, "generator") 104 | -------------------------------------------------------------------------------- /src/machines/generators/kayoubi.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import random 5 | 6 | import nico 7 | import nico/vec 8 | 9 | import common 10 | import util 11 | 12 | import core/scales 13 | 14 | import core/basemachine 15 | import ui/machineview 16 | import machines/master 17 | 18 | type 19 | KayoubiAlgorithm = enum 20 | kaTriTrance 21 | kaStomper 22 | kaMrMarkov 23 | kaWobble 24 | kaChipArp1 25 | kaChipArp2 26 | kaSampleAndHoldOn 27 | kaSaikoClassic 28 | kaSaikoLead 29 | kaScaleWalker 30 | kaTooEasy 31 | kaTestPattern 32 | 33 | KayoubiScale = enum 34 | ksMajor 35 | ksMinor 36 | ksDorian 37 | ksPentatonic 38 | ksMajorTriad 39 | ksMinorTriad 40 | ksBlues 41 | ksChromatic 42 | 43 | Kayoubi = ref object of Machine 44 | algorithm: KayoubiAlgorithm 45 | scale: KayoubiScale 46 | ticksPerBeat: int 47 | beatsPerLoop: int 48 | x,y: float32 49 | density: float32 50 | seed: int 51 | baseNote: int 52 | playing: bool 53 | 54 | tickTimer: int 55 | tickCounter: int 56 | beatCounter: int 57 | 58 | rand1: Rand 59 | rand2: Rand 60 | b1,b2,b3,b4: int 61 | 62 | outNote: int 63 | outOct: int 64 | 65 | proc initAlgorithmTriTrance(self: Kayoubi) = 66 | self.rand1 = initRand(self.seed + 1) 67 | self.rand2 = initRand(self.seed + 2) 68 | 69 | self.b1 = self.rand1.next().int and 0x7 70 | self.b2 = self.rand1.next().int and 0x7 71 | self.b3 = self.rand2.next().int and 0x15 72 | if self.b3 >= 7: 73 | self.b3 -= 7 74 | else: 75 | self.b3 = 0 76 | self.b3 -= 4 77 | 78 | self.b4 = 0 79 | 80 | proc initAlgorithm(self: Kayoubi) = 81 | case self.algorithm: 82 | of kaTriTrance: 83 | self.initAlgorithmTriTrance() 84 | else: 85 | discard 86 | 87 | proc scaleToNote(self: Kayoubi): int = 88 | var octOffset = self.outOct 89 | var scaleIdx = self.outNote 90 | 91 | let scale = case self.scale: 92 | of ksMajor: 93 | scaleMajor 94 | of ksMinor: 95 | scaleMinor 96 | of ksMajorTriad: 97 | scaleMajorTriad 98 | of ksMinorTriad: 99 | scaleMinorTriad 100 | of ksDorian: 101 | scaleDorian 102 | of ksPentatonic: 103 | scalePentatonic 104 | of ksBlues: 105 | scaleBlues 106 | else: 107 | scaleMajor 108 | 109 | while scaleIdx < 0: 110 | scaleIdx += scale.notes.len 111 | octOffset -= 1 112 | 113 | while scaleIdx >= scale.notes.len: 114 | scaleIdx -= scale.notes.len 115 | octOffset += 1 116 | 117 | return self.baseNote + 12 * octOffset + scale.notes[scaleIdx mod scale.notes.len] 118 | 119 | proc setNote(self: Kayoubi, aoct, anote: int) = 120 | self.outOct = aoct 121 | self.outNote = anote 122 | 123 | proc processTriTrance(self: Kayoubi, I: int) = 124 | case (I + self.b2) mod 3: 125 | of 0: 126 | if self.rand2.rand(1) == 1 and self.rand2.rand(1) == 1: 127 | self.b3 = self.rand2.next().int and 0x15 128 | if self.b3 >= 7: 129 | self.b3 -= 7 130 | else: 131 | self.b3 = 0 132 | self.b3 -= 4 133 | self.setNote(0, self.b3) 134 | of 1: 135 | self.setNote(1, self.b3) 136 | if self.rand1.rand(1) == 1: 137 | self.b2 = self.rand1.next().int and 0x7 138 | of 2: 139 | self.setNote(2, self.b1) 140 | if self.rand1.rand(1) == 1: 141 | self.b1 = (self.rand1.next().int shr 5) and 0x7 142 | else: 143 | discard 144 | 145 | let n = self.scaleToNote() 146 | 147 | if self.rand1.rand(1.0) < self.density: 148 | if self.bindings[0].isBound(): 149 | var (voice,param) = self.bindings[0].getParameter() 150 | param.value = n.float32 151 | param.onchange(param.value, voice) 152 | 153 | 154 | method init(self: Kayoubi) = 155 | procCall init(Machine(self)) 156 | 157 | self.name = "kayoubi" 158 | self.nOutputs = 0 159 | self.nInputs = 0 160 | self.nBindings = 1 161 | self.bindings.setLen(1) 162 | 163 | self.globalParams.add([ 164 | Parameter(kind: Int, name: "algorithm", min: 0.0, max: KayoubiAlgorithm.high.float32, default: 0.0, onchange: proc(newValue: float32, voice: int) = 165 | self.algorithm = newValue.int.KayoubiAlgorithm 166 | self.initAlgorithm() 167 | , getValueString: proc(value: float32, voice: int): string = 168 | return $value.KayoubiAlgorithm 169 | ), 170 | Parameter(kind: Note, name: "base", min: OffNote, max: 255.0, default: 48.0, onchange: proc(newValue: float32, voice: int) = 171 | self.baseNote = newValue.int 172 | ), 173 | Parameter(kind: Trigger, name: "trigger", min: 0.0, max: 1.0, ignoreSave: true, default: 0.0, onchange: proc(newValue: float32, voice: int) = 174 | self.playing = newValue.int != 0 175 | self.tickTimer = 0 176 | self.tickCounter = 0 177 | self.beatCounter = 0 178 | ), 179 | Parameter(kind: Int, name: "scale", min: 0.0, max: KayoubiScale.high.float32, default: 0.0, onchange: proc(newValue: float32, voice: int) = 180 | self.scale = newValue.int.KayoubiScale 181 | , getValueString: proc(value: float32, voice: int): string = 182 | return $value.KayoubiScale 183 | ), 184 | Parameter(kind: Int, name: "TPB", min: 1, max: 16.0, default: 5.0, onchange: proc(newValue: float32, voice: int) = 185 | self.ticksPerBeat = newValue.int 186 | ), 187 | Parameter(kind: Int, name: "BPL", min: 4.0, max: 32.0, default: 4.0, onchange: proc(newValue: float32, voice: int) = 188 | self.beatsPerLoop = newValue.int 189 | ), 190 | Parameter(kind: Float, name: "X", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 191 | self.x = newValue 192 | ), 193 | Parameter(kind: Float, name: "Y", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 194 | self.y = newValue 195 | ), 196 | Parameter(kind: Float, name: "density", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 197 | self.density = newValue 198 | ), 199 | Parameter(kind: Int, name: "seed", min: 0.0, max: (2^15).float32, default: 0.0, onchange: proc(newValue: float32, voice: int) = 200 | self.seed = newValue.int 201 | self.initAlgorithm() 202 | ), 203 | ]) 204 | 205 | self.setDefaults() 206 | 207 | method process(self: Kayoubi) = 208 | if self.baseNote == OffNote or not self.playing: 209 | return 210 | self.tickTimer -= 1 211 | if self.tickTimer <= 0: 212 | self.tickTimer += (sampleRate.int / (beatsPerSecond() * self.ticksPerBeat)).int 213 | self.tickCounter += 1 214 | if self.tickCounter >= self.ticksPerBeat: 215 | self.tickCounter = 0 216 | self.beatCounter += 1 217 | 218 | case self.algorithm: 219 | of kaTriTrance: 220 | self.processTriTrance(self.tickCounter + self.beatCounter) 221 | else: 222 | discard 223 | 224 | proc newKayoubi(): Machine = 225 | var m = new(Kayoubi) 226 | m.init() 227 | return m 228 | 229 | registerMachine("kayoubi", newKayoubi, "generator") 230 | -------------------------------------------------------------------------------- /src/machines/generators/kit.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | import nico/vec 6 | 7 | import common 8 | 9 | import core/envelope 10 | import core/sample 11 | import ui/menu 12 | 13 | 14 | type 15 | Kit = ref object of Machine 16 | KitVoice = ref object of Voice 17 | playing: bool 18 | osc: SampleOsc 19 | env: Envelope 20 | useEnv: bool 21 | gain: float32 22 | 23 | {.this:self.} 24 | 25 | method addVoice*(self: Kit) = 26 | var voice = new(KitVoice) 27 | voices.add(voice) 28 | voice.init(self) 29 | voice.env.init() 30 | voice.osc.stereo = true 31 | 32 | for param in mitems(voice.parameters): 33 | param.value = param.default 34 | param.onchange(param.value, voices.high) 35 | 36 | method init(self: Kit) = 37 | procCall init(Machine(self)) 38 | name = "kit" 39 | nOutputs = 1 40 | nInputs = 0 41 | stereo = true 42 | useKeyboard = true 43 | 44 | voiceParams.add([ 45 | Parameter(name: "trigger", separator: true, deferred: true, kind: Float, min: 0.0, max: 1.0, onchange: proc(newValue: float32, voice: int) = 46 | var v = KitVoice(self.voices[voice]) 47 | if v.osc.sample != nil: 48 | v.playing = true 49 | v.osc.reset() 50 | v.gain = newValue 51 | v.env.trigger() 52 | ), 53 | Parameter(name: "pitch", kind: Float, min: 0.5, max: 2.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 54 | var v = KitVoice(self.voices[voice]) 55 | v.osc.speed = newValue 56 | ), 57 | Parameter(name: "decay", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 58 | var v = KitVoice(self.voices[voice]) 59 | v.env.d = newValue 60 | ), 61 | Parameter(name: "use env", kind: Bool, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 62 | var v = KitVoice(self.voices[voice]) 63 | v.useEnv = newValue.bool 64 | ), 65 | ]) 66 | 67 | self.setDefaults() 68 | 69 | for i in 0..<4: 70 | self.addVoice() 71 | 72 | method process*(self: Kit) {.inline.} = 73 | outputSamples[0] = 0.0 74 | for i in 0..= 0 and voice < voices.len: 97 | # open sample selection menu 98 | pushMenu(newSampleMenu(vec2f(mx,my), "samples/") do(sample: Sample): 99 | var v = KitVoice(self.voices[voice]) 100 | v.osc.sample = sample 101 | if voice >= 0 and voice < voices.high: 102 | voice += 1 103 | ) 104 | 105 | method saveExtraData(self: Kit): string = 106 | result = "" 107 | for voice in mitems(voices): 108 | var v = KitVoice(voice) 109 | if v.osc.sample != nil: 110 | result &= v.osc.sample.filename & "|" & v.osc.sample.name & "\n" 111 | else: 112 | result &= "\n" 113 | 114 | method loadExtraData(self: Kit, data: string) = 115 | var voice = 0 116 | for line in data.splitLines: 117 | if voice > voices.high: 118 | break 119 | let sline = line.strip() 120 | if sline == "": 121 | voice += 1 122 | continue 123 | var v = KitVoice(voices[voice]) 124 | v.osc.sample = loadSample(sline.split("|")[0], sline.split("|")[1]) 125 | voice += 1 126 | 127 | 128 | proc newKit(): Machine = 129 | var kit = new(Kit) 130 | kit.init() 131 | return kit 132 | 133 | registerMachine("kit", newKit, "generator") 134 | -------------------------------------------------------------------------------- /src/machines/generators/looper.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | 6 | import common 7 | 8 | import core/envelope 9 | import core/sample 10 | import ui/menu 11 | 12 | import machines.master 13 | 14 | 15 | type 16 | Looper = ref object of Machine 17 | playing: bool 18 | osc: SampleOsc 19 | 20 | {.this:self.} 21 | 22 | method init(self: Looper) = 23 | procCall init(Machine(self)) 24 | name = "looper" 25 | nOutputs = 1 26 | nInputs = 0 27 | stereo = true 28 | 29 | osc.stereo = true 30 | 31 | globalParams.add([ 32 | Parameter(name: "trigger", separator: true, deferred: true, kind: Trigger, min: 0.0, max: 1.0, onchange: proc(newValue: float32, voice: int) = 33 | if osc.sample != nil: 34 | playing = true 35 | osc.reset() 36 | ), 37 | Parameter(name: "loop", kind: Bool, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 38 | osc.loop = newValue.bool 39 | ), 40 | Parameter(name: "speed", kind: Float, min: 0.00001, max: 4.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 41 | osc.speed = newValue 42 | ), 43 | Parameter(name: "length", kind: Int, min: 1.0, max: 64.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 44 | osc.setSpeedByLength(newValue.int.float32 / beatsPerSecond()) 45 | globalParams[2].value = osc.speed 46 | ), 47 | Parameter(name: "offset", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 48 | osc.offset = newValue 49 | ), 50 | ]) 51 | 52 | setDefaults() 53 | 54 | method onBPMChange(self: Looper, bpm: int) = 55 | let length = globalParams[3].value.int 56 | osc.setSpeedByLength(length.float32 / beatsPerSecond()) 57 | globalParams[2].value = osc.speed 58 | 59 | method process*(self: Looper) {.inline.} = 60 | if osc.sample != nil: 61 | if playing: 62 | outputSamples[0] = osc.process() 63 | if osc.finished: 64 | playing = false 65 | 66 | method updateExtraData(self: Looper, x,y,w,h: int) = 67 | if mousebtnp(0): 68 | let (mx,my) = mouse() 69 | # open sample selection menu 70 | pushMenu(newSampleMenu(vec2f(mx,my), "samples/") do(sample: Sample): 71 | self.osc.sample = sample 72 | self.osc.reset() 73 | ) 74 | 75 | 76 | method saveExtraData(self: Looper): string = 77 | result = "" 78 | if osc.sample != nil: 79 | result = osc.sample.filename 80 | 81 | method loadExtraData(self: Looper, data: string) = 82 | if data != "": 83 | osc.sample = loadSample(data, data) 84 | 85 | proc newMachine(): Machine = 86 | var m = new(Looper) 87 | m.init() 88 | return m 89 | 90 | registerMachine("looper", newMachine, "generator") 91 | -------------------------------------------------------------------------------- /src/machines/generators/mod_lfsr.nim: -------------------------------------------------------------------------------- 1 | import common 2 | 3 | import core.lfsr 4 | 5 | 6 | {.this:self.} 7 | 8 | type 9 | LFSRMachine = ref object of Machine 10 | lfsr: LFSR 11 | freq: float32 12 | nextClick: int 13 | 14 | method init(self: LFSRMachine) = 15 | procCall init(Machine(self)) 16 | name = "lfsr" 17 | nInputs = 0 18 | nOutputs = 1 19 | stereo = false 20 | nextClick = 1 21 | 22 | lfsr.init() 23 | 24 | globalParams.add([ 25 | Parameter(kind: Float, name: "freq", min: 20.0'f, max: 24000.0'f, default: 440.0'f, onchange: proc(newValue: float32, voice: int) = 26 | self.freq = clamp(newValue, 20.0'f, 24000'f) 27 | ), 28 | ]) 29 | 30 | setDefaults() 31 | 32 | method process(self: LFSRMachine) = 33 | nextClick -= 1 34 | if nextClick <= 0: 35 | discard lfsr.process() 36 | if freq <= 1.0'f: 37 | nextClick = sampleRate.int 38 | else: 39 | nextClick = ((1.0 / freq) * sampleRate).int 40 | outputSamples[0] = lfsr.output 41 | 42 | proc newMachine(): Machine = 43 | var m = new(LFSRMachine) 44 | m.init() 45 | return m 46 | 47 | registerMachine("lfsr", newMachine, "generator") 48 | -------------------------------------------------------------------------------- /src/machines/generators/noise.nim: -------------------------------------------------------------------------------- 1 | import common 2 | 3 | import core.oscillator 4 | 5 | 6 | type 7 | NoiseMachine = ref object of Machine 8 | osc: Osc 9 | 10 | {.this:self.} 11 | 12 | method init(self: NoiseMachine) = 13 | procCall init(Machine(self)) 14 | name = "noise" 15 | nInputs = 0 16 | nOutputs = 1 17 | stereo = false 18 | osc.kind = Noise 19 | 20 | setDefaults() 21 | 22 | 23 | method process(self: NoiseMachine) {.inline.} = 24 | outputSamples[0] = osc.process() 25 | 26 | proc newNoiseMachine(): Machine = 27 | result = new(NoiseMachine) 28 | result.init() 29 | 30 | registerMachine("noise", newNoiseMachine, "generator") 31 | -------------------------------------------------------------------------------- /src/machines/generators/organ.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import common 4 | import util 5 | 6 | import core.oscillator 7 | import core.filter 8 | import core.envelope 9 | 10 | import machines.master 11 | 12 | 13 | # 9 osc Organ 14 | 15 | const nOperators = 9 16 | 17 | const tunings = [ 18 | 0.5, 19 | 1.0 + (8 / 12.0), 20 | 1.0, 21 | 2.0, 22 | 2.0 + (7 / 12.0), 23 | 3.0, 24 | 3.0 + (4 / 12.0), 25 | 3.0 + (7 / 12.0), 26 | 4.0, 27 | ] 28 | const names = [ 29 | "16'", 30 | "5 1/3'", 31 | "8'", 32 | "4'", 33 | "2 2/3'", 34 | "2'", 35 | "1 3/5'", 36 | "1 1/3'", 37 | "1'", 38 | ] 39 | 40 | type 41 | 42 | OrganVoice = ref object of Voice 43 | pitch: float32 44 | note: int 45 | oscs: array[nOperators, Osc] 46 | env: Envelope 47 | 48 | Organ = ref object of Machine 49 | amps: array[nOperators, float32] 50 | envSettings: tuple[a,d,s,r: float32] 51 | tremolo: Osc 52 | tremoloAmount: float32 53 | 54 | {.this:self.} 55 | 56 | method init(self: OrganVoice, machine: Organ) = 57 | procCall init(Voice(self), machine) 58 | 59 | for osc in mitems(oscs): 60 | osc.kind = Sin 61 | 62 | method addVoice*(self: Organ) = 63 | var voice = new(OrganVoice) 64 | voices.add(voice) 65 | voice.init(self) 66 | 67 | proc initNote(self: Organ, voiceId: int, note: int) = 68 | var voice = OrganVoice(voices[voiceId]) 69 | if note == OffNote: 70 | voice.note = note 71 | voice.env.release() 72 | else: 73 | voice.note = note 74 | voice.pitch = noteToHz(note.float32) 75 | voice.env.a = self.envSettings.a 76 | voice.env.d = self.envSettings.d 77 | voice.env.s = self.envSettings.s 78 | voice.env.r = self.envSettings.r 79 | voice.env.trigger() 80 | 81 | method trigger(self: Organ, note: int) = 82 | for i,voice in mpairs(voices): 83 | var v = OrganVoice(voice) 84 | if v.note == OffNote: 85 | initNote(i, note) 86 | let param = v.getParameter(0) 87 | param.value = note.float32 88 | return 89 | 90 | method release(self: Organ, note: int) = 91 | for i,voice in mpairs(voices): 92 | var v = OrganVoice(voice) 93 | if v.note == note: 94 | initNote(i, OffNote) 95 | let param = v.getParameter(0) 96 | param.value = OffNote.float32 97 | 98 | method init(self: Organ) = 99 | procCall init(Machine(self)) 100 | 101 | nInputs = 0 102 | nOutputs = 1 103 | stereo = false 104 | useKeyboard = true 105 | 106 | name = "organ" 107 | 108 | self.globalParams.add([ 109 | Parameter(name: "a", kind: Float, min: 0.0, max: 1.0, default: 0.01, onchange: proc(newValue: float32, voice: int) = 110 | self.envSettings.a = newValue 111 | ), 112 | Parameter(name: "d", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 113 | self.envSettings.d = newValue 114 | ), 115 | Parameter(name: "s", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 116 | self.envSettings.s = newValue 117 | ), 118 | Parameter(name: "r", kind: Float, min: 0.0, max: 1.0, default: 0.1, onchange: proc(newValue: float32, voice: int) = 119 | self.envSettings.r = newValue 120 | ), 121 | Parameter(name: "tremolo spd", kind: Float, min: 0.0, max: 60.0, default: 10.0, onchange: proc(newValue: float32, voice: int) = 122 | self.tremolo.freq = newValue 123 | ), 124 | Parameter(name: "tremolo amt", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 125 | self.tremoloAmount = newValue 126 | ), 127 | ]) 128 | 129 | 130 | for i in 0..nOperators-1: 131 | (proc() = 132 | let opId = i 133 | self.globalParams.add([ 134 | Parameter(name: names[opId], kind: Int, min: 0.0, max: 8.0, default: if opId == 2: 1.0 else: 0.0, onchange: proc(newValue: float32, voice: int) = 135 | self.amps[opId] = newValue / 8.0 136 | ), 137 | ]) 138 | )() 139 | 140 | self.voiceParams.add([ 141 | Parameter(name: "note", kind: Note, min: 0.0, max: 255.0, default: OffNote, onchange: proc(newValue: float32, voice: int) = 142 | self.initNote(voice, newValue.int) 143 | , getValueString: proc(value: float32, voice: int): string = 144 | if value == OffNote: 145 | return "Off" 146 | else: 147 | return noteToNoteName(value.int) 148 | ) 149 | ]) 150 | 151 | setDefaults() 152 | 153 | addVoice() 154 | 155 | method process(self: Organ) {.inline.} = 156 | outputSamples[0] = 0 157 | 158 | let t = tremolo.process() 159 | 160 | for voice in mitems(self.voices): 161 | var v = OrganVoice(voice) 162 | for i,osc in mpairs(v.oscs): 163 | osc.freq = v.pitch * tunings[i] 164 | outputSamples[0] += osc.process() * amps[i] * v.env.process() * lerp(1.0, t, tremoloAmount) 165 | 166 | proc newOrgan(): Machine = 167 | var organ = new(Organ) 168 | organ.init() 169 | return organ 170 | 171 | registerMachine("organ", newOrgan, "generator") 172 | -------------------------------------------------------------------------------- /src/machines/generators/osc.nim: -------------------------------------------------------------------------------- 1 | import common 2 | 3 | import core.oscillator 4 | 5 | 6 | {.this:self.} 7 | 8 | type 9 | OscMachine = ref object of Machine 10 | osc: Osc 11 | phaseOffset: float32 12 | 13 | method init(self: OscMachine) = 14 | procCall init(Machine(self)) 15 | name = "osc" 16 | nInputs = 0 17 | nOutputs = 1 18 | stereo = false 19 | 20 | globalParams.add([ 21 | Parameter(kind: Int, name: "shape", min: OscKind.low.float32, max: OscKind.high.float32, onchange: proc(newValue: float32, voice: int) = 22 | osc.kind = newValue.OscKind 23 | , getValueString: proc(value: float32, voice: int): string = 24 | return $value.OscKind 25 | ), 26 | Parameter(kind: Float, name: "freq", min: 0.0001, max: 24000.0, default: 440.0, onchange: proc(newValue: float32, voice: int) = 27 | osc.freq = newValue 28 | ), 29 | Parameter(kind: Float, name: "pw", min: 0.0001, max: 0.9999, default: 0.5, onchange: proc(newValue: float32, voice: int) = 30 | osc.pulseWidth = newValue 31 | ), 32 | Parameter(kind: Float, name: "phmod", min: 0.0, max: 10.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 33 | phaseOffset = newValue 34 | ), 35 | ]) 36 | 37 | setDefaults() 38 | 39 | method process(self: OscMachine) = 40 | osc.phase += phaseOffset 41 | outputSamples[0] = osc.process() 42 | osc.phase -= phaseOffset 43 | 44 | proc newOscMachine(): Machine = 45 | var m = new(OscMachine) 46 | m.init() 47 | return m 48 | 49 | registerMachine("osc", newOscMachine, "generator") 50 | -------------------------------------------------------------------------------- /src/machines/generators/sampler.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | 6 | import common 7 | 8 | import core/envelope 9 | import core/sample 10 | import ui/menu 11 | 12 | type 13 | Sampler = ref object of Machine 14 | 15 | type 16 | SamplerVoice = ref object of Voice 17 | note: int 18 | playing: bool 19 | osc: SampleOsc 20 | env: Envelope 21 | vel: int 22 | 23 | method addVoice*(self: Sampler) = 24 | var voice = new(SamplerVoice) 25 | self.voices.add(voice) 26 | voice.init(self) 27 | voice.env.init() 28 | voice.osc.stereo = true 29 | 30 | for param in mitems(voice.parameters): 31 | param.value = param.default 32 | param.onchange(param.value, self.voices.high) 33 | 34 | proc initNote*(self: Sampler, voiceId: int, note: int) = 35 | var voice = SamplerVoice(self.voices[voiceId]) 36 | voice.note = note 37 | if voice.note == OffNote: 38 | voice.env.release() 39 | voice.playing = false 40 | else: 41 | if voice.osc.sample != nil: 42 | voice.playing = true 43 | voice.osc.reset() 44 | voice.osc.speed = noteToHz(note.float32) / voice.osc.sample.rootPitch 45 | voice.env.trigger(voice.vel.float32 / 127.0f) 46 | 47 | method init(self: Sampler) = 48 | procCall init(Machine(self)) 49 | self.name = "sampler" 50 | self.nOutputs = 1 51 | self.nInputs = 0 52 | self.stereo = true 53 | 54 | self.voiceParams.add([ 55 | Parameter(name: "note", separator: true, deferred: true, kind: Note, min: OffNote, max: 127.0, default: OffNote, onchange: proc(newValue: float32, voice: int) = 56 | self.initNote(voice, newValue.int) 57 | ), 58 | Parameter(name: "vel", separator: false, deferred: false, kind: Int, min: 0.0, max: 127.0, default: 80.0, onchange: proc(newValue: float32, voice: int) = 59 | SamplerVoice(self.voices[voice]).vel = newValue.int 60 | ), 61 | Parameter(name: "a", kind: Float, min: 0.0, max: 5.0, default: 0.001, onchange: proc(newValue: float32, voice: int) = 62 | var v = SamplerVoice(self.voices[voice]) 63 | v.env.a = exp(newValue) - 1.0 64 | , getValueString: proc(value: float32, voice: int): string = 65 | return (exp(value) - 1.0).formatFloat(ffDecimal, 2) & " s" 66 | ), 67 | Parameter(name: "d", kind: Float, min: 0.0, max: 5.0, default: 0.1, onchange: proc(newValue: float32, voice: int) = 68 | var v = SamplerVoice(self.voices[voice]) 69 | v.env.d = exp(newValue) - 1.0 70 | , getValueString: proc(value: float32, voice: int): string = 71 | return (exp(value) - 1.0).formatFloat(ffDecimal, 2) & " s" 72 | ), 73 | Parameter(name: "dexp", kind: Float, min: 0.1, max: 10.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 74 | var v = SamplerVoice(self.voices[voice]) 75 | v.env.decayExp = newValue 76 | ), 77 | Parameter(name: "s", kind: Float, min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 78 | var v = SamplerVoice(self.voices[voice]) 79 | v.env.s = newValue 80 | ), 81 | Parameter(name: "r", kind: Float, min: 0.0, max: 5.0, default: 0.1, onchange: proc(newValue: float32, voice: int) = 82 | var v = SamplerVoice(self.voices[voice]) 83 | v.env.r = exp(newValue) - 1.0 84 | , getValueString: proc(value: float32, voice: int): string = 85 | return (exp(value) - 1.0).formatFloat(ffDecimal, 2) & " s" 86 | ), 87 | ]) 88 | 89 | self.setDefaults() 90 | 91 | method process*(self: Sampler) = 92 | self.outputSamples[0] = 0'f 93 | for i in 0..= 0 and voice < self.voices.len: 116 | # open sample selection menu 117 | pushMenu(newSampleMenu(vec2f(mx,my), "samples/") do(sample: Sample): 118 | var v = SamplerVoice(self.voices[voice]) 119 | v.osc.sample = sample 120 | ) 121 | 122 | method saveExtraData(self: Sampler): string = 123 | result = "" 124 | for voice in mitems(self.voices): 125 | var v = SamplerVoice(voice) 126 | if v.osc.sample != nil: 127 | result &= v.osc.sample.filename & "|" & v.osc.sample.name & "\n" 128 | else: 129 | result &= "\n" 130 | 131 | method loadExtraData(self: Sampler, data: string) = 132 | var voice = 0 133 | for line in data.splitLines: 134 | if voice > self.voices.high: 135 | break 136 | let sline = line.strip() 137 | if sline == "": 138 | voice += 1 139 | continue 140 | var v = SamplerVoice(self.voices[voice]) 141 | v.osc.sample = loadSample(sline.split("|")[0], sline.split("|")[1]) 142 | voice += 1 143 | 144 | method trigger*(self: Sampler, note: int) = 145 | for i,voice in mpairs(self.voices): 146 | var v = SamplerVoice(voice) 147 | if v.note == OffNote: 148 | self.initNote(i, note) 149 | let param = v.getParameter(0) 150 | param.value = note.float32 151 | return 152 | 153 | method release*(self: Sampler, note: int) = 154 | for i,voice in mpairs(self.voices): 155 | var v = SamplerVoice(voice) 156 | if v.note == note: 157 | self.initNote(i, OffNote) 158 | let param = v.getParameter(0) 159 | param.value = OffNote.float32 160 | 161 | proc newMachine(): Machine = 162 | var m = new(Sampler) 163 | m.init() 164 | return m 165 | 166 | registerMachine("sampler", newMachine, "generator") 167 | -------------------------------------------------------------------------------- /src/machines/generators/tb303.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import common 4 | import util 5 | 6 | import core.oscillator 7 | import core.filter 8 | import core.envelope 9 | 10 | import machines.master 11 | 12 | 13 | type 14 | TB303 = ref object of Machine 15 | osc: Osc 16 | filter: BiquadFilter 17 | envAmp: Envelope 18 | envFlt: Envelope 19 | 20 | note: int 21 | slideNote: int 22 | nextSlideNote: int 23 | accentNextNote: bool 24 | cutoff: float32 25 | resonance: float32 26 | envMod: float32 27 | accentAmount: float32 28 | slideAmount: float32 29 | slideTime: float32 30 | 31 | 32 | {.this:self.} 33 | 34 | proc initNote(self: TB303, note: int) = 35 | self.note = note 36 | if self.note == OffNote: 37 | self.envAmp.release() 38 | self.envFlt.release() 39 | self.slideTime = Master(masterMachine).beatsPerMinute / 60.0 40 | else: 41 | self.osc.freq = noteToHz(note.float32) 42 | self.envAmp.trigger(if self.accentNextNote: 1'f else: 0.75'f) 43 | self.envFlt.trigger(if self.accentNextNote: 1'f else: 0.75'f) 44 | self.accentNextNote = false 45 | self.slideAmount = 0.0 46 | self.slideTime = Master(masterMachine).beatsPerMinute / 60.0 47 | if self.nextSlideNote != self.note and self.nextSlideNote != OffNote: 48 | self.slideNote = self.nextSlideNote 49 | else: 50 | self.slideNote = self.note 51 | self.nextSlideNote = OffNote 52 | 53 | method init(self: TB303) = 54 | procCall init(Machine(self)) 55 | 56 | nInputs = 0 57 | nOutputs = 1 58 | stereo = false 59 | name = "303" 60 | 61 | osc.kind = Saw 62 | osc.pulseWidth = 0.5 63 | 64 | envAmp.a = 0.01 65 | envAmp.d = 0.5 66 | envAmp.decayKind = Exponential 67 | envAmp.decayExp = 50.0 68 | envAmp.s = 0.0 69 | envAmp.r = 0 70 | 71 | envFlt.a = 0.01 72 | envFlt.d = 0.5 73 | envFlt.decayKind = Exponential 74 | envFlt.decayExp = 50.0 75 | envFlt.s = 0.0 76 | envFlt.r = 0 77 | 78 | filter.kind = Lowpass 79 | filter.init() 80 | envAmp.init() 81 | envFlt.init() 82 | 83 | self.globalParams.add([ 84 | Parameter(name: "note", kind: Note, min: 0.0, max: 255.0, deferred: true, default: OffNote, onchange: proc(newValue: float32, voice: int) = 85 | self.initNote(newValue.int) 86 | ), 87 | Parameter(name: "slide", kind: Note, min: 0.0, max: 255.0, default: OffNote, onchange: proc(newValue: float32, voice: int) = 88 | self.nextSlideNote = newValue.int 89 | ), 90 | Parameter(name: "accent", kind: Bool, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 91 | self.accentNextNote = newValue.bool 92 | ), 93 | Parameter(name: "decay", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 94 | self.envFlt.d = newValue.float32 95 | ), 96 | Parameter(name: "cutoff", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 97 | self.cutoff = exp(lerp(-8.0, -0.8, newValue)) 98 | ), 99 | Parameter(name: "res", kind: Float, min: 0.01, max: 10.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 100 | self.resonance = newValue 101 | ), 102 | Parameter(name: "envMod", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 103 | self.envMod = newValue 104 | ), 105 | Parameter(name: "accentMod", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 106 | self.accentAmount = newValue.float32 107 | ), 108 | Parameter(name: "wave", kind: Int, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 109 | self.osc.kind = if newValue == 0: Saw else: Sqr 110 | ), 111 | 112 | ]) 113 | 114 | setDefaults() 115 | 116 | method process(self: TB303) = 117 | if slideNote != OffNote: 118 | slideAmount += invSampleRate 119 | slideAmount = clamp(slideAmount, 0.0, slideTime) 120 | osc.freq = lerp(noteToHz(note.float32), noteToHz(slideNote.float32), invLerp(0.0, slideTime, slideAmount)) 121 | else: 122 | osc.freq = noteToHz(note.float32) 123 | let amp = envAmp.process() 124 | filter.cutoff = cutoff + (envFlt.process() * envMod * 0.1) 125 | filter.resonance = resonance 126 | filter.calc() 127 | outputSamples[0] = filter.process(osc.process()) * amp 128 | 129 | method trigger*(self: TB303, note: int) = 130 | self.initNote(note) 131 | 132 | method release*(self: TB303, note: int) = 133 | self.initNote(OffNote) 134 | 135 | proc newTB303(): Machine = 136 | var my303 = new(TB303) 137 | my303.init() 138 | return my303 139 | 140 | registerMachine("303", newTB303, "generator") 141 | -------------------------------------------------------------------------------- /src/machines/io/audioin.nim: -------------------------------------------------------------------------------- 1 | # machine that provides audio input from the system 2 | # maybe allow to choose the inputs 3 | 4 | import common 5 | 6 | type 7 | AudioInMachine = ref object of Machine 8 | 9 | {.this:self.} 10 | 11 | method init(self: AudioInMachine) = 12 | procCall init(Machine(self)) 13 | name = "input" 14 | nInputs = 0 15 | nOutputs = 1 16 | stereo = true 17 | setDefaults() 18 | 19 | 20 | method process(self: AudioInMachine) {.inline.} = 21 | outputSamples[0] = inputSample 22 | 23 | proc newMachine(): Machine = 24 | var m = new(AudioInMachine) 25 | m.init() 26 | return m 27 | 28 | registerMachine("input", newMachine, "io") 29 | -------------------------------------------------------------------------------- /src/machines/io/filerec.nim: -------------------------------------------------------------------------------- 1 | #import nico 2 | # 3 | #import common 4 | #import util 5 | # 6 | #import sndfile 7 | #import core/basemachine 8 | # 9 | # 10 | #{.this:self.} 11 | # 12 | #type FileRec = ref object of Machine 13 | # filename: string 14 | # recording: bool 15 | # file: ptr TSNDFILE 16 | # lastSample: cfloat 17 | # 18 | # 19 | #method init(self: FileRec) = 20 | # procCall init(Machine(self)) 21 | # 22 | # nInputs = 1 23 | # nOutputs = 0 24 | # stereo = true 25 | # 26 | # name = "filerec" 27 | # 28 | # self.filename = "out.wav" 29 | # 30 | # self.globalParams.add([ 31 | # Parameter(name: "record", kind: Trigger, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float, voice: int) = 32 | # self.recording = newValue.bool 33 | # if self.recording: 34 | # # open file 35 | # var sfinfo: TINFO 36 | # sfinfo.samplerate = samplerate 37 | # sfinfo.channels = 2 38 | # sfinfo.format = (SF_FORMAT_WAV or SF_FORMAT_FLOAT) 39 | # self.file = open(self.filename.cstring, WRITE, sfinfo.addr) 40 | # if self.file != nil: 41 | # echo "file opened for writing: ", self.filename 42 | # else: 43 | # echo "error opening file for writing: ", self.filename 44 | # echo strerror(nil) 45 | # self.recording = false 46 | # else: 47 | # # close file if it was open 48 | # if self.file != nil: 49 | # self.file = nil 50 | # self.recording = false 51 | # ), 52 | # ]) 53 | # 54 | # setDefaults() 55 | # 56 | #method process(self: FileRec) {.inline.} = 57 | # let sample = getInput() 58 | # if sampleId mod 2 == 1 and self.file != nil: 59 | # var data: array[2, cfloat] = [lastSample, sample] 60 | # let ret = self.file.writef_float(data[0].addr, 1) 61 | # if ret != 1: 62 | # echo "error writing data" 63 | # self.file = nil 64 | # self.recording = false 65 | # lastSample = sample 66 | # 67 | #method drawBox(self: FileRec) = 68 | # setColor(if recording: 8 else: 2) 69 | # rectfill(getAABB()) 70 | # setColor(6) 71 | # rect(getAABB()) 72 | # printc(name, pos.x, pos.y - 2) 73 | # 74 | #proc newFileRec(): Machine = 75 | # var m = new(FileRec) 76 | # m.init() 77 | # return m 78 | # 79 | #registerMachine("filerec", newFileRec, "io") 80 | -------------------------------------------------------------------------------- /src/machines/io/keyboard.nim: -------------------------------------------------------------------------------- 1 | import nico 2 | import nico/vec 3 | 4 | import common 5 | import util 6 | 7 | import core/basemachine 8 | import core/scales 9 | 10 | 11 | const polyphony = 16 12 | 13 | type Keyboard = ref object of Machine 14 | nOctaves: int 15 | noteBuffer: array[polyphony, tuple[note: int, age: int]] 16 | size: int 17 | scale: int 18 | baseNote: int 19 | 20 | {.this:self.} 21 | 22 | method init*(self: Keyboard) = 23 | procCall init(Machine(self)) 24 | 25 | name = "keyboard" 26 | nOutputs = 0 27 | nInputs = 0 28 | nBindings = polyphony * 2 29 | bindings.setLen(nBindings) 30 | useMidi = true 31 | useKeyboard = true 32 | midiChannel = 0 33 | 34 | for i in 0.. oldestAge: 96 | oldestAge = noteBuffer[i].age 97 | oldestVoice = i 98 | 99 | noteBuffer[oldestVoice].note = note 100 | noteBuffer[oldestVoice].age = 0 101 | if bindings[oldestVoice*2].isBound: 102 | var (voice,param) = bindings[oldestVoice*2].getParameter() 103 | param.value = note.float32 104 | param.onchange(param.value, voice) 105 | 106 | if bindings[oldestVoice*2+1].isBound: 107 | var (voice,param) = bindings[oldestVoice*2+1].getParameter() 108 | param.value = vel.float32 / 127.0 109 | param.onchange(param.value, voice) 110 | 111 | proc noteOff(self: Keyboard, note: int) = 112 | for i in 0.. channelPeaksL[i]: 76 | channelPeaksL[i] = absc 77 | else: 78 | c *= cos(channelPan[i] * PI * 0.5) 79 | let absc = abs(c) 80 | if absc > channelPeaksR[i]: 81 | channelPeaksR[i] = absc 82 | outputSamples[0] += c 83 | 84 | outputSamples[0] *= gain 85 | 86 | proc newMaster(): Machine = 87 | result = new(Master) 88 | result.init() 89 | masterMachine = result 90 | 91 | proc beatsPerMinute*(): float32 = 92 | var m = Master(masterMachine) 93 | return m.beatsPerMinute 94 | 95 | proc beatsPerSecond*(): float32 = beatsPerMinute() / 60.0 96 | 97 | proc secondsPerBeat*(): float32 = 1.0 / beatsPerSecond() 98 | 99 | import nico 100 | 101 | method drawExtraData(self: Master, x,y,w,h: int) = 102 | # draw our input volumes 103 | var y = y 104 | var totalL = 0.0 105 | var totalR = 0.0 106 | for i in 0.. 0.9: 119 | setColor(7) 120 | elif ampL > 0.75: 121 | setColor(4) 122 | else: 123 | setColor(3) 124 | rectfill(x + 1, y + 8, x + 1 + ((w - 2).float32 * (clamp(ampL, 0.0, 2.0) * 0.5)).int, y + 8 + 4) 125 | if ampR > 0.9: 126 | setColor(7) 127 | elif ampR > 0.75: 128 | setColor(4) 129 | else: 130 | setColor(3) 131 | rectfill(x + 1, y + 8 + 4 + 2, x + 1 + ((w - 2).float32 * (clamp(ampR, 0.0, 2.0) * 0.5)).int, y + 8 + 4 + 2 + 4) 132 | 133 | setColor(7) 134 | vline(x + 1 + (w - 2) div 2, y + 10, y + 24) 135 | setColor(8) 136 | vline(x + 1 + (((w - 2) div 2).float32 * channelGain[i]).int, y + 8, y + 26) 137 | 138 | y += 25 139 | 140 | if totalL > totalPeakL: 141 | totalPeakL = totalL 142 | if totalR > totalPeakR: 143 | totalPeakR = totalR 144 | 145 | block: 146 | let ampL = totalL 147 | let ampR = totalR 148 | 149 | if ampL > 0.9f: 150 | setColor(7) 151 | elif ampL > 0.75f: 152 | setColor(4) 153 | else: 154 | setColor(3) 155 | rectfill(x + 1, y + 8, x + 1 + ((w - 2).float32 * (clamp(ampL, 0.0, 2.0) * 0.5)).int, y + 8 + 4) 156 | if ampR > 0.9: 157 | setColor(7) 158 | elif ampR > 0.75f: 159 | setColor(4) 160 | else: 161 | setColor(3) 162 | rectfill(x + 1, y + 8 + 4 + 2, x + 1 + ((w - 2).float32 * (clamp(ampR, 0.0, 2.0) * 0.5)).int, y + 8 + 4 + 2 + 4) 163 | 164 | setColor(7) 165 | vline(x + 1 + (w - 2) div 2, y + 10, y + 24) 166 | setColor(8) 167 | vline(x + 1 + (((w - 2) div 2).float32 * gain).int, y + 8, y + 26) 168 | 169 | totalPeakL *= 0.9f 170 | totalPeakR *= 0.9f 171 | 172 | registerMachine("master", newMaster) 173 | -------------------------------------------------------------------------------- /src/machines/math/accumulator.nim: -------------------------------------------------------------------------------- 1 | import common 2 | import math 3 | 4 | type 5 | Accumulator = ref object of Machine 6 | value: float32 7 | max: float32 8 | 9 | {.this:self.} 10 | 11 | method init(self: Accumulator) = 12 | procCall init(Machine(self)) 13 | name = "acc" 14 | nOutputs = 0 15 | nInputs = 0 16 | stereo = false 17 | 18 | nBindings = 1 19 | bindings.setLen(1) 20 | 21 | globalParams.add([ 22 | Parameter(name: "add", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 23 | self.value += newValue 24 | if self.value >= self.max: 25 | # trigger 26 | if self.bindings[0].isBound(): 27 | var (voice,param) = self.bindings[0].getParameter() 28 | param.value = 1.0 29 | param.onchange(1.0, voice) 30 | self.value = self.value mod self.max 31 | self.globalParams[2].value = self.value 32 | ), 33 | Parameter(name: "max", kind: Float, min: 1.0, max: 1000.0, default: 4.0, onchange: proc(newValue: float32, voice: int) = 34 | self.max = newValue 35 | if self.value >= self.max: 36 | self.value = self.value mod self.max 37 | self.globalParams[2].value = self.value 38 | ), 39 | Parameter(name: "value", kind: Float, min: 0.0, max: 1000.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 40 | self.value = newValue 41 | ), 42 | ]) 43 | setDefaults() 44 | 45 | proc newMachine(): Machine = 46 | var m = new(Accumulator) 47 | m.init() 48 | return m 49 | 50 | registerMachine("acc", newMachine, "math") 51 | -------------------------------------------------------------------------------- /src/machines/math/operators.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import nico 4 | 5 | import common 6 | import util 7 | 8 | import core/filter 9 | 10 | 11 | {.this:self.} 12 | 13 | type 14 | Operation = enum 15 | Add 16 | Sub 17 | Mul 18 | Div 19 | Exp 20 | Operator = ref object of Machine 21 | operation: Operation 22 | OperatorE = ref object of Machine 23 | operation: Operation 24 | v1,v2: float32 25 | 26 | method init(self: Operator) = 27 | procCall init(Machine(self)) 28 | name = "." 29 | nInputs = 2 30 | nOutputs = 1 31 | stereo = false 32 | 33 | globalParams.add([ 34 | Parameter(kind: Int, name: "op", min: Operation.low.float32, max: Operation.high.float32, default: Add.float32, onchange: proc(newValue: float32, voice: int) = 35 | self.operation = newValue.Operation 36 | , getValueString: proc(value: float32, voice: int): string = 37 | return $value.Operation 38 | ), 39 | ]) 40 | 41 | setDefaults() 42 | 43 | proc send(self: OperatorE) = 44 | if bindings[0].isBound(): 45 | var (voice,param) = bindings[0].getParameter() 46 | case self.operation: 47 | of Add: 48 | param.value = self.v1 + self.v2 49 | of Sub: 50 | param.value = self.v1 - self.v2 51 | of Mul: 52 | param.value = self.v1 * self.v2 53 | of Div: 54 | if self.v2 != 0: 55 | param.value = self.v1 / self.v2 56 | of Exp: 57 | param.value = pow(self.v1,self.v2) 58 | param.onchange(param.value, voice) 59 | 60 | method init(self: OperatorE) = 61 | procCall init(Machine(self)) 62 | name = "." 63 | nInputs = 0 64 | nOutputs = 0 65 | stereo = false 66 | 67 | bindings = newSeq[Binding](1) 68 | nBindings = 1 69 | 70 | globalParams.add([ 71 | Parameter(kind: Int, name: "op", min: Operation.low.float32, max: Operation.high.float32, default: Add.float32, onchange: proc(newValue: float32, voice: int) = 72 | self.operation = newValue.Operation 73 | , getValueString: proc(value: float32, voice: int): string = 74 | return $value.Operation 75 | ), 76 | Parameter(kind: Float, name: "v1", min: -1000.0, max: 1000.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 77 | self.v1 = newValue 78 | self.send() 79 | 80 | ), 81 | Parameter(kind: Float, name: "v2", min: -1000.0, max: 1000.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 82 | self.v2 = newValue 83 | self.send() 84 | ), 85 | ]) 86 | 87 | setDefaults() 88 | 89 | 90 | method process(self: Operator) {.inline.} = 91 | case operation: 92 | of Add: 93 | outputSamples[0] = getInput(0) + getInput(1) 94 | of Sub: 95 | outputSamples[0] = getInput(0) - getInput(1) 96 | of Mul: 97 | outputSamples[0] = getInput(0) * getInput(1) 98 | of Div: 99 | let divisor = getInput(1) 100 | if divisor == 0: 101 | outputSamples[0] = 0.0 102 | else: 103 | outputSamples[0] = getInput(0) / divisor 104 | of Exp: 105 | outputSamples[0] = pow(getInput(0), getInput(1)) 106 | 107 | method getAABB(self: Operator): AABB = 108 | result.min.x = (pos.x.int - 5).float32 109 | result.min.y = (pos.y.int - 5).float32 110 | result.max.x = (pos.x.int + 5).float32 111 | result.max.y = (pos.y.int + 5).float32 112 | 113 | method getAABB(self: OperatorE): AABB = 114 | result.min.x = (pos.x.int - 5).float32 115 | result.min.y = (pos.y.int - 5).float32 116 | result.max.x = (pos.x.int + 5).float32 117 | result.max.y = (pos.y.int + 5).float32 118 | 119 | 120 | method drawBox(self: Operator) = 121 | setColor(1) 122 | circfill(pos.x, pos.y, 4) 123 | setColor(6) 124 | 125 | printc( 126 | case operation: 127 | of Add: 128 | "+" 129 | of Sub: 130 | "-" 131 | of Mul: 132 | "*" 133 | of Div: 134 | "/" 135 | of Exp: 136 | "^" 137 | , pos.x + 1, pos.y - 2) 138 | 139 | circ(pos.x, pos.y, 4) 140 | 141 | method drawBox(self: OperatorE) = 142 | setColor(2) 143 | circfill(pos.x, pos.y, 4) 144 | setColor(6) 145 | 146 | printc( 147 | case operation: 148 | of Add: 149 | "+" 150 | of Sub: 151 | "-" 152 | of Mul: 153 | "*" 154 | of Div: 155 | "/" 156 | of Exp: 157 | "^" 158 | , pos.x + 1, pos.y - 2) 159 | 160 | circ(pos.x, pos.y, 4) 161 | 162 | proc newOperator(): Machine = 163 | var m = new(Operator) 164 | m.init() 165 | return m 166 | 167 | proc newOperatorE(): Machine = 168 | var m = new(OperatorE) 169 | m.init() 170 | return m 171 | 172 | registerMachine("op-a", newOperator, "math") 173 | registerMachine("op-e", newOperatorE, "math") 174 | -------------------------------------------------------------------------------- /src/machines/ui/button.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | import nico/vec 6 | 7 | import common 8 | import util 9 | 10 | import ui/layoutview 11 | 12 | 13 | type Button = ref object of Machine 14 | state: bool 15 | onValue: float32 16 | offValue: float32 17 | toggle: bool 18 | gamepad: int 19 | gamepadButton: int 20 | eventListener: EventListener 21 | color: int 22 | 23 | {.this:self.} 24 | 25 | proc setOn(self: Button) = 26 | state = true 27 | if bindings[0].isBound(): 28 | var (voice, param) = bindings[0].getParameter() 29 | param.value = onValue 30 | param.onchange(param.value, voice) 31 | 32 | proc setOff(self: Button) = 33 | state = false 34 | if bindings[0].isBound(): 35 | var (voice, param) = bindings[0].getParameter() 36 | param.value = offValue 37 | param.onchange(param.value, voice) 38 | 39 | method init(self: Button) = 40 | procCall init(Machine(self)) 41 | nBindings = 1 42 | bindings.setLen(1) 43 | name = "button" 44 | 45 | self.globalParams.add([ 46 | Parameter(name: "state", kind: Trigger, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 47 | self.state = newValue.bool 48 | ), 49 | Parameter(name: "on", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 50 | self.onValue = newValue 51 | ), 52 | Parameter(name: "off", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 53 | self.offValue = newValue 54 | ), 55 | Parameter(name: "toggle", kind: Int, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 56 | self.toggle = newValue.bool 57 | ), 58 | Parameter(name: "gamepad", kind: Int, min: 0'f, max: 3'f, default: 0'f, onchange: proc(newValue: float32, voice: int) = 59 | self.gamepad = newValue.int 60 | ), 61 | Parameter(name: "button", kind: Int, min: -1'f, max: NicoButton.high.float32, default: -1, onchange: proc(newValue: float32, voice: int) = 62 | self.gamepadButton = newValue.int 63 | ), 64 | Parameter(name: "color", kind: Int, min: 1f, max: 16f, default: 4f, onchange: proc(newValue: float32, voice: int) = 65 | self.color = newValue.int 66 | ), 67 | ]) 68 | 69 | self.eventListener = addEventListener(proc(e: Event): bool = 70 | if e.kind == ekButtonDown and e.button.int == self.gamepadButton and e.which.int == self.gamepad: 71 | self.setOn() 72 | return false 73 | elif e.kind == ekButtonUp and e.button.int == self.gamepadButton and e.which.int == self.gamepad: 74 | self.setOff() 75 | return false 76 | return false 77 | ) 78 | 79 | setDefaults() 80 | 81 | method cleanup(self: Button) = 82 | removeEventListener(self.eventListener) 83 | 84 | method createBinding*(self: Button, slot: int, target: Machine, paramId: int) = 85 | procCall createBinding(Machine(self), slot, target, paramId) 86 | 87 | # match input to be the same as the target param 88 | var (voice,param) = target.getParameter(paramId) 89 | var inputParam = globalParams[1].addr 90 | inputParam.kind = param.kind 91 | inputParam.min = param.min 92 | inputParam.max = param.max 93 | inputParam.getValueString = param.getValueString 94 | 95 | inputParam = globalParams[2].addr 96 | inputParam.kind = param.kind 97 | inputParam.min = param.min 98 | inputParam.max = param.max 99 | inputParam.getValueString = param.getValueString 100 | 101 | 102 | method getAABB(self: Button): AABB = 103 | result.min.x = self.pos.x - 12.0 104 | result.min.y = self.pos.y - 12.0 105 | result.max.x = self.pos.x + 12.0 106 | result.max.y = self.pos.y + 12.0 107 | 108 | proc getButtonAABB(self: Button): AABB = 109 | result.min.x = self.pos.x - 6.0 110 | result.min.y = self.pos.y - 6.0 111 | result.max.x = self.pos.x + 6.0 112 | result.max.y = self.pos.y + 6.0 113 | 114 | const gamepadButtonNames = [ 115 | "A", "B", "X", "Y", "back", "guide", "start", "L3", "R3", "L1", "R1", "UP", "DOWN", "LEFT", "RIGHT", 116 | ] 117 | 118 | const gamepadButtonColors = [ 119 | 11, 3, 8, 2, 12, 1, 10, 9, 120 | ] 121 | 122 | method drawBox(self: Button) = 123 | let x = self.pos.x.int 124 | let y = self.pos.y.int 125 | 126 | if self.gamepadButton >= 0 and self.gamepadButton < gamepadButtonColors.len div 2: 127 | setColor(0) 128 | circfill(self.pos.x, self.pos.y, 7) 129 | setColor(gamepadButtonColors[self.gamepadButton*2]) 130 | circfill(self.pos.x, self.pos.y, 5) 131 | setColor(gamepadButtonColors[self.gamepadButton*2+1]) 132 | circ(self.pos.x, self.pos.y, 5) 133 | if state: 134 | setColor(7) 135 | circ(self.pos.x, self.pos.y, 7) 136 | 137 | else: 138 | setColor(if state: 7 else: self.color) 139 | rrectfill(getButtonAABB()) 140 | setColor(1) 141 | rrect(getButtonAABB()) 142 | 143 | var binding = bindings[0].addr 144 | if binding.machine != nil: 145 | var (voice, param) = binding.machine.getParameter(binding.param) 146 | setColor(6) 147 | printc(param.name, self.pos.x, self.pos.y + 9) 148 | 149 | 150 | if self.gamepadButton >= 0 and self.gamepadButton < gamepadButtonNames.len: 151 | setColor(0) 152 | printc(gamepadButtonNames[self.gamepadButton], pos.x, pos.y) 153 | 154 | 155 | method handleClick(self: Button, mouse: Vec2f): bool = 156 | if pointInAABB(mouse, getButtonAABB()): 157 | return true 158 | return false 159 | 160 | method event(self: Button, event: Event, camera: Vec2f): (bool, bool) = 161 | if toggle and event.kind == ekMouseButtonDown: 162 | state = not state 163 | if state: 164 | self.setOn() 165 | else: 166 | self.setOff() 167 | return (true, false) 168 | 169 | if not state and event.kind == ekMouseButtonDown: 170 | setOn() 171 | return (true, true) 172 | 173 | elif state and event.kind == ekMouseButtonUp: 174 | setOff() 175 | return (false, false) 176 | 177 | return (false, true) 178 | 179 | proc newButton(): Machine = 180 | var button = new(Button) 181 | button.init() 182 | return button 183 | 184 | registerMachine("button", newButton, "ui") 185 | -------------------------------------------------------------------------------- /src/machines/ui/knob.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | import nico/vec 6 | 7 | import common 8 | import util 9 | 10 | import core/basemachine 11 | import ui/layoutview 12 | import ui/menu 13 | 14 | 15 | type Knob = ref object of Machine 16 | lastmv: Vec2f 17 | held: bool 18 | min,max: float32 19 | sensitivity: float32 20 | center: float32 21 | spring: float32 22 | midicc: int 23 | learning: bool 24 | 25 | {.this:self.} 26 | 27 | method init(self: Knob) = 28 | procCall init(Machine(self)) 29 | nBindings = 1 30 | bindings.setLen(1) 31 | name = "knob" 32 | useMidi = true 33 | midiChannel = 0 34 | 35 | self.globalParams.add([ 36 | Parameter(name: "min", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 37 | self.min = newValue 38 | ), 39 | Parameter(name: "max", kind: Float, min: 0.0, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 40 | self.max = newValue 41 | ), 42 | Parameter(name: "sensitiv", kind: Float, min: 0.0001, max: 1.0, default: 1.0, onchange: proc(newValue: float32, voice: int) = 43 | self.sensitivity = newValue 44 | ), 45 | Parameter(name: "center", kind: Float, min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 46 | self.center = newValue 47 | ), 48 | Parameter(name: "spring", kind: Float, min: 0.0, max: 10.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 49 | self.spring = newValue 50 | ), 51 | Parameter(name: "cc", kind: Int, min: 0.0, max: 120.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 52 | self.midicc = newValue.int 53 | ), 54 | ]) 55 | 56 | setDefaults() 57 | 58 | 59 | method drawBox(self: Knob) = 60 | let x = self.pos.x.int 61 | let y = self.pos.y.int 62 | 63 | setColor(4) 64 | circfill(x, y, 4) 65 | setColor(1) 66 | circ(x, y, 5) 67 | setColor(6) 68 | if bindings[0].machine != nil: 69 | var (voice,param) = bindings[0].machine.getParameter(bindings[0].param) 70 | let min = lerp(param.min,param.max,self.min) 71 | let max = lerp(param.min,param.max,self.max) 72 | let range = max - min 73 | if range != 0.0: 74 | let angle = lerp(degToRad(-180.0 - 45.0), degToRad(45.0), ((param.value - min) / range)) 75 | line(x,y, x + (cos(angle) * 4f).int, y + (sin(angle) * 4f).int) 76 | printShadowC(param.name, x, y + 8) 77 | printShadowC( 78 | if param.getValueString != nil: 79 | param.getValueString(param.value, voice) 80 | elif param.kind == Int: 81 | $param.value.int 82 | else: 83 | param.value.formatFloat(ffDecimal, 2) 84 | , x, y + 16) 85 | else: 86 | printShadowC(name, x, y + 8) 87 | 88 | method getAABB(self: Knob): AABB = 89 | result.min.x = self.pos.x - 12.0 90 | result.min.y = self.pos.y - 6.0 91 | result.max.x = self.pos.x + 12.0 92 | result.max.y = self.pos.y + 12.0 93 | 94 | proc getKnobAABB(self: Knob): AABB = 95 | result.min.x = self.pos.x - 6.0 96 | result.min.y = self.pos.y - 6.0 97 | result.max.x = self.pos.x + 6.0 98 | result.max.y = self.pos.y + 6.0 99 | 100 | method midiEvent(self: Knob, event: MidiEvent) = 101 | if event.command == 3: 102 | if learning: 103 | self.midicc = event.data1.int 104 | self.globalParams[4].value = self.midicc.float32 105 | self.learning = false 106 | echo "assigned cc: ", self.midicc 107 | elif event.data1 == midicc.uint8: 108 | if bindings[0].isBound: 109 | var (voice,param) = bindings[0].getParameter() 110 | let min = lerp(param.min,param.max,min) 111 | let max = lerp(param.min,param.max,max) 112 | param.value = lerp(min, max, event.data2.float32 / 127.0) 113 | param.onchange(param.value, voice) 114 | 115 | 116 | method handleClick(self: Knob, mouse: Vec2f): bool = 117 | if pointInAABB(mouse, getKnobAABB()): 118 | let (mx,my) = mouse() 119 | lastmv = vec2f(mx,my) 120 | return true 121 | return false 122 | 123 | method event(self: Knob, event: Event, camera: Vec2f): (bool, bool) = 124 | case event.kind: 125 | of ekMouseButtonUp: 126 | if event.button == 1: 127 | held = false 128 | return (true,false) 129 | 130 | of ekMouseButtonDown: 131 | held = true 132 | return (true,true) 133 | 134 | of ekMouseMotion: 135 | if bindings[0].machine != nil: 136 | var (voice,param) = bindings[0].machine.getParameter(bindings[0].param) 137 | let shift = shift() 138 | let ctrl = ctrl() 139 | let move = (if ctrl and shift: 0.0001 elif ctrl: 0.1 elif shift: 0.001 else: 0.01) * sensitivity 140 | 141 | let min = lerp(param.min,param.max,min) 142 | let max = lerp(param.min,param.max,max) 143 | param.value -= event.yrel * move * (max - min) 144 | param.value = clamp(param.value, min, max) 145 | param.onchange(param.value, voice) 146 | return (false,true) 147 | else: 148 | discard 149 | 150 | return (false,true) 151 | 152 | method process(self: Knob) = 153 | if bindings[0].machine != nil and not held: 154 | if self.spring > 0.0: 155 | var (voice,param) = bindings[0].machine.getParameter(bindings[0].param) 156 | let d = param.value - center 157 | let f = d * spring * invSampleRate 158 | param.value -= f 159 | param.value = clamp(param.value, min, max) 160 | param.onchange(param.value, voice) 161 | 162 | method getMenu*(self: Knob, mv: Vec2f): Menu = 163 | result = procCall getMenu(Machine(self), mv) 164 | result.items.add(newMenuItem("midi learn") do(): 165 | self.learning = true 166 | echo "learning enabled" 167 | popMenu() 168 | ) 169 | 170 | proc newKnob(): Machine = 171 | var knob = new(Knob) 172 | knob.init() 173 | return knob 174 | 175 | registerMachine("knob", newKnob, "ui") 176 | -------------------------------------------------------------------------------- /src/machines/ui/value.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | import nico/vec 6 | 7 | import common 8 | import util 9 | 10 | import core/basemachine 11 | import ui/layoutview 12 | import ui/menu 13 | 14 | 15 | type ValueMachine = ref object of Machine 16 | value: float32 17 | 18 | {.this:self.} 19 | 20 | proc setValue(self: ValueMachine, value: float32) = 21 | self.value = value 22 | 23 | if bindings[0].isBound(): 24 | var (voice,param) = bindings[0].getParameter() 25 | param.value = value 26 | param.onchange(value, voice) 27 | 28 | self.globalParams[0].value = value 29 | 30 | method init(self: ValueMachine) = 31 | procCall init(Machine(self)) 32 | nBindings = 1 33 | bindings.setLen(1) 34 | name = "value" 35 | 36 | self.globalParams.add([ 37 | Parameter(name: "value", kind: Float, min: -10000.0, max: 10000.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 38 | self.setValue(newValue) 39 | ), 40 | ]) 41 | setDefaults() 42 | 43 | method createBinding(self: ValueMachine, slot: int, target: Machine, paramId: int) = 44 | procCall createBinding(Machine(self), slot, target, paramId) 45 | # send value 46 | var (voice,param) = bindings[slot].getParameter() 47 | param.value = value 48 | param.onchange(value, voice) 49 | 50 | proc inputMenu(self: ValueMachine, mv: Vec2f): Menu = 51 | var menu = newMenu(mv, "") 52 | var te = newMenuItemText("value", $value) do(newValue: string): 53 | try: 54 | self.setValue(parseFloat(newValue)) 55 | except ValueError: 56 | discard 57 | menu.items.add(te) 58 | return menu 59 | 60 | method handleClick*(self: ValueMachine, mouse: Vec2f): bool = 61 | if pointInAABB(mouse, getAABB()): 62 | return true 63 | return false 64 | 65 | #method event*(self: ValueMachine, event: Event, camera: Vec2f): (bool, bool) = 66 | # case event.kind: 67 | # of MouseButtonDown: 68 | # if event.button.button == 1: 69 | # if event.button.clicks == 2: 70 | # pushMenu(self.inputMenu(mouse() + vec2f(-4.0, -4.0))) 71 | # return (true,false) 72 | # return (false,true) 73 | # else: 74 | # return (false,false) 75 | 76 | method getMenu*(self: ValueMachine, mv: Vec2f): Menu = 77 | result = procCall getMenu(Machine(self), mv) 78 | result.items.add(newMenuItem("set value") do(): 79 | popMenu() 80 | pushMenu(self.inputMenu(mv)) 81 | ) 82 | 83 | proc newValue(): Machine = 84 | var m = new(ValueMachine) 85 | m.init() 86 | return m 87 | 88 | registerMachine("value", newValue, "ui") 89 | -------------------------------------------------------------------------------- /src/machines/ui/xy.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | import nico/vec 6 | 7 | import common 8 | import util 9 | 10 | import ui/layoutview 11 | 12 | const size = 24'f 13 | const halfSize = 12'f 14 | const padding = 6'f 15 | 16 | type AxisType = enum 17 | atFullRange = "full" 18 | atPositive = "+" 19 | atNegative = "-" 20 | 21 | type XY = ref object of Machine 22 | gamepad: int 23 | gamepadXAxis: int 24 | gamepadYAxis: int 25 | xAxisType: AxisType 26 | yAxisType: AxisType 27 | xAxisMin: float32 28 | yAxisMin: float32 29 | xAxisMax: float32 30 | yAxisMax: float32 31 | invertXAxis: bool 32 | invertYAxis: bool 33 | eventListener: EventListener 34 | x,y: float32 35 | tx,ty: float32 36 | speed: float32 37 | 38 | {.this:self.} 39 | 40 | proc setX(self: XY, xval: float32) = 41 | let x = clamp(xval * (if self.invertXAxis: -1'f else: 1'f), -1'f, 1'f) 42 | if bindings[0].isBound(): 43 | var (voice, param) = bindings[0].getParameter() 44 | param.value = lerp(self.xAxisMin, self.xAxisMax, clamp(x * 0.5'f + 0.5'f)) 45 | param.onchange(param.value, voice) 46 | 47 | proc setY(self: XY, yval: float32) = 48 | let y = clamp(yval * (if self.invertYAxis: -1'f else: 1'f), -1'f, 1'f) 49 | if bindings[1].isBound(): 50 | var (voice, param) = bindings[1].getParameter() 51 | param.value = lerp(self.yAxisMin, self.yAxisMax, clamp(y * 0.5'f + 0.5'f)) 52 | param.onchange(param.value, voice) 53 | 54 | method init(self: XY) = 55 | procCall init(Machine(self)) 56 | nBindings = 2 57 | bindings.setLen(2) 58 | name = "xy" 59 | speed = 5.0'f 60 | 61 | self.globalParams.add([ 62 | Parameter(name: "gamepad", kind: Int, min: 0'f, max: 3'f, default: 0'f, onchange: proc(newValue: float32, voice: int) = 63 | self.gamepad = newValue.int 64 | ), 65 | Parameter(name: "xaxis", kind: Int, min: 0'f, max: NicoAxis.high.float32, default: 0'f, onchange: proc(newValue: float32, voice: int) = 66 | self.gamepadXAxis = newValue.int 67 | ), 68 | Parameter(name: "invert x", kind: Bool, min: 0'f, max: 1'f, default: 0'f, onchange: proc(newValue: float32, voice: int) = 69 | self.invertXAxis = newValue.bool 70 | ), 71 | Parameter(name: "xaxis min", kind: Float, min: -1'f, max: 1'f, default: -1'f, onchange: proc(newValue: float32, voice: int) = 72 | self.xAxisMin = newValue.float32 73 | ), 74 | Parameter(name: "xaxis max", kind: Float, min: -1'f, max: 1'f, default: 1'f, onchange: proc(newValue: float32, voice: int) = 75 | self.xAxisMax = newValue.float32 76 | ), 77 | Parameter(name: "xaxis type", kind: Int, min: 0'f, max: AxisType.high.float32, default: 0'f, onchange: proc(newValue: float32, voice: int) = 78 | self.xAxisType = newValue.AxisType 79 | ), 80 | Parameter(name: "yaxis", kind: Int, separator: true, min: 0'f, max: NicoAxis.high.float32, default: 1'f, onchange: proc(newValue: float32, voice: int) = 81 | self.gamepadYAxis = newValue.int 82 | ), 83 | Parameter(name: "invert y", kind: Bool, min: 0'f, max: 1'f, default: 0'f, onchange: proc(newValue: float32, voice: int) = 84 | self.invertYAxis = newValue.bool 85 | ), 86 | Parameter(name: "yaxis min", kind: Float, min: -1'f, max: 1'f, default: -1'f, onchange: proc(newValue: float32, voice: int) = 87 | self.yAxisMin = newValue.float32 88 | ), 89 | Parameter(name: "yaxis max", kind: Float, min: -1'f, max: 1'f, default: 1'f, onchange: proc(newValue: float32, voice: int) = 90 | self.yAxisMax = newValue.float32 91 | ), 92 | 93 | Parameter(name: "yaxis type", kind: Int, min: 0'f, max: AxisType.high.float32, default: 0'f, onchange: proc(newValue: float32, voice: int) = 94 | self.yAxisType = newValue.AxisType 95 | ), 96 | Parameter(name: "speed", kind: Float, min: 0.1'f, max: 100'f, default: 10'f, onchange: proc(newValue: float32, voice: int) = 97 | self.speed = newValue 98 | ), 99 | ]) 100 | 101 | self.eventListener = addEventListener(proc(e: Event): bool = 102 | if e.kind == ekAxisMotion and e.which.int == self.gamepad: 103 | if e.button.int == self.gamepadXAxis: 104 | self.tx = e.xrel 105 | return true 106 | if e.button.int == self.gamepadYAxis: 107 | self.ty = e.xrel 108 | return true 109 | return false 110 | ) 111 | 112 | setDefaults() 113 | 114 | method createBinding*(self: XY, slot: int, target: Machine, paramId: int) = 115 | procCall createBinding(Machine(self), slot, target, paramId) 116 | 117 | # match input to be the same as the target param 118 | if slot == 0: 119 | var (voice,param) = target.getParameter(paramId) 120 | var inputParam = globalParams[3].addr 121 | inputParam.kind = param.kind 122 | inputParam.min = param.min 123 | inputParam.max = param.max 124 | inputParam.getValueString = param.getValueString 125 | 126 | inputParam = globalParams[4].addr 127 | inputParam.kind = param.kind 128 | inputParam.min = param.min 129 | inputParam.max = param.max 130 | inputParam.getValueString = param.getValueString 131 | 132 | elif slot == 1: 133 | var (voice,param) = target.getParameter(paramId) 134 | var inputParam = globalParams[8].addr 135 | inputParam.kind = param.kind 136 | inputParam.min = param.min 137 | inputParam.max = param.max 138 | inputParam.getValueString = param.getValueString 139 | 140 | inputParam = globalParams[9].addr 141 | inputParam.kind = param.kind 142 | inputParam.min = param.min 143 | inputParam.max = param.max 144 | inputParam.getValueString = param.getValueString 145 | 146 | method cleanup(self: XY) = 147 | removeEventListener(self.eventListener) 148 | 149 | method getAABB(self: XY): AABB = 150 | result.min.x = self.pos.x - halfSize - padding 151 | result.min.y = self.pos.y - halfSize - padding 152 | result.max.x = self.pos.x + halfSize + padding 153 | result.max.y = self.pos.y + halfSize + padding 154 | 155 | proc getButtonAABB(self: XY): AABB = 156 | result.min.x = self.pos.x - halfSize 157 | result.min.y = self.pos.y - halfSize 158 | result.max.x = self.pos.x + halfSize 159 | result.max.y = self.pos.y + halfSize 160 | 161 | method drawBox(self: XY) = 162 | let x = self.pos.x.int 163 | let y = self.pos.y.int 164 | 165 | setColor(1) 166 | rrectfill(getAABB()) 167 | setColor(0) 168 | rrectfill(getButtonAABB()) 169 | setColor(12) 170 | circfill(x + self.tx * halfSize, y + self.ty * halfSize, 2) 171 | setColor(7) 172 | circfill(x + self.x * halfSize, y + self.y * halfSize, 1) 173 | 174 | setColor(if bypass: 5 elif disabled: 1 else: 6) 175 | rrect(getAABB()) 176 | printc(name, pos.x, pos.y + halfSize) 177 | 178 | method handleClick(self: XY, mouse: Vec2f): bool = 179 | if pointInAABB(mouse, getButtonAABB()): 180 | return true 181 | return false 182 | 183 | method update(self: XY, dt: float32) = 184 | self.x = lerp(self.x, self.tx, speed * dt) 185 | self.y = lerp(self.y, self.ty, speed * dt) 186 | self.setX(self.x) 187 | self.setY(self.y) 188 | 189 | method event(self: XY, event: Event, camera: Vec2f): (bool, bool) = 190 | if event.kind == ekMouseButtonDown: 191 | return (true, true) 192 | 193 | elif event.kind == ekMouseButtonUp: 194 | return (false, false) 195 | 196 | elif event.kind == ekMouseMotion: 197 | self.tx = clamp((event.x.float32 - camera.x.float32 - self.pos.x.float32) / size, -1'f, 1'f) 198 | self.ty = clamp((event.y.float32 - camera.y.float32 - self.pos.y.float32) / size, -1'f, 1'f) 199 | 200 | return (false, true) 201 | 202 | proc newXY(): Machine = 203 | var m = new(XY) 204 | m.init() 205 | return m 206 | 207 | registerMachine("xy", newXY, "ui") 208 | -------------------------------------------------------------------------------- /src/machines/util/arp.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import nico 4 | 5 | import common 6 | 7 | import machines/master 8 | 9 | import core/chords 10 | 11 | 12 | {.this:self.} 13 | 14 | type ArpMode = enum 15 | Up 16 | Down 17 | UpDown 18 | 19 | type Arp = ref object of Machine 20 | note: int 21 | chord: int 22 | speed: float32 # tpb 23 | nNotes: int # how many notes in the sequence to loop over 24 | mode: ArpMode 25 | 26 | step: float32 27 | playing: bool 28 | 29 | method init(self: Arp) = 30 | procCall init(Machine(self)) 31 | 32 | nInputs = 0 33 | nOutputs = 0 34 | nBindings = 1 35 | bindings.setLen(1) 36 | name = "arp" 37 | 38 | setDefaults() 39 | 40 | globalParams.add([ 41 | Parameter(kind: Note, name: "note", min: OffNote, max: 256.0, default: OffNote, onchange: proc(newValue: float32, voice: int) = 42 | self.note = newValue.int 43 | self.step = 0.0 44 | self.playing = if self.note == OffNote: false else: true 45 | if self.note == OffNote: 46 | # send target OffNote too 47 | if self.bindings[0].isBound(): 48 | var (voice, param) = self.bindings[0].getParameter() 49 | param.value = newValue 50 | param.onchange(newValue, voice) 51 | , getValueString: proc(value: float32, voice: int): string = 52 | return noteToNoteName(value.int) 53 | ), 54 | Parameter(kind: Int, name: "chord", min: 0.0, max: chordList.high.float32, default: 0.0, onchange: proc(newValue: float32, voice: int) = 55 | self.chord = newValue.int 56 | , getValueString: proc(value: float32, voice: int): string = 57 | return chordList[value.int][0] 58 | ), 59 | Parameter(kind: Int, name: "mode", min: mode.low.float32, max: mode.high.float32, default: Up.float32, onchange: proc(newValue: float32, voice: int) = 60 | self.mode = newValue.ArpMode 61 | , getValueString: proc(value: float32, voice: int): string = 62 | return $value.ArpMode 63 | ), 64 | Parameter(kind: Int, name: "speed", min: 1.0, max: 16.0, default: 4.0, onchange: proc(newValue: float32, voice: int) = 65 | self.speed = newValue 66 | ), 67 | Parameter(kind: Int, name: "notes", min: 1.0, max: 16.0, default: 4.0, onchange: proc(newValue: float32, voice: int) = 68 | self.nNotes = newValue.int 69 | ), 70 | ]) 71 | 72 | setDefaults() 73 | 74 | method process(self: Arp) = 75 | if playing: 76 | let lastTick = step.int 77 | step += (beatsPerSecond() * speed) * invSampleRate 78 | if step.int >= nNotes: 79 | step = step mod nNotes.float32 80 | let i = step.int 81 | if bindings[0].machine != nil: 82 | if lastTick != i: 83 | var (voice, param) = bindings[0].getParameter() 84 | let intervals = chordList[chord][1] 85 | 86 | var j = i 87 | case mode: 88 | of Up: 89 | j = i 90 | of Down: 91 | j = nNotes - i 92 | of UpDown: 93 | j = if i < nNotes div 2: i else: nNotes - i 94 | let oct = j div intervals.len 95 | param.value = (note + intervals[j mod intervals.len] + oct * 12).float32 96 | param.onchange(param.value, voice) 97 | 98 | method drawExtraData(self: Arp, x,y,w,h: int) = 99 | let intervals = chordList[chord][1] 100 | var yv = y 101 | for i in 0..nNotes-1: 102 | setColor(if i == step.int: 8 else: 6) 103 | var j = i 104 | case mode: 105 | of Up: 106 | j = i 107 | of Down: 108 | j = (nNotes - 1) - i 109 | of UpDown: 110 | j = if i < nNotes div 2: i else: nNotes - i 111 | 112 | let oct = j div intervals.len 113 | print(noteToNoteName(note + intervals[j mod intervals.len] + oct * 12), x + 1, yv) 114 | 115 | yv += 8 116 | 117 | 118 | proc newArp(): Machine = 119 | var arp = new(Arp) 120 | arp.init() 121 | return arp 122 | 123 | registerMachine("arp", newArp, "util") 124 | -------------------------------------------------------------------------------- /src/machines/util/chord.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import nico 4 | 5 | import common 6 | 7 | {.this:self.} 8 | 9 | import core/chords 10 | 11 | type ChordMachine = ref object of Machine 12 | root: int 13 | chord: int 14 | inversion: int 15 | delay: float32 16 | reverse: bool 17 | currentChord: seq[int] 18 | chordNoteIndex: int 19 | nextNoteTimer: float32 20 | 21 | proc startChord(self: ChordMachine, root: int, chord: int, inversion: int) = 22 | self.root = root 23 | self.chord = chord 24 | self.inversion = inversion 25 | 26 | if root == OffNote: 27 | currentChord = @[] 28 | for i in 0..<4: 29 | if self.bindings[i].isBound(): 30 | var (voice, param) = self.bindings[i].getParameter() 31 | param.value = OffNote 32 | param.onchange(OffNote, voice) 33 | else: 34 | currentChord = @[] 35 | for i in 0..<4: 36 | let chordIntervals = chordList[chord].intervals 37 | let chordNote = if i < chordIntervals.len: root + chordIntervals[i] else: OffNote 38 | currentChord.add(chordNote) 39 | chordNoteIndex = 0 40 | nextNoteTimer = 0 41 | for i in 0.. 0: 91 | self.nextNoteTimer -= invSampleRate 92 | 93 | if self.currentChord.len > 0: 94 | for i in 0..<4: 95 | if self.nextNoteTimer <= 0 and i == self.chordNoteIndex and self.bindings[i].isBound(): 96 | if i < self.currentChord.len: 97 | var (voice, param) = self.bindings[i].getParameter() 98 | var chordNote = if self.reverse: self.currentChord[^(self.chordNoteIndex+1)] else: self.currentChord[self.chordNoteIndex] 99 | param.value = chordNote.float32 100 | param.onchange(chordNote.float32, voice) 101 | else: 102 | var (voice, param) = self.bindings[i].getParameter() 103 | param.value = OffNote 104 | param.onchange(OffNote, voice) 105 | self.chordNoteIndex += 1 106 | self.nextNoteTimer = self.delay 107 | 108 | proc newChordMachine(): Machine = 109 | var m = new(ChordMachine) 110 | m.init() 111 | return m 112 | 113 | registerMachine("chord", newChordMachine, "util") 114 | -------------------------------------------------------------------------------- /src/machines/util/dc.nim: -------------------------------------------------------------------------------- 1 | import common 2 | 3 | import core.filter 4 | 5 | 6 | type 7 | DCRemover = ref object of Machine 8 | filterL: OnePoleFilter 9 | filterR: OnePoleFilter 10 | 11 | const cutoff = 10.0'f 12 | 13 | {.this:self.} 14 | 15 | method init(self: DCRemover) = 16 | procCall init(Machine(self)) 17 | name = "dc" 18 | nInputs = 1 19 | nOutputs = 1 20 | stereo = true 21 | filterL.init() 22 | filterR.init() 23 | 24 | filterL.kind = Highpass 25 | filterR.kind = Highpass 26 | filterL.setCutoff(cutoff) 27 | filterR.setCutoff(cutoff) 28 | 29 | filterL.calc() 30 | filterR.calc() 31 | 32 | setDefaults() 33 | 34 | 35 | method process(self: DCRemover) {.inline.} = 36 | outputSamples[0] = 0.0 37 | for input in mitems(self.inputs): 38 | outputSamples[0] += input.getSample() 39 | 40 | if sampleId mod 2 == 0: 41 | outputSamples[0] = filterL.process(outputSamples[0]) 42 | else: 43 | outputSamples[0] = filterR.process(outputSamples[0]) 44 | 45 | proc newDCRemover(): Machine = 46 | var dc = new(DCRemover) 47 | dc.init() 48 | return dc 49 | 50 | registerMachine("dc", newDCRemover, "util") 51 | -------------------------------------------------------------------------------- /src/machines/util/karp.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import nico 4 | 5 | import common 6 | 7 | import machines.master 8 | 9 | 10 | {.this:self.} 11 | 12 | type KArpMode = enum 13 | Up 14 | Down 15 | UpDown 16 | Random 17 | 18 | const maxSteps = 16 19 | 20 | type 21 | KArp = ref object of Machine 22 | speed: float32 # tpb 23 | mode: KArpMode 24 | 25 | steps: array[maxSteps, int] 26 | step: float32 27 | 28 | method init(self: KArp) = 29 | procCall init(Machine(self)) 30 | 31 | nInputs = 0 32 | nOutputs = 0 33 | nBindings = 1 34 | bindings.setLen(1) 35 | name = "Karp" 36 | 37 | setDefaults() 38 | 39 | for i in 0.. 0: 77 | var trigger = false 78 | step += (beatsPerSecond() * speed) * invSampleRate 79 | if step.int != lastStep: 80 | trigger = true 81 | step = step mod nSteps.float32 82 | let i = step.int 83 | if trigger: 84 | if bindings[0].machine != nil: 85 | var k = 0 86 | for j in 0..voices.high: 87 | if steps[j] != OffNote: 88 | if k == i: 89 | var (voice, param) = bindings[0].getParameter() 90 | param.value = steps[j].float32 91 | param.onchange(param.value, voice) 92 | break 93 | k += 1 94 | else: 95 | step = 0.0 96 | 97 | method drawExtraData(self: KArp, x,y,w,h: int) = 98 | var yv = y 99 | for i in 0..voices.high: 100 | let note = steps[i] 101 | if note != OffNote: 102 | setColor(if step.int == i: 8 else: 7) 103 | print($i & ": " & noteToNoteName(note), x + 1, yv) 104 | yv += 8 105 | 106 | proc newKArp(): Machine = 107 | var arp = new(KArp) 108 | arp.init() 109 | return arp 110 | 111 | registerMachine("Karp", newKArp, "util") 112 | -------------------------------------------------------------------------------- /src/machines/util/lfo.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import nico 5 | 6 | import common 7 | import util 8 | 9 | import core/oscillator 10 | import core/basemachine 11 | 12 | import machines/master 13 | 14 | 15 | type 16 | LFOMode = enum 17 | MinMax 18 | CenterAmp 19 | LFO = ref object of Machine 20 | osc: LFOOsc 21 | min,max: float32 22 | center,amp: float32 23 | mode: LFOMode 24 | freq: float32 25 | bpmSync: bool 26 | 27 | {.this:self.} 28 | 29 | proc setFreq(self: LFO) = 30 | if bpmSync: 31 | osc.freq = ((freq * 16.0).floor / 16.0) * beatsPerSecond() 32 | else: 33 | osc.freq = freq 34 | 35 | method onBPMChange(self: LFO, bpm: int) = 36 | setFreq() 37 | 38 | method init(self: LFO) = 39 | procCall init(Machine(self)) 40 | nOutputs = 0 41 | nInputs = 0 42 | name = "lfo" 43 | nBindings = 1 44 | bindings.setLen(1) 45 | 46 | osc.pulseWidth = 0.5 47 | 48 | globalParams.add([ 49 | Parameter(name: "freq", kind: Float, min: 0.0, max: 10.0, default: 0.1, onchange: proc(newValue: float32, voice: int) = 50 | self.freq = newValue 51 | self.setFreq() 52 | , getValueString: proc(value: float32, voice: int): string = 53 | if self.bpmSync: 54 | return getFractionStr((value * 16.0).int, 16) 55 | else: 56 | return $value.formatFloat(ffDecimal, 2) & " hZ" 57 | ), 58 | Parameter(name: "mode", kind: Int, min: 0, max: LFOMode.high.float32, default: MinMax.float32, onchange: proc(newValue: float32, voice: int) = 59 | self.mode = newValue.LFOMode 60 | , getValueString: proc(value: float32, voice: int): string = 61 | return $value.LFOMode 62 | ), 63 | Parameter(name: "shape", kind: Int, min: OscKind.low.float32, max: OscKind.high.float32, default: Sin.float32, onchange: proc(newValue: float32, voice: int) = 64 | self.osc.kind = newValue.OscKind 65 | , getValueString: proc(value: float32, voice: int): string = 66 | return $value.OscKind 67 | ), 68 | Parameter(name: "min", kind: Float, min: 0.0, max: 1.0, default: 0.1, onchange: proc(newValue: float32, voice: int) = 69 | self.min = newValue 70 | , getValueString: proc(value: float32, voice: int): string = 71 | var binding = self.bindings[0] 72 | if binding.machine != nil: 73 | var (voice, param) = binding.machine.getParameter(binding.param) 74 | return param[].valueString(lerp(param.min, param.max, value)) 75 | else: 76 | return value.formatFloat(ffDecimal, 2) 77 | ), 78 | Parameter(name: "max", kind: Float, min: 0.0, max: 1.0, default: 0.9, onchange: proc(newValue: float32, voice: int) = 79 | self.max = newValue 80 | , getValueString: proc(value: float32, voice: int): string = 81 | var binding = self.bindings[0] 82 | if binding.machine != nil: 83 | var (voice, param) = binding.machine.getParameter(binding.param) 84 | return param[].valueString(lerp(param.min, param.max, value)) 85 | else: 86 | return value.formatFloat(ffDecimal, 2) 87 | ), 88 | Parameter(name: "center", kind: Float, min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 89 | self.center = newValue 90 | , getValueString: proc(value: float32, voice: int): string = 91 | var binding = self.bindings[0] 92 | if binding.machine != nil: 93 | var (voice, param) = binding.machine.getParameter(binding.param) 94 | return param[].valueString(lerp(param.min, param.max, value)) 95 | else: 96 | return value.formatFloat(ffDecimal, 2) 97 | ), 98 | Parameter(name: "amp", kind: Float, min: 0.0, max: 1.0, default: 0.1, onchange: proc(newValue: float32, voice: int) = 99 | self.amp = newValue 100 | , getValueString: proc(value: float32, voice: int): string = 101 | return (value * 100.0).formatFloat(ffDecimal, 2) & "%" 102 | ), 103 | Parameter(name: "bpmsync", kind: Bool, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 104 | self.bpmSync = newValue.bool 105 | self.setFreq() 106 | ), 107 | Parameter(name: "phase", kind: Float, min: 0.0, max: TAU, default: 0.0, onchange: proc(newValue: float32, voice: int) = 108 | self.osc.phase = newValue 109 | ), 110 | ]) 111 | 112 | setDefaults() 113 | 114 | method process(self: LFO) {.inline.} = 115 | if freq == 0.0: 116 | return 117 | let oscVal = osc.process() 118 | 119 | for binding in bindings: 120 | if binding.machine != nil: 121 | var (voice, param) = binding.machine.getParameter(binding.param) 122 | case mode: 123 | of MinMax: 124 | param.value = lerp(param.min, param.max, lerp(min, max, invLerp(-1.0, 1.0, oscVal))) 125 | of CenterAmp: 126 | param.value = lerp(param.min, param.max, lerp(center - amp, center + amp, invLerp(-1.0, 1.0, oscVal))) 127 | param.onchange(param.value, voice) 128 | 129 | globalParams[8].value = osc.phase 130 | 131 | method getAABB*(self: LFO): AABB = 132 | result.min.x = pos.x - 16 133 | result.min.y = pos.y - 4 134 | result.max.x = pos.x + 16 135 | result.max.y = pos.y + 16 136 | 137 | method drawBox*(self: LFO) = 138 | setColor(2) 139 | rectfill(getAABB()) 140 | setColor(6) 141 | rect(getAABB()) 142 | 143 | var binding = bindings[0].addr 144 | if binding.machine != nil: 145 | var (voice, param) = binding.machine.getParameter(binding.param) 146 | printc(param.name, pos.x, pos.y - 2) 147 | else: 148 | printc(name, pos.x, pos.y - 2) 149 | 150 | setColor(0) 151 | rectfill(pos.x - 15, pos.y + 4, pos.x + 15, pos.y + 14) 152 | setColor(5) 153 | line(pos.x, pos.y + 4, pos.x, pos.y + 14) 154 | for i in -15..15: 155 | if i == 0: 156 | setColor(7) 157 | else: 158 | setColor(1) 159 | let val = osc.peek(osc.phase + ((i.float32 / 30.float32))) 160 | pset(pos.x + i, pos.y + 9 - val * 4.0) 161 | 162 | 163 | method createBinding(self: LFO, slot: int, target: Machine, paramId: int) = 164 | procCall createBinding(Machine(self), slot, target, paramId) 165 | var binding = bindings[0].addr 166 | var (voice, param) = binding.machine.getParameter(binding.param) 167 | self.name = param.name & " lfo" 168 | 169 | proc newLFO(): Machine = 170 | var lfo = new(LFO) 171 | lfo.init() 172 | return lfo 173 | 174 | method drawExtraData(self: LFO, x,y,w,h: int) = 175 | var y = y 176 | setColor(4) 177 | for binding in bindings: 178 | if binding.machine != nil: 179 | var (voice, param) = binding.machine.getParameter(binding.param) 180 | printr(binding.machine.name & ": " & param.name, x + w, y) 181 | y += 8 182 | printr(param[].valueString(param[].value), x + w, y) 183 | y += 8 184 | 185 | setColor(6) 186 | printr(osc.freq.formatFloat(ffDecimal, 2), x + w, y) 187 | 188 | registerMachine("lfo", newLFO, "util") 189 | -------------------------------------------------------------------------------- /src/machines/util/paramlp.nim: -------------------------------------------------------------------------------- 1 | ## Takes an input parameter and smoothes it and passes it on 2 | 3 | import math 4 | 5 | import common 6 | import util 7 | 8 | import core.filter 9 | 10 | 11 | {.this:self.} 12 | 13 | 14 | type ParamLP = ref object of Machine 15 | filter: OnePoleFilter 16 | targetValue: float32 17 | actualValue: float32 18 | 19 | method init(self: ParamLP) = 20 | procCall init(Machine(self)) 21 | 22 | nBindings = 1 23 | bindings.setLen(1) 24 | 25 | name = "PARAMlp" 26 | 27 | filter.kind = Lowpass 28 | filter.init() 29 | 30 | globalParams.add([ 31 | Parameter(name: "input", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 32 | self.targetValue = newValue 33 | ), 34 | Parameter(name: "smooth", kind: Float, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 35 | self.filter.setCutoff(exp(lerp(-12.0, 0.0, 1.0-newValue))) 36 | self.filter.calc() 37 | ), 38 | ]) 39 | 40 | setDefaults() 41 | 42 | method createBinding(self: ParamLP, slot: int, targetMachine: Machine, paramId: int) = 43 | procCall createBinding(Machine(self), slot, targetMachine, paramId) 44 | 45 | # match input to be the same as the target param 46 | var (voice,param) = targetMachine.getParameter(paramId) 47 | var inputParam = globalParams[0].addr 48 | inputParam.kind = param.kind 49 | inputParam.min = param.min 50 | inputParam.max = param.max 51 | inputParam.getValueString = param.getValueString 52 | 53 | 54 | method process(self: ParamLP) = 55 | # take input param and lowpass it and send it to binding 56 | self.actualValue = filter.process(self.targetValue) 57 | 58 | if bindings[0].isBound: 59 | var (voice, param) = bindings[0].getParameter() 60 | param.value = self.actualValue 61 | param.onchange(self.actualValue, voice) 62 | 63 | proc newParamLP(): Machine = 64 | var m = new(ParamLP) 65 | m.init() 66 | return m 67 | 68 | 69 | registerMachine("PARAMlp", newParamLP, "util") 70 | -------------------------------------------------------------------------------- /src/machines/util/paramrecorder.nim: -------------------------------------------------------------------------------- 1 | import common 2 | import nico 3 | import nico/vec 4 | import util 5 | import streams 6 | 7 | type ParamRecorderMode = enum 8 | Stop 9 | Playback 10 | Recording 11 | 12 | const nSlots = 16 13 | 14 | type ParamRecorder = ref object of Machine 15 | buffers: array[nSlots,seq[float32]] 16 | mode: ParamRecorderMode 17 | rate: int 18 | writeHead: int 19 | readHead: int 20 | loop: bool 21 | slot: int 22 | slotNext: int 23 | slotLength: int 24 | nextSample: int 25 | 26 | {.this:self.} 27 | 28 | proc setMode(self: ParamRecorder, mode: ParamRecorderMode) = 29 | self.mode = mode 30 | case mode: 31 | of Stop: 32 | self.readHead = 0 33 | self.writeHead = 0 34 | of Playback: 35 | self.readHead = 0 36 | self.writeHead = 0 37 | self.slot = self.slotNext 38 | self.nextSample = 0 39 | if self.buffers[self.slot].len == 0: 40 | self.mode = Stop 41 | of Recording: 42 | self.readHead = 0 43 | self.writeHead = 0 44 | self.slot = self.slotNext 45 | self.nextSample = 0 46 | let length = (self.slotLength * sampleRate.int) div self.rate 47 | echo "new length: ", length 48 | self.buffers[self.slot].setLen(length) 49 | 50 | var (voice,param) = self.getParameter(2) 51 | param.value = self.mode.float32 52 | 53 | 54 | method init(self: ParamRecorder) = 55 | procCall init(Machine(self)) 56 | 57 | nBindings = 1 58 | nInputs = 0 59 | nOutputs = 0 60 | bindings.setLen(1) 61 | name = "prec" 62 | 63 | self.globalParams.add([ 64 | Parameter(name: "slot", kind: Int, min: 0.0, max: (nSlots-1).float32, default: 0.0, onchange: proc(newValue: float32, voice: int) = 65 | self.slotNext = newValue.int 66 | ), 67 | Parameter(name: "length", kind: Float, min: 0.0, max: 60.0, default: 10.0, onchange: proc(newValue: float32, voice: int) = 68 | self.slotLength = newValue.int 69 | ), 70 | Parameter(name: "mode", kind: Int, min: 0.0, max: 2.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 71 | self.setMode(newValue.int.ParamRecorderMode) 72 | ), 73 | Parameter(name: "loop", kind: Int, min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 74 | self.loop = newValue.bool 75 | ), 76 | Parameter(name: "rate", kind: Int, min: 1.0, max: 10000.0, default: 60.0, onchange: proc(newValue: float32, voice: int) = 77 | self.rate = newValue.int 78 | self.nextSample = self.rate 79 | ), 80 | ]) 81 | 82 | setDefaults() 83 | 84 | method getAABB*(self: ParamRecorder): AABB = 85 | result.min.x = pos.x - 17 86 | result.min.y = pos.y - 6 87 | result.max.x = pos.x + 17 88 | result.max.y = pos.y + 10 89 | 90 | method getRecordAABB*(self: ParamRecorder): AABB = 91 | result.min.x = pos.x - 11 - 3 92 | result.min.y = pos.y + 5 - 3 93 | result.max.x = pos.x - 11 + 3 94 | result.max.y = pos.y + 5 + 3 95 | 96 | method getPlayAABB*(self: ParamRecorder): AABB = 97 | result.min.x = pos.x + 11 - 3 98 | result.min.y = pos.y + 5 - 3 99 | result.max.x = pos.x + 11 + 3 100 | result.max.y = pos.y + 5 + 3 101 | 102 | method drawBox(self: ParamRecorder) = 103 | let x = self.pos.x.int 104 | let y = self.pos.y.int 105 | 106 | setColor(1) 107 | rectfill(getAABB()) 108 | setColor(6) 109 | rect(getAABB()) 110 | 111 | if bindings[0].machine != nil: 112 | var (voice,param) = bindings[0].machine.getParameter(bindings[0].param) 113 | printShadowC(param.name, x, y - 4) 114 | 115 | setColor(if mode == Recording: 8 else: 0) 116 | circfill(x - 11, y + 5, 2) 117 | 118 | setColor(if mode == Playback: 11 else: 0) 119 | circfill(x + 11, y + 5, 2) 120 | 121 | setColor(0) 122 | rectfill(x - 5, y + 1, x + 5, y + 7) 123 | 124 | block: 125 | setColor(if mode == Playback: 11 else: 3) 126 | let x = x - 5 + ((readHead.float32 / buffers[slot].len.float32) * 10.0).float32 127 | line(x, y + 1, x, y + 7) 128 | 129 | if mode == Recording: 130 | setColor(8) 131 | let x = x - 5 + ((writeHead.float32 / buffers[slot].len.float32) * 10.0).float32 132 | line(x, y + 1, x, y + 7) 133 | 134 | 135 | method handleClick(self: ParamRecorder, mouse: Vec2f): bool = 136 | if pointInAABB(mouse, getRecordAABB()): 137 | if mode == Recording: 138 | self.setMode(Stop) 139 | else: 140 | self.setMode(Recording) 141 | elif pointInAABB(mouse, getPlayAABB()): 142 | if mode == Playback: 143 | self.setMode(Stop) 144 | else: 145 | self.setMode(Playback) 146 | return false 147 | 148 | 149 | method process(self: ParamRecorder) = 150 | if self.mode == Stop: 151 | return 152 | 153 | nextSample -= 1 154 | if nextSample <= 0: 155 | nextSample = rate 156 | 157 | if bindings[0].machine != nil: 158 | var (voice,param) = bindings[0].machine.getParameter(bindings[0].param) 159 | case self.mode: 160 | of Recording: 161 | buffers[slot][writeHead mod buffers[slot].len] = param.value.float32 162 | writeHead += 1 163 | if writeHead > buffers[slot].high: 164 | writeHead = 0 165 | setMode(Stop) 166 | of Playback: 167 | param.value = buffers[slot][readHead mod buffers[slot].len].float32 168 | param.onchange(param.value, voice) 169 | readHead += 1 170 | if readHead > buffers[slot].high: 171 | readHead = 0 172 | if not loop: 173 | setMode(Stop) 174 | of Stop: 175 | discard 176 | 177 | method saveExtraData(self: ParamRecorder): string = 178 | var ss = newStringStream("") 179 | for s in 0.. 0: 190 | var ss = newStringStream(data) 191 | try: 192 | for s in 0.. 0: 205 | setColor(7) 206 | else: 207 | setColor(5) 208 | print("slot " & $s, x + 2, yv) 209 | yv += 16 210 | 211 | proc newParamRecorder(): Machine = 212 | var m = new(ParamRecorder) 213 | m.init() 214 | return m 215 | 216 | registerMachine("paramrec", newParamRecorder, "util") 217 | -------------------------------------------------------------------------------- /src/machines/util/probgate.nim: -------------------------------------------------------------------------------- 1 | ## Takes an input and potentially passes it on based on probability 2 | 3 | import common 4 | import random 5 | 6 | 7 | {.this:self.} 8 | 9 | type 10 | ProbGate = ref object of Machine 11 | probability: float32 12 | 13 | method init(self: ProbGate) = 14 | procCall init(Machine(self)) 15 | nInputs = 0 16 | nOutputs = 0 17 | nBindings = 1 18 | name = "probgate" 19 | bindings.setLen(1) 20 | 21 | globalParams.add([ 22 | Parameter(kind: Float, name: "%", min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 23 | self.probability = newValue 24 | ), 25 | Parameter(kind: Float, name: "input", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 26 | if rand(1.0) <= self.probability: 27 | if self.bindings[0].isBound(): 28 | var (voice,param) = self.bindings[0].getParameter() 29 | param.value = newValue 30 | param.onchange(newValue, voice) 31 | ), 32 | ]) 33 | 34 | setDefaults() 35 | 36 | method createBinding*(self: ProbGate, slot: int, target: Machine, paramId: int) = 37 | procCall createBinding(Machine(self), slot, target, paramId) 38 | 39 | # match input to be the same as the target param 40 | var (voice,param) = target.getParameter(paramId) 41 | var inputParam = globalParams[1].addr 42 | inputParam.kind = param.kind 43 | inputParam.min = param.min 44 | inputParam.max = param.max 45 | inputParam.getValueString = param.getValueString 46 | 47 | proc newProbGate(): Machine = 48 | var pg = new(ProbGate) 49 | pg.init() 50 | return pg 51 | 52 | 53 | registerMachine("%gate", newProbGate, "util") 54 | -------------------------------------------------------------------------------- /src/machines/util/probpath.nim: -------------------------------------------------------------------------------- 1 | ## When triggered sends a signal to a weighted random binding 2 | 3 | import common 4 | import random 5 | 6 | 7 | {.this:self.} 8 | 9 | type 10 | ProbPathVoice = ref object of Voice 11 | value: float32 12 | weight: float32 13 | ProbPath = ref object of Machine 14 | 15 | method init(self: ProbPathVoice, machine: Machine) = 16 | procCall init(Voice(self), machine) 17 | 18 | method addVoice(self: ProbPath) = 19 | var voice = new(ProbPathVoice) 20 | voice.init(self) 21 | voices.add(voice) 22 | 23 | method init(self: ProbPath) = 24 | procCall init(Machine(self)) 25 | nInputs = 0 26 | nOutputs = 0 27 | nBindings = 1 28 | name = "probpath" 29 | bindings.setLen(1) 30 | 31 | globalParams.add([ 32 | Parameter(kind: Trigger, name: "trigger", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 33 | # sum up all the voices 34 | var weight = 0.0 35 | if self.bindings[0].isBound(): 36 | for voice in self.voices: 37 | var v = ProbPathVoice(voice) 38 | weight += v.weight 39 | let r = rand(weight) 40 | var vr = 0.0 41 | for voice in self.voices: 42 | var v = ProbPathVoice(voice) 43 | vr += v.weight 44 | if vr >= r: 45 | var (voice,param) = self.bindings[0].getParameter() 46 | param.value = v.value 47 | param.onchange(v.value, voice) 48 | break 49 | ), 50 | ]) 51 | voiceParams.add([ 52 | Parameter(kind: Float, name: "output", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 53 | var v = ProbPathVoice(self.voices[voice]) 54 | v.value = newValue 55 | ), 56 | Parameter(kind: Float, name: "weight", min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 57 | var v = ProbPathVoice(self.voices[voice]) 58 | v.weight = newValue 59 | ), 60 | ]) 61 | 62 | setDefaults() 63 | 64 | method createBinding*(self: ProbPath, slot: int, target: Machine, paramId: int) = 65 | procCall createBinding(Machine(self), slot, target, paramId) 66 | 67 | # match input to be the same as the target param 68 | var (voice,param) = target.getParameter(paramId) 69 | var inputParam = globalParams[1].addr 70 | inputParam.kind = param.kind 71 | inputParam.min = param.min 72 | inputParam.max = param.max 73 | inputParam.default = param.default 74 | inputParam.getValueString = param.getValueString 75 | 76 | inputParam = voiceParams[0].addr 77 | inputParam.kind = param.kind 78 | inputParam.min = param.min 79 | inputParam.max = param.max 80 | inputParam.default = param.default 81 | inputParam.getValueString = param.getValueString 82 | 83 | for voice in mitems(voices): 84 | var v = ProbPathVoice(voice) 85 | v.parameters[0].kind = param.kind 86 | v.parameters[0].min = param.min 87 | v.parameters[0].max = param.max 88 | v.parameters[0].default = param.default 89 | v.parameters[0].getValueString = param.getValueString 90 | 91 | proc newProbPath(): Machine = 92 | var pp = new(ProbPath) 93 | pp.init() 94 | return pp 95 | 96 | 97 | registerMachine("probpath", newProbPath, "util") 98 | -------------------------------------------------------------------------------- /src/machines/util/probpick.nim: -------------------------------------------------------------------------------- 1 | ## When triggered sends a weighted random signal 2 | 3 | import common 4 | import random 5 | 6 | 7 | {.this:self.} 8 | 9 | type 10 | ProbPickVoice = ref object of Voice 11 | value: float32 12 | weight: float32 13 | ProbPick = ref object of Machine 14 | 15 | method init(self: ProbPickVoice, machine: Machine) = 16 | procCall init(Voice(self), machine) 17 | 18 | method addVoice(self: ProbPick) = 19 | var voice = new(ProbPickVoice) 20 | voices.add(voice) 21 | voice.init(self) 22 | 23 | method init(self: ProbPick) = 24 | procCall init(Machine(self)) 25 | nInputs = 0 26 | nOutputs = 0 27 | nBindings = 1 28 | name = "probpick" 29 | bindings.setLen(1) 30 | 31 | globalParams.add([ 32 | Parameter(kind: Trigger, name: "trigger", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 33 | # sum up all the voices 34 | var weight = 0.0 35 | if self.bindings[0].isBound(): 36 | for voice in self.voices: 37 | var v = ProbPickVoice(voice) 38 | weight += v.weight 39 | let r = rand(weight) 40 | var vr = 0.0 41 | for voice in self.voices: 42 | var v = ProbPickVoice(voice) 43 | vr += v.weight 44 | if vr >= r: 45 | var (voice,param) = self.bindings[0].getParameter() 46 | param.value = v.value 47 | param.onchange(v.value, voice) 48 | break 49 | ), 50 | ]) 51 | voiceParams.add([ 52 | Parameter(kind: Float, name: "output", min: 0.0, max: 1.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 53 | var v = ProbPickVoice(self.voices[voice]) 54 | v.value = newValue 55 | ), 56 | Parameter(kind: Float, name: "weight", min: 0.0, max: 1.0, default: 0.5, onchange: proc(newValue: float32, voice: int) = 57 | var v = ProbPickVoice(self.voices[voice]) 58 | v.weight = newValue 59 | ), 60 | ]) 61 | 62 | setDefaults() 63 | 64 | method createBinding*(self: ProbPick, slot: int, target: Machine, paramId: int) = 65 | procCall createBinding(Machine(self), slot, target, paramId) 66 | 67 | # match input to be the same as the target param 68 | var (voice,param) = target.getParameter(paramId) 69 | 70 | var inputParam = voiceParams[0].addr 71 | inputParam.kind = param.kind 72 | inputParam.min = param.min 73 | inputParam.max = param.max 74 | inputParam.default = param.default 75 | inputParam.getValueString = param.getValueString 76 | 77 | for voice in mitems(voices): 78 | var v = ProbPickVoice(voice) 79 | v.parameters[0].kind = param.kind 80 | v.parameters[0].min = param.min 81 | v.parameters[0].max = param.max 82 | v.parameters[0].default = param.default 83 | v.parameters[0].getValueString = param.getValueString 84 | 85 | proc newProbPick(): Machine = 86 | var pp = new(ProbPick) 87 | pp.init() 88 | return pp 89 | 90 | 91 | registerMachine("%pick", newProbPick, "util") 92 | -------------------------------------------------------------------------------- /src/machines/util/scale.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import strutils 3 | 4 | import random 5 | 6 | import nico 7 | import nico/vec 8 | 9 | import common 10 | import util 11 | 12 | import core/scales 13 | 14 | import core/basemachine 15 | import ui/machineview 16 | import machines/master 17 | 18 | type 19 | ScaleMachine = ref object of Machine 20 | scale: int 21 | baseNote: int 22 | 23 | method init(self: ScaleMachine) = 24 | procCall init(Machine(self)) 25 | 26 | self.name = "scale" 27 | self.nOutputs = 0 28 | self.nInputs = 0 29 | self.nBindings = 1 30 | self.bindings.setLen(1) 31 | 32 | self.globalParams.add([ 33 | Parameter(kind: Note, name: "base", min: 0.0, max: 255.0, default: 48.0, onchange: proc(newValue: float32, voice: int) = 34 | self.baseNote = newValue.int 35 | ), 36 | Parameter(kind: Int, name: "scale", min: 0.0, max: scaleList.high.float32, default: 0.0, onchange: proc(newValue: float32, voice: int) = 37 | self.scale = newValue.int 38 | , getValueString: proc(value: float32, voice: int): string = 39 | return scaleList[value.int].name 40 | ), 41 | Parameter(kind: Int, name: "input", min: 0.0, max: 255.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 42 | if self.bindings[0].isBound(): 43 | var (voice,param) = self.bindings[0].getParameter() 44 | let scale = scaleList[self.scale] 45 | let n = newValue.int 46 | let oct = n div scale.notes.len 47 | param.value = (self.baseNote + 12 * oct + scale.notes[n mod scale.notes.len]).float32 48 | param.onchange(param.value, voice) 49 | 50 | ), 51 | ]) 52 | 53 | self.setDefaults() 54 | 55 | proc newMachine(): Machine = 56 | var m = new(ScaleMachine) 57 | m.init() 58 | return m 59 | 60 | registerMachine("scale", newMachine, "util") 61 | -------------------------------------------------------------------------------- /src/machines/util/spectrogram.nim: -------------------------------------------------------------------------------- 1 | import core/fft 2 | import common 3 | import core/basemachine 4 | import nico 5 | import util 6 | import core/ringbuffer 7 | import strutils 8 | import math 9 | 10 | type 11 | SpectrogramMachine = ref object of Machine 12 | buffer: Ringbuffer[float32] 13 | data: seq[float32] 14 | resolution: int 15 | responseGraph: seq[float32] 16 | logView: bool 17 | 18 | {.this:self.} 19 | 20 | proc graphResponse(self: SpectrogramMachine) = 21 | for i in 0.. highestPeakValue: 78 | highestPeak = i 79 | highestPeakValue = responseGraph[i] 80 | 81 | setColor(1) 82 | vline(pos.x - 32 + (highestPeak.float32 / (resolution div 2).float32) * 64.0, pos.y - 31, pos.y + 31) 83 | 84 | let hz = sampleRateFractionToHz(highestPeak.float32 / resolution.float32) 85 | 86 | printr("$1 hZ".format(hz.int), pos.x + 30, pos.y - 30) 87 | printr("$1".format(hzToNoteName(hz)), pos.x + 30, pos.y - 20) 88 | 89 | var sample0: float32 90 | var sample1: float32 91 | 92 | for i in 1..<64: 93 | setColor(3) 94 | 95 | if logView: 96 | sample0 = (i-1).float32 97 | sample1 = i.float32 98 | else: 99 | sample0 = (i-1).float32 100 | sample1 = i.float32 101 | 102 | let scale = if logView: responseGraph.len.float32 / 10.0'f else: responseGraph.len.float32 / 2.0'f 103 | 104 | let v0 = abs(responseGraph.getSubsample((sample0 / 64.0'f) * scale)) 105 | let v1 = abs(responseGraph.getSubsample((sample1 / 64.0'f) * scale)) 106 | line( 107 | pos.x + i - 1 - 32, 108 | pos.y + 31 - clamp(v0 * 2.0, 0, 62.0), 109 | pos.x + i - 32, 110 | pos.y + 31 - clamp(v1 * 2.0, 0, 62.0) 111 | ) 112 | 113 | 114 | proc newMachine(): Machine = 115 | var m = new(SpectrogramMachine) 116 | m.init() 117 | return m 118 | 119 | registerMachine("spectrogram", newMachine, "util") 120 | -------------------------------------------------------------------------------- /src/machines/util/split.nim: -------------------------------------------------------------------------------- 1 | import common 2 | 3 | {.this:self.} 4 | 5 | type 6 | SplitMachine = ref object of Machine 7 | 8 | # takes an event input and duplicates it 9 | 10 | method init(self: SplitMachine) = 11 | procCall init(Machine(self)) 12 | name = "split" 13 | nInputs = 0 14 | nOutputs = 0 15 | stereo = false 16 | 17 | nBindings = 8 18 | bindings = newSeq[Binding](8) 19 | 20 | globalParams.add([ 21 | Parameter(name: "value", kind: Float, min: -1000.0, max: 1000.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 22 | for i in 0..7: 23 | if bindings[i].isBound(): 24 | var (voice,param) = bindings[i].getParameter() 25 | param.value = newValue 26 | param.onchange(newValue, voice) 27 | ), 28 | ]) 29 | 30 | setDefaults() 31 | 32 | method createBinding(self: SplitMachine, slot: int, target: Machine, paramId: int) = 33 | procCall createBinding(Machine(self), slot, target, paramId) 34 | var binding = bindings[0].addr 35 | var (voice, param) = binding.machine.getParameter(binding.param) 36 | globalParams[0].kind = param.kind 37 | 38 | proc newSplitMachine(): Machine = 39 | var m = new(SplitMachine) 40 | m.init() 41 | return m 42 | 43 | registerMachine("split", newSplitMachine, "util") 44 | -------------------------------------------------------------------------------- /src/machines/util/transposer.nim: -------------------------------------------------------------------------------- 1 | ## Takes an input note parameter and transposes it 2 | 3 | {.this:self.} 4 | 5 | import math 6 | 7 | import common 8 | import util 9 | 10 | import core.filter 11 | 12 | 13 | type Transposer = ref object of Machine 14 | octaves: int 15 | semitones: int 16 | 17 | method init(self: Transposer) = 18 | procCall init(Machine(self)) 19 | 20 | nBindings = 1 21 | bindings.setLen(1) 22 | 23 | name = "transp" 24 | 25 | globalParams.add([ 26 | Parameter(name: "input", kind: Note, deferred: true, min: OffNote, max: 255.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 27 | if self.bindings[0].isBound(): 28 | var (voice,param) = self.bindings[0].getParameter() 29 | if newValue == OffNote: 30 | param.value = OffNote 31 | else: 32 | param.value = newValue + (self.octaves * 12 + self.semitones).float32 33 | param.onchange(param.value, voice) 34 | ), 35 | Parameter(name: "oct", kind: Int, min: -4.0, max: 4.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 36 | self.octaves = newValue.int 37 | ), 38 | Parameter(name: "semi", kind: Int, min: -12.0, max: 12.0, default: 0.0, onchange: proc(newValue: float32, voice: int) = 39 | self.semitones = newValue.int 40 | ), 41 | ]) 42 | 43 | setDefaults() 44 | 45 | method createBinding(self: Transposer, slot: int, targetMachine: Machine, paramId: int) = 46 | procCall createBinding(Machine(self), slot, targetMachine, paramId) 47 | 48 | # match input to be the same as the target param 49 | var (voice,param) = targetMachine.getParameter(paramId) 50 | var inputParam = globalParams[0].addr 51 | inputParam.kind = param.kind 52 | inputParam.min = param.min 53 | inputParam.max = param.max 54 | inputParam.getValueString = param.getValueString 55 | 56 | 57 | proc newTransposer(): Machine = 58 | var m = new(Transposer) 59 | m.init() 60 | return m 61 | 62 | registerMachine("transp", newTransposer, "util") 63 | -------------------------------------------------------------------------------- /src/midi.nim: -------------------------------------------------------------------------------- 1 | import winim 2 | import winim/extra 3 | import tables 4 | 5 | type MidiCommand* = enum 6 | mcNoteOff = 0x8 7 | mcNoteOn = 0x9 8 | mcNotePressure = 0xA 9 | mcControlChange = 0xB 10 | mcProgramChange = 0xC 11 | mcChannelPressure = 0xD 12 | mcPitchBend = 0xE 13 | 14 | type MidiMessage* = object 15 | case command: MidiCommand 16 | of mcNoteOn,mcNoteOff,mcNotePressure: 17 | note: uint8 18 | of mcNoteOn,mcNoteOf: 19 | velocity: uint8 20 | of mcNotePressure: 21 | pressure: uint8 22 | of mcControlChange: 23 | control: uint8 24 | value: uint8 25 | of mcProgramChange: 26 | program: uint8 27 | of mcPitchBend: 28 | bend: uint16 29 | channel: uint8 30 | 31 | type MidiInDevice = int 32 | type MidiInCallback = proc(msg: MidiMessage) 33 | 34 | type MIDIException = object of Exception 35 | 36 | var midiDeviceToCallback = initTable[MidiInDevice, MidiInCallback]() 37 | 38 | proc midiInGetDevices(): seq[string] = 39 | let nDevs = midiInGetNumDevs() 40 | var caps: MIDIINCAPS 41 | for i in 0.. 0: 44 | discard menuStack.pop() 45 | 46 | proc hasMenu*(): bool = 47 | return menuStack.len > 0 48 | 49 | proc getMenu*(): Menu = 50 | return menuStack[menuStack.high] 51 | 52 | proc newMenu*(pos: Vec2f, label: string = ""): Menu = 53 | result = new(Menu) 54 | result.pos = pos 55 | result.label = label 56 | result.items = newSeq[MenuItem]() 57 | result.selected = -1 58 | result.textInputItem = nil 59 | result.scroll = 0 60 | 61 | proc newMenuItem*(label: string, action: proc() = nil, status: MenuItemStatus = Default): MenuItem = 62 | result = new(MenuItem) 63 | result.label = label 64 | result.action = action 65 | result.status = status 66 | 67 | proc newMenuItemText*(label: string, default: string = "", onchange: proc(newValue: string) = nil): MenuItemText = 68 | var mi = new(MenuItemText) 69 | mi.label = label 70 | mi.default = default 71 | mi.value = default 72 | mi.onchange = onchange 73 | return mi 74 | 75 | proc inputText(self: MenuItemText, text: string): bool = 76 | value &= text 77 | if onchange != nil: 78 | onchange(value) 79 | return true 80 | 81 | proc getAABB*(self: Menu): AABB = 82 | result.min.x = pos.x - 2 83 | result.min.y = pos.y - 2 84 | 85 | result.max.y = min(pos.y + items.len.float * 9.0 + 10.0, screenHeight.float - 8.0) 86 | 87 | let rows = max((result.max.y - result.min.y).int div 9, 1) 88 | result.max.y = result.min.y + rows.float * 9.0 + 2.0 89 | let cols = 1 + items.len div rows 90 | result.max.x = result.min.x + cols.float * 64.0 + 4 91 | 92 | self.rows = rows 93 | self.cols = cols 94 | self.colWidth = 64 95 | 96 | method draw*(self: MenuItem, x,y,w: int, selected: bool): int {.base.} = 97 | setColor( 98 | case self.status: 99 | of Default: 1 100 | of Primary: 3 101 | of Warning: 4 102 | of Danger: 2 103 | of Disabled: 0 104 | ) 105 | rectfill(x, y-1, x+w, y + 5) 106 | if selected: 107 | setColor( 108 | case self.status: 109 | of Default: 13 110 | of Primary: 11 111 | of Warning: 9 112 | of Danger: 8 113 | of Disabled: 0 114 | ) 115 | 116 | rectfill(x, y-1, x+w, y + 5) 117 | setColor(if selected: 7 else: 6) 118 | print(label, x + 2, y) 119 | return 9 120 | 121 | method draw*(self: MenuItemText, x,y,w: int, selected: bool): int = 122 | if selected: 123 | setColor(13) 124 | rectfill(x, y, x+w, y + 5) 125 | setColor(if selected: 7 else: 6) 126 | print(label & ": " & value, x + 2, y) 127 | return 9 128 | 129 | proc draw*(self: Menu) = 130 | let (cx,cy) = getCamera() 131 | let aabb = self.getAABB() 132 | 133 | let w = (aabb.max.x - aabb.min.x).int 134 | let h = (aabb.max.y - aabb.min.y).int 135 | 136 | # clamp to screen edges 137 | if aabb.max.x > screenWidth + cx: 138 | pos.x = (screenWidth + cy - w).float32 139 | if aabb.max.y > screenHeight + cx: 140 | pos.y = clamp((screenHeight + cy - h).float32, 0.0, screenHeight.float32 - 8.0) 141 | 142 | let x = pos.x.int 143 | let y = pos.y.int 144 | 145 | setColor(1) 146 | rrectfill(aabb) 147 | var yv = y + 2 148 | var xv = x 149 | if label != "": 150 | setColor(13) 151 | print(label, x + 2, yv) 152 | yv += 9 153 | 154 | let starty = yv 155 | for i in scroll..= aabb.max.y: 159 | yv = starty 160 | xv += 64 161 | 162 | setColor(6) 163 | rrect(aabb) 164 | 165 | proc getItemAtPos(self: Menu, mv: Vec2f): int = 166 | let aabb = self.getAABB() 167 | let rows = (aabb.max.y - aabb.min.y) div 9 - (if label != "": 1 else: 0) 168 | let column = (mv.x - pos.x).int div 64 169 | let row = (mv.y - pos.y).int div 9 - (if label != "": 1 else: 0) 170 | if row < 0: 171 | return -1 172 | if row >= rows: 173 | return -1 174 | let item = row + (column * rows) + scroll 175 | if item >= 0 and item < items.len: 176 | return item 177 | return -1 178 | 179 | proc event*(self: Menu, event: Event): bool = 180 | case event.kind: 181 | of ekMouseWheel: 182 | self.scroll = clamp(self.scroll - event.ywheel, 0, items.high) 183 | let mv = mouseVec() 184 | let aabb = self.getAABB() 185 | if pointInAABB(mv, aabb): 186 | # determine item under cursor 187 | let item = self.getItemAtPos(mv) 188 | if item >= 0: 189 | selected = item 190 | return true 191 | of ekMouseMotion: 192 | let mv = mouseVec() 193 | let aabb = self.getAABB() 194 | if pointInAABB(mv, aabb): 195 | # determine item under cursor 196 | let item = self.getItemAtPos(mv) 197 | if item >= 0: 198 | selected = item 199 | return false 200 | 201 | of ekMouseButtonDown: 202 | let mv = mouseVec() 203 | if pointInAABB(mv, self.getAABB()): 204 | if event.button == 1 and selected >= 0: 205 | if ctrl() and items[selected].altAction != nil: 206 | items[selected].altAction() 207 | if ctrl() == false and items[selected].action != nil: 208 | items[selected].action() 209 | return true 210 | 211 | elif event.button == 1: 212 | if back != nil: 213 | back() 214 | else: 215 | popMenu() 216 | return true 217 | 218 | of ekTextInput: 219 | if textInputItem != nil: 220 | textInputItem.value &= event.text 221 | if textInputItem.onchange != nil: 222 | textInputItem.onchange(textInputItem.value) 223 | return true 224 | 225 | of ekKeyDown, ekKeyUp: 226 | let down = event.kind == ekKeyDown 227 | let scancode = event.scancode 228 | 229 | if down: 230 | if selected >= 0 and selected < items.len and items[selected] of MenuItemText: 231 | # if pressing a key while text item is selected, take it as text input 232 | let te = MenuItemText(items[selected]) 233 | 234 | if scancode == SCANCODE_BACKSPACE and te.value.len > 0: 235 | te.value = te.value[0..te.value.high-1] 236 | if te.onchange != nil: 237 | te.onchange(te.value) 238 | return true 239 | 240 | if not isTextInput(): 241 | startTextInput() 242 | self.textInputItem = te 243 | 244 | elif isTextInput(): 245 | self.textInputItem = nil 246 | stopTextInput() 247 | 248 | case scancode: 249 | of SCANCODE_UP: 250 | selected -= 1 251 | if selected < 0: 252 | selected = items.high 253 | return true 254 | of SCANCODE_DOWN: 255 | selected += 1 256 | if selected > items.high: 257 | selected = 0 258 | return true 259 | of SCANCODE_RETURN: 260 | if selected < 0: 261 | return true 262 | if items[selected].action != nil: 263 | items[selected].action() 264 | return true 265 | of SCANCODE_ESCAPE: 266 | if back != nil: 267 | back() 268 | else: 269 | popMenu() 270 | return true 271 | else: 272 | discard 273 | else: 274 | discard 275 | 276 | return false 277 | -------------------------------------------------------------------------------- /src/ui/paramwindow.nim: -------------------------------------------------------------------------------- 1 | import common 2 | import nico 3 | import nico/vec 4 | import math 5 | import util 6 | 7 | type ParamWindow = ref object of Window 8 | machine*: Machine 9 | favOnly*: bool 10 | scroll*: int 11 | currentParam*: int 12 | dragging*: bool 13 | clickPos*: Vec2f 14 | 15 | const paramNameWidth = 64 16 | 17 | 18 | 19 | method drawContents(self: ParamWindow, x,y,w,h: int) = 20 | # draw parameters for the machine 21 | let sliderWidth = self.w - paramNameWidth - 6 22 | 23 | var yv = y 24 | var x = x 25 | var i = 0 26 | for voice, param in self.machine.parameters(self.favOnly): 27 | if i < self.scroll: 28 | i += 1 29 | continue 30 | 31 | if param.separator: 32 | setColor(5) 33 | line(x,yv,x+paramNameWidth + sliderWidth,yv) 34 | yv += 4 35 | 36 | setColor(if i == self.currentParam: 8 elif param.fav: 10 else: 7) 37 | print((if voice > -1: $voice & ": " else: "") & param.name, x, yv) 38 | 39 | if param.kind != Trigger: 40 | printr(param[].valueString(param.value), x + 63, yv) 41 | var range = (param.max - param.min) 42 | if range == 0.0: 43 | range = 1.0 44 | 45 | let minX = x + paramNameWidth 46 | var maxX = minX + sliderWidth 47 | 48 | if param.kind == Trigger: 49 | # draw button 50 | maxX = minX + 8 51 | if param.triggerTime == frame: 52 | setColor(7) 53 | elif param.triggerTime >= frame: 54 | setColor(8) 55 | else: 56 | setColor(0) 57 | rectfill(minX, yv, maxX, yv+4) 58 | setColor(7) 59 | rect(minX-1, yv-1, maxX+1, yv+4+1) 60 | 61 | else: 62 | # draw slider background 63 | setColor(0) 64 | rectfill(minX, yv, maxX, yv+4) 65 | 66 | # draw slider fill 67 | setColor(if i == self.currentParam: 8 else: 6) 68 | 69 | let zeroX = x + paramNameWidth + sliderWidth.float32 * clamp(invLerp(param.min, param.max, 0.0), 0.0, 1.0) 70 | rectfill(clamp(zeroX, minX, maxX), yv, clamp(minX + sliderWidth.float32 * invLerp(param.min, param.max, param.value), minX, maxX), yv+4) 71 | 72 | # draw default bar 73 | if param.kind != Note: 74 | setColor(7) 75 | let defaultX = minX + sliderWidth.float32 * invLerp(param.min, param.max, param.default) 76 | line(defaultX, yv, defaultX, yv+4) 77 | 78 | yv += 8 79 | 80 | i += 1 81 | if yv + 7 >= y + h: 82 | setColor(7) 83 | print("...", x, yv) 84 | break 85 | 86 | proc getParamByPos(self: ParamWindow, x,y,w,h: int, px,py: int): int = 87 | var yv = y 88 | let nParams = self.machine.getParameterCount(self.favOnly) 89 | for i in self.scroll..= yv and py <= yv + 7: 94 | return i 95 | yv += 8 96 | return -1 97 | 98 | method eventContents(self: ParamWindow, x,y,w,h: int, e: Event): bool = 99 | let sliderWidth = self.w - paramNameWidth - 6 100 | 101 | case e.kind: 102 | of ekMouseWheel: 103 | let mv = mouseVec() 104 | if mv.x >= x and mv.x <= x + w and mv.y >= y and mv.y <= y + h: 105 | self.scroll -= e.ywheel 106 | if self.scroll < 0: 107 | self.scroll = 0 108 | let nParams = self.machine.getParameterCount(self.favOnly) 109 | if self.scroll >= nParams: 110 | self.scroll = nParams 111 | return true 112 | of ekMouseButtonUp: 113 | case e.button: 114 | of 1: 115 | if self.dragging: 116 | self.dragging = false 117 | return true 118 | else: 119 | discard 120 | of ekMouseButtonDown: 121 | case e.button: 122 | of 1: 123 | # check if they clicked on a param bar 124 | let mv = mouseVec() 125 | if mv.x >= x and mv.x <= x + w and mv.y >= y and mv.y <= y + h: 126 | # TODO handle scrollbar 127 | let i = self.getParamByPos(x,y,w,h, mv.x.int, mv.y.int) 128 | if i >= 0: 129 | if mv.x > x + paramNameWidth: 130 | self.currentParam = i 131 | let (voice,param) = self.machine.getParameter(self.currentParam, self.favOnly) 132 | if param.kind == Trigger: 133 | param.onchange(param.value, voice) 134 | param[].trigger(voice) 135 | elif param.kind == Bool: 136 | param.value = if param.value == 0f: 1f else: 0f 137 | param.onchange(param.value, voice) 138 | param[].trigger(voice) 139 | else: 140 | self.dragging = true 141 | self.clickPos = mv 142 | return true 143 | elif mv.x < x + paramNameWidth and e.clicks == 2: 144 | let (voice, param) = self.machine.getParameter(i, self.favOnly) 145 | param.fav = not param.fav 146 | of 3: 147 | # check if they clicked on a param bar 148 | # reset to default value 149 | let mv = mouseVec() 150 | if mv.x >= x and mv.x <= x + w and mv.y >= y and mv.y <= y + h: 151 | if mv.x > x + paramNameWidth: 152 | var yv = y 153 | let nParams = self.machine.getParameterCount() 154 | for i in self.scroll..= y and mv.y <= yv + 7: 159 | self.currentParam = i 160 | let (voice,param) = self.machine.getParameter(self.currentParam, self.favOnly) 161 | param.value = param.default 162 | param.onchange(param.value, voice) 163 | param[].trigger(voice) 164 | return true 165 | yv += 8 166 | else: 167 | discard 168 | of ekMouseMotion: 169 | if self.dragging: 170 | var (voice, param) = self.machine.getParameter(self.currentParam, self.favOnly) 171 | if shift(): 172 | # jump directly to value 173 | param.value = lerp(param.min, param.max, clamp(invLerp(paramNameWidth.float32, paramNameWidth.float + sliderWidth.float, e.x.float), 0.0, 1.0)) 174 | else: 175 | # relative shift 176 | let range = param.max - param.min 177 | let ydist = e.y.float32 - self.clickPos.y 178 | let sensitivity = clamp(10.0 / abs(e.y.float32 - self.clickPos.y)) 179 | let speed = (range / sliderWidth.float32) * sensitivity 180 | if ydist < 3: 181 | param.value = lerp(param.min, param.max, clamp(invLerp(paramNameWidth.float32, paramNameWidth.float + sliderWidth.float, (e.x - x).float), 0.0, 1.0)) 182 | else: 183 | param.value = clamp(param.value + e.xrel * speed, param.min, param.max) 184 | param.onchange(param.value, voice) 185 | return false 186 | of ekKeyDown: 187 | let mv = mouseVec() 188 | if mv.x >= x and mv.x <= x + w and mv.y >= y and mv.y <= y + h: 189 | if e.keycode == K_F: 190 | self.favOnly = not self.favOnly 191 | self.currentParam = 0 192 | self.scroll = 0 193 | return true 194 | else: 195 | discard 196 | return false 197 | 198 | proc newParamWindow*(machine: Machine, x,y,w,h: int): Window = 199 | var win = new(ParamWindow) 200 | win.machine = machine 201 | win.pos = vec2f(x,y) 202 | win.w = w 203 | win.h = h 204 | win.title = machine.name 205 | return win 206 | --------------------------------------------------------------------------------