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 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__)