sonic-pi-akai-apc-mini 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f1ebf42972f8a9b3d2906047bbbeb2292a1a1a4d8f0ba4de4e74eb809b0c6ea
4
- data.tar.gz: 03ab06c2fefd7b6741a303374073404d1b065213734e185b3b0da95a29beda9e
3
+ metadata.gz: bd279db408994ddf67942a34f1576feb382b7fedafdcaeeeb227f447b6b1073c
4
+ data.tar.gz: 628d1d73459b6e01f7554317e53db308967b17753cc94c2e48845d9c909bd485
5
5
  SHA512:
6
- metadata.gz: 3868b1f5b216126776a0d388eecddc36992d4354c22d3fb519061b59e26dcc0219acae3e1b388102f89ab8dacdbb476bfa11c41dcf8733e0db054fefac8c1043
7
- data.tar.gz: d4f0b4c643f33c5e41705376b40beb68eb58ae8801f4e3db006c660128a301637f474cfb26aa0911bc0cd7ef38342e29f233ab1a6add6c98bc87ef0ba4e3415b
6
+ metadata.gz: dd0673065b74993754899a449a1129ecfcce6b252fb93834ac8be33d148cff04f13a1361417ee9682117cd4bf9a41cd8d4d89a83347479335dcadbaaa9d99d35
7
+ data.tar.gz: 20d6cc379e57eda32090cd3cae0b70fab1c18946d9f17ffe79cbb34eac12d7aa556eee4065c5abd28fdd2ce7cc87a5ca5648a610c37b24a1da0cdd6f72069eee
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sonic-pi-akai-apc-mini (0.1.0)
4
+ sonic-pi-akai-apc-mini (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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
- First of all, call `initialize_akai` at the top of your buffer. That will make all the features available.
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 (they are 0-8, left to right). `target-values` is the range of values the fader will map to (and defaults to `(0..1)`\*). Some examples:
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?(line, col)
4
- n = (line * 8) + col
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.first
26
- 8.times do |beat|
26
+ first_row = rows.keys.max
27
+ Controller.model.grid_columns.times do |beat|
27
28
  prev = (beat - 1) % 8
28
- midi_note_on (first_row * 8) + prev, get("switch_#{(first_row * 8) + prev}") ? 1 : 0
29
- midi_note_on (first_row * 8) + beat, 5
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 get("switch_#{(row * 8) + beat}")
34
+ in_thread(&sound) if switch?(row, beat)
32
35
  end
33
- sleep duration / 8.0
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
- 8.times do |beat|
39
- prev = (beat - 1) % 8
40
- midi_note_on (rows.first * 8) + prev, get("switch_#{(rows.first * 8) + prev}") ? 1 : 0
41
- midi_note_on (rows.first * 8) + beat, 5
42
- rows.each.with_index do |row, index|
43
- opts = options.respond_to?(:call) ? options.call : options
44
- play(notes[index], opts) if get("switch_#{(row * 8) + beat}")
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, 0
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, 5
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('/midi:apc_mini_apc_mini_midi_1_20_0:1/control_change')
89
- set "fader_#{n - 48}", value
90
- midi_note_on n - 48 + 64, value.zero? ? 0 : 1
91
- if attachment = get("attached_fader_#{n - 48}")
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 "free play". Whenever one is,
98
- # pressed, we check if that row is being used to "free play". If it is, we play. If it's
99
- # not, we manage it as a switch.
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('/midi:apc_mini_apc_mini_midi_1_20_0:1/note_on')
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('/midi:apc_mini_apc_mini_midi_1_20_0:1/note_off')
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
- def key_range(row, col, max_size)
18
- first = (row * 8) + col
19
- last = [(row * 8) + 7, first + max_size - 1].min
20
- (first..last)
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
@@ -1,3 +1,4 @@
1
1
  require_relative './sonic-pi-akai-apc-mini/core_extensions/range'
2
2
  require_relative './sonic-pi-akai-apc-mini/helpers'
3
3
  require_relative './sonic-pi-akai-apc-mini/api'
4
+ require_relative './sonic-pi-akai-apc-mini/controller'
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.1.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-09 00:00:00.000000000 Z
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__)