Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pitch sequencer, more parameters, more grid controls, and nb support #4

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,102 @@ Note that after installing you must `SYSTEM => RESET` your Norns before running
Launching randomizes all voices, and drops you into a euclidean sequencer. Controls are as follows:

```
Trigger View
E1 select
E2 density
E3 length
K2 reset phase
K3 start/stop

Pitch View
E1 select
E2 note
E3 (turn right) increment all pitches in sequence
E3 (turn left) randomly increment each pitch in sequence

Both Views
K1 = ALT
K2 reset phase
K3 start/stop
ALT-E1 = bpm
ALT+K2 = switch between trigger and pitch views
ALT+K3 = randomize all voices
```

Use the `PARAMS` menu to configure voices manually, set up clock/sync behavior, MIDI map, and manage voice presets.
Use the `PARAMS` menu to configure voices manually, set up clock/sync behavior, MIDI map, and manage voice presets. To switch between "Trigger View" and "Pitch View" press and hold down K1 and press K2 afterwards. Trigger View displays the trigger patterns for each track. Pitch View displays the note and pitch sequence for each track.

Each track has a fixed length pitch sequence of 4. Each step will offset the current note by either 0, +5, +7, or +12 semitones. Each time a trigger occurs, it will step forward 1 step in the pitch sequence. For example, the default note is 60 which is middle C. If the offset sequence is 0, +5, 0, +12, then you will hear midi note sequence 60, 65, 60, 72. Turn E3 to the right to increment all offsets, which would result in a sequence +5, +7, +5, 0 instead. If you turn E3 to the left, then for each step there is a 50% chance the offset will increment. Press a pad on the grid (columns 12 - 15) to increment the offset of a step. Pressing K2 only resets the trigger sequencer.

## Grid Control
Tap in & clear rhythms with 1-4, nudge synth parameters up & down with 5-10. Randomize the synth voice with 15
![Grid Control](grid-rudiments.PNG)

Tap in & clear rhythms with 1-4, nudge synth parameters up & down with 5-11, set pitch offset sequence with 12-15, randomize the synth voice with 16.

Column Pads (left to right)
1. Clear all triggers
2. Increase density by 1
3. Set length to 1
4. Increase length by 1
5. Decrease pitch by 1 note
6. Increase pitch by 1 note
7. Decrease envelope time
8. Increase envelope time
9. Decrease LFO time, behavior varies per synth
10. Increase LFO time, behavior varies per synth
11. Switch (ON/OFF), behavior varies per synth
12. Pitch sequence step 1
13. Pitch sequence step 2
14. Pitch sequence step 3
15. Pitch sequence step 4
16. Randomizes synth voice, behavior varies per synth

## nb synth controls

To turn off the internal supercollider engine Rudiments, find the parameter internal_ON_OFF and set it to 0 (default 1, ON). To use nb synths, the library nb is required as well as the nb mods for each synth you wish to use.

In the parameter menu, there should be "nb_1" through "nb_8". There is an nb voice selector for each track in Rudiments. Multiple tracks can select the same nb voice. For example, multiple tracks can select doubledecker to play polyphonically. Or multiple tracks can select emplaitress 1 to combine multiple tracks into a more complicated monophonic sequence.

Here are specific grid column behaviors (columns 9, 10, 11, 16) for each supported nb voice.

### [nb_drumcrow](https://github.com/entzmingerc/nb_drumcrow)
9. Decrease pulse width (-0.05)
10. Increase pulse width (+0.05) (pw behavior changes for each drumcrow synth model)
11. ON/OFF: bit, bit lfo, and amplitude envelope bit
16. Randomizes env decay, pulse width, pulse width 2, and midi note of track

### [emplaitress](https://github.com/sixolet/emplaitress)
9. Decrease harmonics (-0.05)
10. Increase harmonics (+0.05)
11. Set FM mod to 0.9 (ON) or 0 (OFF)
16. Randomizes decay, harmonics, timbre, morph, and midi note of track

### [oilcan](https://github.com/zjb-s/oilcan)
9. Decrease modulation release for all 7 timbres (x * 0.9)
10. Increase modulation release for all 7 timbres (x * 1.1)
11. +1 midi note (ON) +0 midi note (OFF)
16. Randomizes frequency, sweep time, sweep index, env release, modulation release, modulation level, modulation ratio, feedback, and fold for all 7 timbres

