tidal-midi-0.9: Sound/Tidal/MIDI/Tetra.hs
{-# LANGUAGE OverloadedStrings #-}
{-|
Mapping and Abstractions for the analog four-voice synthesizer, TETR4 by Dave Smith Instruments.
Rather than being a tutorial on how to write your own mappings for your MIDI devices, this is more like the kitchen sink: it shows what is possible.
The DSI TETR4 has four separate voices that can (when set to multi-mode) be controlled via four different MIDI channels. Almost everything you can control directly on the device can be automated via MIDI messages.
-}
module Sound.Tidal.MIDI.Tetra where
import Sound.Tidal.MIDI.Control
import Sound.Tidal.Params
hiding (cutoff_p, attack_p, decay_p, sustain_p, release_p)
import Sound.Tidal.Pattern (Pattern(..), atom)
import Sound.Tidal.Stream
((|+|), (|=|), ( # ), merge, ParamPattern)
import Control.Applicative
{-|
The controller mapping for TETR4
To get the most out of this device with tidal, I chose to use the non-registered parameter (NRPN) variant to control it. This allows to use the full spectrum for controls like @cutoff@ that allow more granular stepping than standard MIDI (164 instead of 128 steps).
Many parameters diverge from the default ranges of 0 to 127 and therefore this mapping uses the @mapRange@ functionality for controls to allow you to always use double params, e.g.:
>>> t1 $ n (run 4) # famt "0.5" # cutoff "0.3" # resonance "0.8"
while @famt@, @cutoff@ and @resonance@ all have different ranges of operation (0..254, 0..14 and 0..127 respectively)
Some parameters are essentially mode switches and I chose to pass the values you enter into patterns directly through to the resulting control change. The method @passThru@ on control specification simply skips the scaling of values (you can write your own scaler if you want).
For very generic control I reused Tidal's own params, like @cutoff@, @attack@, @gain@, @pan@ and @release@. Sometimes the defaults for Tidal do not make sense for MIDI. See @(_, cutoff_p)@ below the actual controller shape for examples on how to deal with this.
-}
tetraController :: ControllerShape
tetraController =
ControllerShape
{ controls =
[ mrNRPN osc1freq_p 0 (0, 120)
, mrNRPN osc1detune_p 1 (0, 100)
, NRPN osc1shape_p 2 (0, 103) passThru
, mNRPN osc1glide_p 3
, NRPN osc1kbd_p 4 (0, 1) passThru
, mrNRPN osc2freq_p 5 (0, 120)
, mrNRPN osc2detune_p 6 (0, 100)
, NRPN osc2shape_p 7 (0, 103) passThru
, mNRPN osc2glide_p 8
, NRPN osc2kbd_p 9 (0, 1) passThru
, NRPN oscsync_p 10 (0, 1) passThru
, NRPN glidemode_p 11 (0, 3) passThru
, NRPN oscslop_p 12 (0, 5) passThru
, mrNRPN oscmix_p 13 (0, 127)
, mNRPN noise_p 14
, mrNRPN cutoff_p 15 (0, 164)
, mNRPN resonance_p 16
, mNRPN kamt_p 17
, mNRPN audiomod_p 18
, NRPN fpoles_p 19 (0, 1) passThru
-- filter envelope
, mrNRPN famt_p 20 (0, 254)
, mNRPN fvel_p 21
, mNRPN fdel_p 22
, mNRPN fatk_p 23
, mNRPN fdcy_p 24
, mNRPN fsus_p 25
, mNRPN frel_p 26
, mNRPN vcavol_p 27
, mNRPN pan_p 28
, mNRPN gain_p 29 -- max volume by default
, mrNRPN vamt_p 30 (0, 127) -- max vca envelope amount
, mNRPN vvel_p 31
, mNRPN vdel_p 32
, mNRPN attack_p 33
, mrNRPN decay_p 34 (0, 127)
, mrNRPN sustain_p 35 (0, 127)
, mNRPN release_p 36
, NRPN lfo1rate_p 37 (0, 166) passThru
, NRPN lfo1shape_p 38 (0, 4) passThru
, mNRPN lfo1amt_p 39
, NRPN lfo1dest_p 40 (0, 43) passThru
, NRPN lfo1sync_p 41 (0, 1) passThru
, NRPN lfo2rate_p 42 (0, 166) passThru -- unsynced
, NRPN lfo2shape_p 43 (0, 4) passThru
, mNRPN lfo2amt_p 44
, NRPN lfo2dest_p 45 (0, 43) passThru
, NRPN lfo2sync_p 46 (0, 1) passThru
, NRPN lfo3rate_p 47 (0, 166) passThru -- unsynced
, NRPN lfo3shape_p 48 (0, 4) passThru
, mNRPN lfo3amt_p 49
, NRPN lfo3dest_p 50 (0, 43) passThru
, NRPN lfo3sync_p 51 (0, 1) passThru
, NRPN lfo4rate_p 52 (0, 166) passThru -- unsynced
, NRPN lfo4shape_p 53 (0, 4) passThru
, mNRPN lfo4amt_p 54
, NRPN lfo4dest_p 55 (0, 43) passThru
, NRPN lfo4sync_p 56 (0, 1) passThru
, NRPN emod_p 57 (0, 43) passThru
, mrNRPN eamt_p 58 (0, 254)
, mNRPN evel_p 59
, mNRPN edel_p 60
, mNRPN eatk_p 61
, mNRPN edcy_p 62
, mNRPN esus_p 63
, mNRPN erel_p 64
, NRPN mod1src_p 65 (0, 20) passThru
, mrNRPN mod1amt_p 66 (0, 254)
, NRPN mod1dst_p 67 (0, 47) passThru
, NRPN mod2src_p 68 (0, 20) passThru
, mrNRPN mod2amt_p 69 (0, 254)
, NRPN mod2dst_p 70 (0, 47) passThru
, NRPN mod3src_p 71 (0, 20) passThru
, mrNRPN mod3amt_p 72 (0, 254)
, NRPN mod3dst_p 73 (0, 47) passThru
, NRPN mod4src_p 74 (0, 20) passThru
, mrNRPN mod4amt_p 75 (0, 254)
, NRPN mod4dst_p 76 (0, 47) passThru
, NRPN seq1dst_p 77 (0, 47) passThru
, NRPN seq2dst_p 78 (0, 47) passThru
, NRPN seq3dst_p 79 (0, 47) passThru
, NRPN seq4dst_p 80 (0, 47) passThru
, mrNRPN mwhl_p 81 (0, 254)
, NRPN mwhldst_p 82 (0, 47) passThru
, mrNRPN aftt_p 83 (0, 254)
, NRPN afttdst_p 84 (0, 47) passThru
, mrNRPN breath_p 85 (0, 254)
, NRPN breathdst_p 86 (0, 47) passThru
, mrNRPN mvel_p 87 (0, 254)
, NRPN mveldst_p 88 (0, 47) passThru
, mrNRPN foot_p 89 (0, 254)
, NRPN footdst_p 90 (0, 47) passThru
, NRPN kbpm_p 91 (30, 250) passThru
, NRPN clockdiv_p 92 (0, 12) passThru -- TODO: document values
, NRPN bendrng_p 93 (0, 12) passThru
, NRPN sqntrig_p 94 (0, 4) passThru -- TODO: document values
, NRPN unisonkey_p 95 (0, 5) passThru -- TODO: document values
, NRPN unisonmode_p 96 (0, 4) passThru -- TODO: document values
, NRPN arpmode_p 97 (0, 14) passThru
, NRPN erepeat_p 98 (0, 1) passThru
, NRPN unison_p 99 (0, 1) passThru
, NRPN arp_p 100 (0, 1) passThru
, NRPN sqn_p 101 (0, 1) passThru
, NRPN mcr1_p 105 (0, 183) passThru
, NRPN mcr2_p 106 (0, 183) passThru
, NRPN mcr3_p 107 (0, 183) passThru
, NRPN mcr4_p 108 (0, 183) passThru
, mNRPN shape_p 110
, mrNRPN btnfreq_p 111 (0, 127)
, mrNRPN btnvel_p 112 (0, 127)
, NRPN btnmode_p 113 (0, 1) passThru
, mNRPN pitch1_p 114
, mNRPN pitch2_p 115
, mNRPN fbvol_p 116
-- left out: editor byte,
, mrNRPN ksplitpoint_p 118 (0, 127)
-- each patch has layer a/b
, NRPN kmode_p 119 (0, 2) passThru -- 0: normal, 1: stack, both layers respond to full key range, 2: split at ksplitpoint
, mCC notesoff_p 123
, mCC ccreset_p 121
, mCC damp_p 64
]
, latency = 0.1
}
tetra = toShape tetraController
(notesoff, notesoff_p) = pF "notesoff" (Just 0)
(ccreset, ccreset_p) = pF "ccreset" (Just 0)
(damp, damp_p) = pF "damp" (Just (-1))
(osc1freq, osc1freq_p) = pF "osc1freq" (Just 0.2)
(osc1detune, osc1detune_p) = pF "osc1detune" (Just 0.5)
(osc1shape, osc1shape_p) = pI "osc1shape" (Just 1)
(osc1glide, osc1glide_p) = pF "osc1glide" (Just 0)
(osc1kbd, osc1kbd_p) = pI "osc1kbd" (Just 1)
(osc2freq, osc2freq_p) = pF "osc2freq" (Just 0.2)
(osc2detune, osc2detune_p) = pF "osc2detune" (Just 0.5)
(osc2shape, osc2shape_p) = pI "osc2shape" (Just 1)
(osc2glide, osc2glide_p) = pF "osc2glide" (Just 0)
(osc2kbd, osc2kbd_p) = pI "osc2kbd" (Just 1)
(oscsync, oscsync_p) = pF "oscsync" (Just 0)
(glidemode, glidemode_p) = pI "glidemode" (Just 0)
(oscslop, oscslop_p) = pF "oscslop" (Just 0)
(oscmix, oscmix_p) = pF "oscmix" (Just 0.5)
(noise, noise_p) = pF "noise" (Just 0)
(kamt, kamt_p) = pF "kamt" (Just 0.5)
-- overridden defaults for MIDI
(_, cutoff_p) = pF "cutoff" (Just 1)
(_, attack_p) = pF "attack" (Just 0.12)
(_, release_p) = pF "release" (Just 0.01)
(_, decay_p) = pF "decay" (Just 0.5)
(_, sustain_p) = pF "sustain" (Just 0.5)
(audiomod, audiomod_p) = pF "audiomod" (Just 0)
(fpoles, fpoles_p) = pI "fpoles" (Just 0)
twopole = fpoles "0"
fourpole = fpoles "1"
-- filter envelope
(famt, famt_p) = pF "famt" (Just 0.5)
(fvel, fvel_p) = pF "fvel" (Just 0)
(fdel, fdel_p) = pF "fdel" (Just 0)
(fatk, fatk_p) = pF "fatk" (Just 0.01)
(fdcy, fdcy_p) = pF "fdcy" (Just 0)
(fsus, fsus_p) = pF "fsus" (Just 0)
(frel, frel_p) = pF "frel" (Just 0.01)
(vcavol, vcavol_p) = pF "vcavol" (Just 0)
(vamt, vamt_p) = pF "vamt" (Just 1)
(vvel, vvel_p) = pF "vvel" (Just 0)
(vdel, vdel_p) = pF "vdel" (Just 0)
(lfo1rate, lfo1rate_p) = pF "lfo1rate" (Just 0)
(lfo1shape, lfo1shape_p) = pI "lfo1shape" (Just 0)
(lfo1amt, lfo1amt_p) = pF "lfo1amt" (Just 0)
(lfo1dest, lfo1dest_p) = pF "lfo1dest" (Just 0)
(lfo1sync, lfo1sync_p) = pI "lfo1sync" (Just 0)
(lfo2rate, lfo2rate_p) = pF "lfo2rate" (Just 0)
(lfo2shape, lfo2shape_p) = pI "lfo2shape" (Just 0)
(lfo2amt, lfo2amt_p) = pF "lfo2amt" (Just 0)
(lfo2dest, lfo2dest_p) = pF "lfo2dest" (Just 0)
(lfo2sync, lfo2sync_p) = pI "lfo2sync" (Just 0)
(lfo3rate, lfo3rate_p) = pF "lfo3rate" (Just 0)
(lfo3shape, lfo3shape_p) = pI "lfo3shape" (Just 0)
(lfo3amt, lfo3amt_p) = pF "lfo3amt" (Just 0)
(lfo3dest, lfo3dest_p) = pF "lfo3dest" (Just 0)
(lfo3sync, lfo3sync_p) = pI "lfo3sync" (Just 0)
(lfo4rate, lfo4rate_p) = pF "lfo4rate" (Just 0)
(lfo4shape, lfo4shape_p) = pI "lfo4shape" (Just 0)
(lfo4amt, lfo4amt_p) = pF "lfo4amt" (Just 0)
(lfo4dest, lfo4dest_p) = pF "lfo4dest" (Just 0)
(lfo4sync, lfo4sync_p) = pI "lfo4sync" (Just 0)
(emod, emod_p) = pF "emod" (Just 0)
(eamt, eamt_p) = pF "eamt" (Just 0.5)
(evel, evel_p) = pF "evel" (Just 0)
(edel, edel_p) = pF "edel" (Just 0)
(eatk, eatk_p) = pF "eatk" (Just 0.01)
(edcy, edcy_p) = pF "edcy" (Just 0)
(esus, esus_p) = pF "esus" (Just 0)
(erel, erel_p) = pF "erel" (Just 0.01)
(mod1src, mod1src_p) = pF "mod1src" (Just 0)
(mod1amt, mod1amt_p) = pF "mod1amt" (Just 0.5)
(mod1dst, mod1dst_p) = pF "mod1dst" (Just 0)
(mod2src, mod2src_p) = pF "mod2src" (Just 0)
(mod2amt, mod2amt_p) = pF "mod2amt" (Just 0.5)
(mod2dst, mod2dst_p) = pF "mod2dst" (Just 0)
(mod3src, mod3src_p) = pF "mod3src" (Just 0)
(mod3amt, mod3amt_p) = pF "mod3amt" (Just 0.5)
(mod3dst, mod3dst_p) = pF "mod3dst" (Just 0)
(mod4src, mod4src_p) = pF "mod4src" (Just 0)
(mod4amt, mod4amt_p) = pF "mod4amt" (Just 0.5)
(mod4dst, mod4dst_p) = pF "mod4dst" (Just 0)
(seq1dst, seq1dst_p) = pF "seq1dst" (Just 0)
(seq2dst, seq2dst_p) = pF "seq2dst" (Just 0)
(seq3dst, seq3dst_p) = pF "seq3dst" (Just 0)
(seq4dst, seq4dst_p) = pF "seq4dst" (Just 0)
(mwhl, mwhl_p) = pF "mwhl" (Just 0.5)
(mwhldst, mwhldst_p) = pI "mwhldst" (Just 0)
(aftt, aftt_p) = pF "aftt" (Just 0.5)
(afttdst, afttdst_p) = pI "afttdst" (Just 0)
(breath, breath_p) = pF "breath" (Just 0.5)
(breathdst, breathdst_p) = pI "breathdst" (Just 0)
(mvel, mvel_p) = pF "mvel" (Just 0.5)
(mveldst, mveldst_p) = pI "mveldst" (Just 0)
(foot, foot_p) = pF "foot" (Just 0.5)
(footdst, footdst_p) = pI "footdst" (Just 0)
(bendrng, bendrng_p) = pI "bendrng" (Just 0)
-- left out: modwheel, breath, footctrl, pressure, velocity
(kbpm, kbpm_p) = pI "kbpm" (Just 0)
(clockdiv, clockdiv_p) = pI "clockdiv" (Just 0)
-- left out: pitchbend range
(sqntrig, sqntrig_p) = pF "sqntrig" (Just 0)
(unisonkey, unisonkey_p) = pF "unisonkey" (Just 0)
(unisonmode, unisonmode_p) = pF "unisonmode" (Just 0)
(arpmode, arpmode_p) = pF "arpmode" (Just 0)
(erepeat, erepeat_p) = pF "erepeat" (Just 0)
(unison, unison_p) = pF "unison" (Just 0)
(arp, arp_p) = pF "arp" (Just 0)
(sqn, sqn_p) = pF "sqn" (Just 0)
(mcr1, mcr1_p) = pI "mcr1" (Just 0)
(mcr2, mcr2_p) = pI "mcr2" (Just 0)
(mcr3, mcr3_p) = pI "mcr3" (Just 0)
(mcr4, mcr4_p) = pI "mcr4" (Just 0)
(btnfreq, btnfreq_p) = pF "btnfreq" (Just 0.25)
(btnvel, btnvel_p) = pF "btnvel" (Just 1)
(btnmode, btnmode_p) = pF "btnmode" (Just 0)
(sub1vol, sub1vol_p) = pF "sub1vol" (Just 0)
(sub2vol, sub2vol_p) = pF "sub2vol" (Just 0)
(fbvol, fbvol_p) = pF "fbvol" (Just 0)
(ksplitpoint, ksplitpoint_p) = pF "ksplitpoint" (Just 0.5)
(kmode, kmode_p) = pF "kmode" (Just 0)
knormal = kmode "0"
kstack = kmode "0.5"
ksplit = kmode "1"
{-| Abstractions
Though not yet finished, these are some examples on how to combine multiple parameters into one single function you can use within your patterns.
Since Tidal 0.7 you can use the @grp@ method to allow things like:
>>> t1 $ n (run 4) # adsr "0.1:0.6:0.3:0.9 0.8:0.3:0.9:0.1"
which alternates between two filter envelope shapes, the first one with a sharp attack and a long decay/release and the latter with a long attack, high sustain and a short release.
-}
-- adsr
adsr = grp [attack_p, decay_p, sustain_p, release_p]
atk3 = grp [attack_p, fatk_p, eatk_p]
dcy3 = grp [decay_p, fdcy_p, edcy_p]
sus3 = grp [sustain_p, fsus_p, esus_p]
rel3' = grp [release_p, frel_p, erel_p]
-- lfo
lfotri = doublePattern 0
lforsaw = doublePattern 1
lfosaw = doublePattern 2
lfopulse = doublePattern 3
lforand = doublePattern 4
{-|
A hack to handle a param with completely different behavior for certain parts of the range:
@lrate@ is limited from 0 to 150 and specifies values that will produce an lfo frequency
@lstep@ however is essentially limited from 0 to 16 and specifies values that will produce rhythmic lfo based on the _sequence speed_. as taken from the manual:
0: 32 steps
1: 16 steps
2: 8 steps
3: 6 steps
4: 4 steps
5: 3 steps
6: 2 steps
7: 1.5 steps
8: 1 step
9: 2/3 steps
10: 1/2 step
11: 1/3 step
12: 1/4 step
13: 1/6 step
14: 1/8 step
15: 1/16 step
-}
lrate r = min 150 . max 0 <$> r
lstep s = min 166 . max 151 . (+ 150) <$> s
lfo1 s d r a = lfo1shape s |+| lfo1dest d |+| lfo1rate r |+| lfo1amt a
lfo2 s d r a = lfo2shape s |+| lfo2dest d |+| lfo2rate r |+| lfo2amt a
lfo3 s d r a = lfo3shape s |+| lfo3dest d |+| lfo3rate r |+| lfo3amt a
lfo4 s d r a = lfo4shape s |+| lfo4dest d |+| lfo4rate r |+| lfo4amt a
-- mod
mod1 s d a = mod1src s |+| mod1dst d |+| mod1amt a
mod2 s d a = mod2src s |+| mod2dst d |+| mod2amt a
mod3 s d a = mod3src s |+| mod3dst d |+| mod3amt a
mod4 s d a = mod4src s |+| mod4dst d |+| mod4amt a
{-| Modulation
named sources and destinations to able to refer them for lfos, mods and also sequences.
-}
doublePattern d = atom d :: Pattern Double
dosc1 = doublePattern 1
dosc2 = doublePattern 2
dosc = doublePattern 3
dmix = doublePattern 4
dnoise = doublePattern 5
dpw1 = doublePattern 6
dpw2 = doublePattern 7
dpw = doublePattern 8
dcut = doublePattern 9
dres = doublePattern 10
damod = doublePattern 11
dvca = doublePattern 12
dspread = doublePattern 13
dlfo1f = doublePattern 14
dlfo2f = doublePattern 15
dlfo3f = doublePattern 16
dlfo4f = doublePattern 17
dlfof = doublePattern 18
dlfo1a = doublePattern 19
dlfo2a = doublePattern 20
dlfo3a = doublePattern 21
dlfo4a = doublePattern 22
dlfoa = doublePattern 23
dfamt = doublePattern 24
dvamt = doublePattern 25
deamt = doublePattern 26
damt = doublePattern 27
dfatk = doublePattern 28
dvatk = doublePattern 29
deatk = doublePattern 30
datk = doublePattern 31
dfdcy = doublePattern 32
dvdcy = doublePattern 33
dedcy = doublePattern 34
ddcy = doublePattern 35
dfrel = doublePattern 36
dvrel = doublePattern 37
derel = doublePattern 38
drel = doublePattern 39
dmod1 = doublePattern 40
dmod2 = doublePattern 41
dmod3 = doublePattern 42
dmod4 = doublePattern 43
dfb = doublePattern 44
dsub1 = doublePattern 45
dsub2 = doublePattern 46
dshape = doublePattern 47
dslew = doublePattern 48
-- mod sources
sseq1 = doublePattern 1
sseq2 = doublePattern 2
sseq3 = doublePattern 3
sseq4 = doublePattern 4
slfo1 = doublePattern 5
slfo2 = doublePattern 6
slfo3 = doublePattern 7
slfo4 = doublePattern 8
sfenv = doublePattern 9
svenv = doublePattern 10
seenv = doublePattern 11
spitchb = doublePattern 12
smodwh = doublePattern 13
saftert = doublePattern 14
sbreath = doublePattern 15
sfoot = doublePattern 16
sexpr = doublePattern 17
svel = doublePattern 18
snote = doublePattern 19
snoise = doublePattern 20
{-|
Presets suck, but I constantly forget how to make drums
with the TETR4, so here are some defaults (that can mostly be changed when using them)
to make a snare and a kick.
Use the snare like this:
>>> t1 $ n "0(3,8)" # snare
To make it shorter:
>>> t1 $ n "0(3,8)" # snare |-| release "0.1" |-| decay "0.1"
-}
snare =
fourpole |=| osc1shape "0" |+| osc1kbd "0" |+| osc2shape "0" |+| osc2kbd "0" |+|
noise "1" |+|
shape "0.7" |+|
fbvol "0.3" |=|
resonance "0.2" |+|
release "0.3" |+|
sustain "0" |+|
decay "0.3" |+|
cutoff "1" |+|
dur "0.02"
{-|
usage would be:
>>> t1 $ n "0 [[0 1] 1]" # kick
you can change the defaults by applying merge operations for certain params:
>>> t1 $ n "0(3,8)" # kick |+| edcy "0.05"
will give the kick more resonance
>>> t1 $ n "0(3,8)" # kick |-| edcy "0.15"
will make it really dry
-}
kick =
fourpole |+| osc1shape "0" |+| osc1kbd "0" |+| osc2shape "0" |+| osc2kbd "0" |+|
sustain "0" |+|
decay "0.95" |+|
release "0.5" |+|
resonance "0.99" |+|
cutoff "0.001" |+|
eamt "0.8" |+|
emod "9" |+|
edcy "0.2"
{-|
Since the TETR4 lacks a high-pass filter, things like snares and especially cymbals are somewhat limited to filtered noise which sounds okish.
Therefore some presets for the different types of sounds you can get from the TETR4 can come in handy and can also be combined and modified with and through others:
-}
{-|
A chip tune like sound, reminds me of gameboys
-}
chip =
osc1shape "2" # osc2shape "2" |+| osc2freq "0.23" # osc1glide "0.05" #
osc2glide "0.05" #
glidemode "3" #
release "0.78"