sonic-pi-akai-apc-mini 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +10 -3
- data/example.rb +65 -0
- data/init_dev.rb +15 -0
- data/lib/sonic-pi-akai-apc-mini/api.rb +37 -32
- data/lib/sonic-pi-akai-apc-mini/controller.rb +68 -0
- data/lib/sonic-pi-akai-apc-mini/helpers.rb +22 -4
- data/lib/sonic-pi-akai-apc-mini.rb +1 -0
- metadata +5 -3
- data/bin/console +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd279db408994ddf67942a34f1576feb382b7fedafdcaeeeb227f447b6b1073c
|
4
|
+
data.tar.gz: 628d1d73459b6e01f7554317e53db308967b17753cc94c2e48845d9c909bd485
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd0673065b74993754899a449a1129ecfcce6b252fb93834ac8be33d148cff04f13a1361417ee9682117cd4bf9a41cd8d4d89a83347479335dcadbaaa9d99d35
|
7
|
+
data.tar.gz: 20d6cc379e57eda32090cd3cae0b70fab1c18946d9f17ffe79cbb34eac12d7aa556eee4065c5abd28fdd2ce7cc87a5ca5648a610c37b24a1da0cdd6f72069eee
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -16,7 +16,7 @@ You should _probably_ not use this yet for live performances, at least without h
|
|
16
16
|
|
17
17
|
## Installation
|
18
18
|
|
19
|
-
Download, clone the code or install the gem, then add this to the top of your Sonic Pi buffer (or your `~/.sonic-pi/config/init.rb`):
|
19
|
+
[Download](https://github.com/porras/sonic-pi-akai-apc-mini/archive/refs/heads/main.zip), clone the code or install [the gem](https://rubygems.org/gems/sonic-pi-akai-apc-mini), then add this to the top of your Sonic Pi buffer (or your `~/.sonic-pi/config/init.rb`):
|
20
20
|
|
21
21
|
```ruby
|
22
22
|
require '<path-to-sonic-pi-akai-apc-mini>/init.rb'
|
@@ -29,15 +29,22 @@ Sonic Pi ships with its own Ruby, meaning that in principle it has no access to
|
|
29
29
|
|
30
30
|
## Usage
|
31
31
|
|
32
|
-
|
32
|
+
If you want to skip this reference, feel free to load `example.rb` on Sonic Pi and start jamming. Otherwise:
|
33
|
+
|
34
|
+
First of all, call `initialize_akai(<model>)` at the top of your buffer. That will make all the features available.
|
33
35
|
|
34
36
|
A small set of functions get added to the Sonic Pi API, in order to use the controls in the APC mini in different ways.
|
35
37
|
|
38
|
+
### Supported models
|
39
|
+
|
40
|
+
* `:apc_mini`
|
41
|
+
* `:apc_key_25` (experimental; please contact the author if you use it, either successfully or not. in any case, only the grid and the knobs are supported, not the keyboard --yet)
|
42
|
+
|
36
43
|
### Faders
|
37
44
|
|
38
45
|
#### `fader(n, [target-values])`
|
39
46
|
|
40
|
-
This function lets you use any of the faders to control the value of _anything_ in Sonic Pi. `n` is the fader number (
|
47
|
+
This function lets you use any of the faders to control the value of _anything_ in Sonic Pi. `n` is the fader number (starting from 0, left to right). `target-values` is the range of values the fader will map to (and defaults to `(0..1)`\*). Some examples:
|
41
48
|
|
42
49
|
```ruby
|
43
50
|
play :c4, amp: fader(0)
|
data/example.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# This is a (probably contrived) example of the kind of things you can control.
|
2
|
+
# This short script sets things up so that:
|
3
|
+
#
|
4
|
+
# Rows 0 and 1 of the grid are used for a simple drum loop with a kick and a
|
5
|
+
# snare. Toggling the buttons in those rows will get you the pattern played, in
|
6
|
+
# real time (note that this is initialized as an empty pattern, and the volume
|
7
|
+
# is controlled by faders 0 and 2, so you won't hear anything until you toggle
|
8
|
+
# some notes and push those faders).
|
9
|
+
#
|
10
|
+
# The is a second live_loop with a chord and an arpeggio based on that chord,
|
11
|
+
# where some of the parameters are controlled by faders:
|
12
|
+
#
|
13
|
+
# 0 -> Kick volume
|
14
|
+
# 1 -> Kick cutoff frequency
|
15
|
+
# 2 -> Snare volume
|
16
|
+
# 3 -> Snare cutoff frequency
|
17
|
+
# 4 -> The chord itself! Atypical UI for this for works fine
|
18
|
+
# 5 -> Chord volume
|
19
|
+
# 6 -> Arpeggio volume
|
20
|
+
# 7 -> Arpeggio reverb amount
|
21
|
+
#
|
22
|
+
# Finally a switch is setup on button [2, 0] (leftmost button of third row,
|
23
|
+
# starting from the bottom) to toggle a background vinyl effect.
|
24
|
+
#
|
25
|
+
# Feel free to experiment with changing the music using the controller only,
|
26
|
+
# then to try to control different things!
|
27
|
+
|
28
|
+
initialize_akai(:apc_mini)
|
29
|
+
|
30
|
+
use_bpm 110
|
31
|
+
|
32
|
+
live_loop :drums do
|
33
|
+
loop_rows(4, {
|
34
|
+
1 => -> { sample :drum_bass_soft, amp: fader(0), cutoff: fader(1, (60..127)) },
|
35
|
+
0 => -> { sample :drum_snare_soft, amp: fader(2), cutoff: fader(3, (60..127)) }
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
live_loop :bass, sync: :drums do
|
40
|
+
sample :vinyl_hiss, sustain: 2, attack: 1, release: 1 if switch?(2, 0)
|
41
|
+
|
42
|
+
crd = fader(4, [
|
43
|
+
chord(:c3, :major),
|
44
|
+
chord(:g3, :major),
|
45
|
+
chord(:a3, :minor),
|
46
|
+
chord(:e3, :major)
|
47
|
+
])
|
48
|
+
|
49
|
+
use_synth :blade
|
50
|
+
|
51
|
+
play_chord crd, release: 4, attack: 2, amp: fader(5)
|
52
|
+
|
53
|
+
use_synth :pluck
|
54
|
+
|
55
|
+
with_fx :reverb do |fx|
|
56
|
+
use_random_seed crd.first
|
57
|
+
attach_fader(7, fx, :room)
|
58
|
+
16.times do
|
59
|
+
play crd.choose + 12, release: 0.25, pan: rrand(-0.1, 0.1),
|
60
|
+
amp: fader(6),
|
61
|
+
on: spread(11, 16).tick(:note)
|
62
|
+
sleep 0.25
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/init_dev.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Script to be used in development. Instead of `require '.../init.rb'` from
|
2
|
+
# sonic pi, `load '.../init_dev.rb'` and code will be reloaded on each Alt+R.
|
3
|
+
|
4
|
+
dir = File.join(__dir__, 'lib', 'sonic-pi-akai-apc-mini')
|
5
|
+
|
6
|
+
load "#{dir}/core_extensions/range.rb"
|
7
|
+
load "#{dir}/helpers.rb"
|
8
|
+
load "#{dir}/api.rb"
|
9
|
+
load "#{dir}/controller.rb"
|
10
|
+
|
11
|
+
# bypass the mechanism that avoids hot-changing the model, forcing a reload of
|
12
|
+
# the config on each call:
|
13
|
+
SonicPiAkaiApcMini::Controller.instance_variable_set(:@_model, nil)
|
14
|
+
|
15
|
+
include SonicPiAkaiApcMini::API
|
@@ -1,8 +1,7 @@
|
|
1
1
|
module SonicPiAkaiApcMini
|
2
2
|
module API
|
3
|
-
def switch?(
|
4
|
-
|
5
|
-
!!get("switch_#{n}")
|
3
|
+
def switch?(row, col)
|
4
|
+
!!get("switch_#{Helpers.key(row, col)}")
|
6
5
|
end
|
7
6
|
|
8
7
|
# default `target` is 0-0.999 instead of 0.1 because many parameters have
|
@@ -11,7 +10,9 @@ module SonicPiAkaiApcMini
|
|
11
10
|
# difference so I think it's a good default.
|
12
11
|
def fader(n, target = (0..0.999), _options = {})
|
13
12
|
# TODO: Try to optimize speed, there is some latency because the
|
14
|
-
# controller send a lot of events (too much granularity)
|
13
|
+
# controller send a lot of events (too much granularity). It is in theory
|
14
|
+
# possible to save some of it by `get`ting the value directly instead of
|
15
|
+
# waiting for all the events to be processed.
|
15
16
|
value = get("fader_#{n}", 0)
|
16
17
|
Helpers.normalize(value, target)
|
17
18
|
end
|
@@ -22,42 +23,41 @@ module SonicPiAkaiApcMini
|
|
22
23
|
end
|
23
24
|
|
24
25
|
def loop_rows(duration, rows)
|
25
|
-
first_row = rows.keys.
|
26
|
-
|
26
|
+
first_row = rows.keys.max
|
27
|
+
Controller.model.grid_columns.times do |beat|
|
27
28
|
prev = (beat - 1) % 8
|
28
|
-
|
29
|
-
|
29
|
+
prev_key = Helpers.key(first_row, prev)
|
30
|
+
beat_key = Helpers.key(first_row, beat)
|
31
|
+
midi_note_on prev_key, get("switch_#{prev_key}") ? Controller.model.light_green : Controller.model.light_off
|
32
|
+
midi_note_on beat_key, Controller.model.light_yellow
|
30
33
|
rows.each do |row, sound|
|
31
|
-
in_thread(&sound) if
|
34
|
+
in_thread(&sound) if switch?(row, beat)
|
32
35
|
end
|
33
|
-
sleep duration /
|
36
|
+
sleep duration.to_f / Controller.model.grid_columns
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
37
40
|
def loop_rows_synth(duration, rows, notes, options = {})
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
sleep duration / 8.0
|
47
|
-
end
|
41
|
+
rows = rows.map.with_index do |row, i|
|
42
|
+
[row, lambda do
|
43
|
+
opts = options.respond_to?(:call) ? options.call : options
|
44
|
+
play(notes[i], opts)
|
45
|
+
end]
|
46
|
+
end.to_h
|
47
|
+
loop_rows(duration, rows)
|
48
48
|
end
|
49
49
|
|
50
50
|
def reset_free_play(row, col, size)
|
51
51
|
size = size.size unless size.is_a?(Integer) # so we can pass the same ring
|
52
52
|
Helpers.key_range(row, col, size).each do |key|
|
53
|
-
midi_note_on key,
|
53
|
+
midi_note_on key, Controller.model.light_off
|
54
54
|
set "free_play_#{key}", nil
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
def free_play(row, col, notes, options = {})
|
59
59
|
Helpers.key_range(row, col, notes.size).each.with_index do |key, i|
|
60
|
-
midi_note_on key,
|
60
|
+
midi_note_on key, Controller.model.light_yellow
|
61
61
|
set "free_play_#{key}", notes[i]
|
62
62
|
end
|
63
63
|
|
@@ -80,26 +80,31 @@ module SonicPiAkaiApcMini
|
|
80
80
|
values[get("selector_current_value_#{krange}")]
|
81
81
|
end
|
82
82
|
|
83
|
-
def initialize_akai
|
83
|
+
def initialize_akai(model)
|
84
|
+
Controller.model = model
|
84
85
|
# This loop manages faders. Whenever they change, the new value is stored via set,
|
85
86
|
# and the corresponding light is turned on/off.
|
86
87
|
live_loop :faders do
|
87
88
|
use_real_time
|
88
|
-
n, value = sync(
|
89
|
-
set "fader_#{n -
|
90
|
-
|
91
|
-
|
89
|
+
n, value = sync(Controller.model.midi_event(:control_change))
|
90
|
+
set "fader_#{n - Controller.model.fader_offset}", value
|
91
|
+
if Controller.model.fader_light_offset
|
92
|
+
midi_note_on n + Controller.model.fader_light_offset,
|
93
|
+
value.zero? ? Controller.model.light_off : Controller.model.light_red
|
94
|
+
end
|
95
|
+
if attachment = get("attached_fader_#{n - Controller.model.fader_offset}")
|
92
96
|
normalized_value = Helpers.normalize(value, attachment[:target])
|
93
97
|
control attachment[:node], attachment[:property] => normalized_value
|
94
98
|
end
|
95
99
|
end
|
96
100
|
|
97
|
-
# Manages the buttons in the grid, both as switches and to
|
98
|
-
# pressed, we check if that row is being used
|
99
|
-
#
|
101
|
+
# Manages the buttons in the grid, both as switches, selectors, and to
|
102
|
+
# "free play". Whenever one is pressed, we check if that row is being used
|
103
|
+
# to "free play". If it is, we play. If it's not, we check if its used as
|
104
|
+
# a selector and manage it. Otherwise, we manage it as a switch.
|
100
105
|
live_loop :switches_and_freeplay do
|
101
106
|
use_real_time
|
102
|
-
n, _vel = sync(
|
107
|
+
n, _vel = sync(Controller.model.midi_event(:note_on))
|
103
108
|
if note = get("free_play_#{n}")
|
104
109
|
cue :free_play, note: note, key: n
|
105
110
|
elsif keys = get("selector_keys_#{n}")
|
@@ -117,7 +122,7 @@ module SonicPiAkaiApcMini
|
|
117
122
|
|
118
123
|
live_loop :free_play_note_offs do
|
119
124
|
use_real_time
|
120
|
-
n, _vel = sync(
|
125
|
+
n, _vel = sync(Controller.model.midi_event(:note_off))
|
121
126
|
if note_control = get("free_play_playing_#{n}")
|
122
127
|
release = note_control.args['release'] || note_control.info.arg_defaults[:release]
|
123
128
|
control note_control, amp: 0, amp_slide: release
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module SonicPiAkaiApcMini
|
2
|
+
module Controller
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def model=(model_name)
|
8
|
+
if @_model
|
9
|
+
if @_model.name == model_name
|
10
|
+
return
|
11
|
+
else
|
12
|
+
raise Error, 'Changing the model is not supported. Please restart Sonic Pi and initialize with new model name'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
config = Configs.fetch(model_name.to_sym) { raise Error, "model #{model_name} not supported" }
|
17
|
+
|
18
|
+
@_model = Model.new(config.merge(name: model_name))
|
19
|
+
end
|
20
|
+
|
21
|
+
def model
|
22
|
+
@_model || raise(Error, 'model not initialized')
|
23
|
+
end
|
24
|
+
|
25
|
+
Model = Struct.new(
|
26
|
+
:name, :midi_port,
|
27
|
+
:grid_rows, :grid_columns, :grid_offset,
|
28
|
+
:fader_count, :fader_offset, :fader_light_offset,
|
29
|
+
:light_off, :light_green, :light_red, :light_yellow,
|
30
|
+
keyword_init: true
|
31
|
+
) do
|
32
|
+
def midi_event(event_name)
|
33
|
+
"/midi:#{midi_port}/#{event_name}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Configs = {
|
38
|
+
apc_mini: {
|
39
|
+
midi_port: 'apc_mini*',
|
40
|
+
grid_rows: 8,
|
41
|
+
grid_columns: 8,
|
42
|
+
grid_offset: 0,
|
43
|
+
fader_count: 9,
|
44
|
+
fader_offset: 48,
|
45
|
+
fader_light_offset: 16,
|
46
|
+
light_off: 0,
|
47
|
+
light_green: 1,
|
48
|
+
light_red: 3,
|
49
|
+
light_yellow: 5
|
50
|
+
},
|
51
|
+
# TODO: Some assumptions here, check!
|
52
|
+
apc_key_25: {
|
53
|
+
midi_port: 'apc_key_25*', # TODO: this is just a guess
|
54
|
+
grid_rows: 5,
|
55
|
+
grid_columns: 8,
|
56
|
+
grid_offset: 0,
|
57
|
+
fader_count: 8,
|
58
|
+
fader_offset: 48,
|
59
|
+
fader_light_offset: nil, # no fader lights
|
60
|
+
light_off: 0,
|
61
|
+
light_green: 1,
|
62
|
+
light_red: 3,
|
63
|
+
light_yellow: 5
|
64
|
+
# TODO: handle keyboard
|
65
|
+
}
|
66
|
+
}.freeze
|
67
|
+
end
|
68
|
+
end
|
@@ -2,6 +2,9 @@ module SonicPiAkaiApcMini
|
|
2
2
|
module Helpers
|
3
3
|
module_function
|
4
4
|
|
5
|
+
# Normalizes a MIDI velocity value (between 0 and 127) to the corresponding
|
6
|
+
# value in `target`, which can be a range, a ring/array, or the special
|
7
|
+
# value :pan (shortcut for (-1..1)).
|
5
8
|
def normalize(value, target)
|
6
9
|
value /= 127.0
|
7
10
|
target = (-1..1) if target == :pan
|
@@ -14,10 +17,25 @@ module SonicPiAkaiApcMini
|
|
14
17
|
end
|
15
18
|
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
20
|
+
class RangeError < StandardError; end
|
21
|
+
|
22
|
+
# Given a starting button (row, col) and a size, return the range of
|
23
|
+
# corresponding MIDI notes for those buttons. If `size` is bigger than the
|
24
|
+
# amount of remaining buttons before the end of the row, the range finishes
|
25
|
+
# at the end of the row. If the currently configured model has fewer
|
26
|
+
# rows/columns, it raises a RangeError error.
|
27
|
+
def key_range(row, col, size)
|
28
|
+
raise RangeError, 'out of range' if row >= Controller.model.grid_rows || col >= Controller.model.grid_columns
|
29
|
+
|
30
|
+
first = key(row, col)
|
31
|
+
last = first + size - 1
|
32
|
+
end_of_row = key(row, Controller.model.grid_columns - 1)
|
33
|
+
|
34
|
+
(first..[last, end_of_row].min)
|
35
|
+
end
|
36
|
+
|
37
|
+
def key(row, col)
|
38
|
+
(row * Controller.model.grid_columns) + col + Controller.model.grid_offset
|
21
39
|
end
|
22
40
|
end
|
23
41
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sonic-pi-akai-apc-mini
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergio Gil
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-01-
|
11
|
+
date: 2022-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -28,12 +28,14 @@ files:
|
|
28
28
|
- README.md
|
29
29
|
- Rakefile
|
30
30
|
- akai-apc-mini.jpg
|
31
|
-
- bin/console
|
32
31
|
- bin/setup
|
32
|
+
- example.rb
|
33
33
|
- exe/sonic-pi-akai-apc-mini
|
34
34
|
- init.rb
|
35
|
+
- init_dev.rb
|
35
36
|
- lib/sonic-pi-akai-apc-mini.rb
|
36
37
|
- lib/sonic-pi-akai-apc-mini/api.rb
|
38
|
+
- lib/sonic-pi-akai-apc-mini/controller.rb
|
37
39
|
- lib/sonic-pi-akai-apc-mini/core_extensions/range.rb
|
38
40
|
- lib/sonic-pi-akai-apc-mini/helpers.rb
|
39
41
|
homepage: https://github.com/porras/sonic-pi-akai-apc-mini
|
data/bin/console
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
require 'bundler/setup'
|
3
|
-
require 'sonic/pi/akai/apc/mini'
|
4
|
-
|
5
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
6
|
-
# with your gem easier. You can also use a different console, if you like.
|
7
|
-
|
8
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
9
|
-
# require "pry"
|
10
|
-
# Pry.start
|
11
|
-
|
12
|
-
require 'irb'
|
13
|
-
IRB.start(__FILE__)
|