Note: Midi note selects which of the 7 "timbres" is triggered! This value wraps around every 7 notes. Try using multiple rudiments rows to trigger the same Oilcan voice with each track at different pitches. The default midi note 60 selects timbre 4. Use column 11 switch for variation of selected timbres (+1 or +0). Use the pitch sequencer to vary the timbre sequencing.

### [nb_rudiments](https://github.com/entzmingerc/nb_rudiments)
9. Decrease lfoFreq and lfoSweep parameters (x * 0.9)
10. Increase lfoFreq and lfoSweep parameters (x * 1.1)
11. Sets osc shape to square (ON) or sine (OFF)
16. Randomizes all parameters except osc shape and gain

### [doubledecker](https://github.com/sixolet/doubledecker)
9. Decrease brilliance (-0.05)
10. Increase brilliance (+0.05)
11. EACH PRESS will increment the layer 2 pitch ratio value and wrap through 9 values
16. Randomizes brilliance, amp release, portomento, and low and high pass filter parameters

## SuperCollider Engine

This script makes a new SuperCollider engine available, `Rudiments`. Please see `lib/engine_rudiments.sc` for the latest parameter definitions.
This script makes a new SuperCollider engine available, `Rudiments`. Please see `lib/engine_rudiments.sc` for the latest parameter definitions. The SuperCollider engine Rudiments was ported as an nb voice called [nb_rudiments](https://github.com/entzmingerc/nb_rudiments). Please refer to [nb](https://llllllll.co/t/n-b-et-al-v0-1/60374) if you'd like to add specific support for an nb voice.

## Thanks

Thanks to [@rbxbx](http://github.com/rbxbx) for porting the playfair sequencer to this engine.
Thanks to [@cfdrake](https://github.com/cfdrake) for creating rudiments
Thanks to [@rbxbx](http://github.com/rbxbx) for porting the playfair sequencer to this engine
Thanks to [@tehn](https://llllllll.co/u/tehn/summary) for writing playfair
Thanks to [@yaw](https://llllllll.co/u/yaw/summary) for adding grid controls
Thanks to [@sixolet](https://llllllll.co/u/sixolet/summary) for writing nb
Thanks to [@postsolarpunk](https://llllllll.co/u/postsolarpunk/summary) for adding pitch view, grid controls, nb support
Binary file modified grid-rudiments.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions lib/nb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Nota Bene

A norns library that plays notes well.

The goal of this is to reduce the work of supporting a lot of different kinds of voices, and to allow people to install the voices they want to play with individually, as mods. You can also package voices with your script.


To use the `nb` library in your script, do:
```
git submodule add https://github.com/sixolet/nb.git lib/nb
```

This will add `nb` as a *submodule*. You can then update it to the latest version by using

```
git submodule update --remote
git commit -m 'update submodules'
```

Then you can edit your script to use the `nb` library. Some highlights:

```
nb:init() -- run this first, from your init method.
```

```
nb:add_param("voice_id", "voice") -- adds a voice selector param to your script.
nb:add_player_params() -- Adds the parameters for the selected voices to your script.
```

```
-- Grab the chosen voice's player off your param
local player = params:lookup_param("voice_id"):get_player()
-- Play a note at velocity 0.5 for 0.2 beats (according to the norns clock)
player:play_note(48, 0.5, 0.2)
-- You can also use note_on and note_off methods
player:note_on(48, 0.5)
--- time elapses...
player:note_off(48)
```

MIDI devices that are currently connected while the script is started will be available for selection as vocies. Other vocies depend on any voices included with the script or installed as mods.
196 changes: 196 additions & 0 deletions lib/nb/lib/nb.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
local mydir = debug.getinfo(1).source:match("@?" .. _path.code .. "(.*/)")
local player_lib = include(mydir .. "player")
local nb = {}

if note_players == nil then
note_players = {}
end

-- note_players is a global that you can add note-players to from anywhere before
-- you call nb:add_param.
nb.players = note_players -- alias the global here. Helps with standalone use.

nb.none = player_lib:new()

-- Set this before init() to affect the number of voices added for some mods.
nb.voice_count = 1

local abbreviate = function(s)
if string.len(s) < 8 then return s end
local acronym = util.acronym(s)
if string.len(acronym) > 3 then return acronym end
return string.sub(s, 1, 8)
end

local function add_midi_players()
for i, v in ipairs(midi.vports) do
for j = 1, nb.voice_count do
(function(i, j)
if v.connected then
local conn = midi.connect(i)
local player = {
conn = conn
}
function player:add_params()
params:add_group("midi_voice_" .. i .. '_' .. j, "midi "..j..": " .. abbreviate(v.name), 2)
params:add_number("midi_chan_" .. i .. '_' .. j, "channel", 1, 16, 1)
params:add_number("midi_modulation_cc_" .. i .. '_' .. j, "modulation cc", 1, 127, 72)
params:hide("midi_voice_" .. i .. '_' .. j)
end

function player:ch()
return params:get("midi_chan_" .. i .. '_' .. j)
end

function player:note_on(note, vel)
self.conn:note_on(note, util.clamp(math.floor(127 * vel), 0, 127), self:ch())
end

function player:note_off(note)
self.conn:note_off(note, 0, self:ch())
end

function player:active()
params:show("midi_voice_" .. i .. '_' .. j)
_menu.rebuild_params()
end

function player:inactive()
params:hide("midi_voice_" .. i .. '_' .. j)
_menu.rebuild_params()
end

function player:modulate(val)
self.conn:cc(params:get("midi_modulation_cc_" .. i.. '_' .. j), util.clamp(math.floor(127 * val), 0, 127),
self:ch())
end

function player:describe()
local mod_d = "cc"
if params.lookup["midi_modulation_cc_" .. i .. '_' .. j] ~= nil then
mod_d = "cc " .. params:get("midi_modulation_cc_" .. i .. '_' .. j)
end
return {
name = "v.name",
supports_bend = false,
supports_slew = false,
modulate_description = mod_d
}
end

nb.players["midi: " .. abbreviate(v.name) .. " " .. j] = player
end
end)(i, j)
end
end
end

-- Call from your init method.
function nb:init()
nb_player_refcounts = {}
add_midi_players()
self:stop_all()
end

-- Add a voice select parameter. Returns the parameter. You can then call
-- `get_player()` on the parameter object, which will return a player you can
-- use to play notes and stuff.
function nb:add_param(param_id, param_name)
local names = {}
for name, _ in pairs(note_players) do
table.insert(names, name)
end
table.sort(names)
table.insert(names, 1, "none")
local names_inverted = tab.invert(names)
params:add_option(param_id, param_name, names, 1)
local string_param_id = param_id .. "_hidden_string"
params:add_text(string_param_id, "_hidden string", "")
params:hide(string_param_id)
local p = params:lookup_param(param_id)
local initialized = false
function p:get_player()
local name = params:get(string_param_id)
if name == "none" then
if p.player ~= nil then
p.player:count_down()
end
p.player = nil
return nb.none
elseif p.player ~= nil and p.player.name == name then
return p.player
else
if p.player ~= nil then
p.player:count_down()
end
local ret = player_lib:new(nb.players[name])
ret.name = name
p.player = ret
ret:count_up()
return ret
end
end

clock.run(function()
clock.sleep(1)
p:get_player()
initialized = true
end, p)
params:set_action(string_param_id, function(name_param)
local i = names_inverted[params:get(string_param_id)]
if i ~= nil then
-- silently set the interface param.
params:set(param_id, i, true)
end
p:get_player()
end)
params:set_action(param_id, function()
if not initialized then return end
local i = p:get()
params:set(string_param_id, names[i])
end)
end

local function pairsByKeys(t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0 -- iterator variable
local iter = function() -- iterator function
i = i + 1
if a[i] == nil then return nil
else return a[i], t[a[i]]
end
end
return iter
end

function nb:add_player_params()
if params.lookup['nb_sentinel_param'] then
return
end
for name, player in pairsByKeys(self:get_players()) do
player:add_params()
end
params:add_binary('nb_sentinel_param', 'nb_sentinel_param')
params:hide('nb_sentinel_param')
end

-- Return all the players in an object by name.
function nb:get_players()
local ret = {}
for k, v in pairs(self.players) do
ret[k] = player_lib:new(v)
end
table.sort(ret)
return ret
end

-- Stop all voices. Call when you load a pset to avoid stuck notes.
function nb:stop_all()
for _, player in pairs(self:get_players()) do
player:stop_all()
end
end

return nb
Loading