nb icon indicating copy to clipboard operation
nb copied to clipboard

seamstress support

Open p3r7 opened this issue 2 years ago • 2 comments

things seem to work but i haven't done much testing.

p3r7 avatar Sep 16 '23 12:09 p3r7

also i've only included the most basic patches to make things work.


additional feature: hot plugging midi devices

i have a more complex & dirty patch that allows supporting the async registration of midi devices in seamstress. this used to be mandatory but https://github.com/ryleelyman/seamstress/issues/57 simplified things.

still it could be a nice to have to support hot-plugging midi devices into seamstress.

here is the patch for completeness

script.lua:

NB_VOICES = 4

-- sync registration of midi devices into nb
function init()
  nb.voice_count = NB_VOICES
  nb:init()

  -- [...] add standard script params
  for i=1,NB_VOICES do
    nb:add_param("nb_voice_"..i, "nb Voice #"..i)
  end

  nb:add_player_params()
end

-- async registration of midi devices into nb
if seamstress then
  midi.add = function (dev, is_input)
    if not is_input then
      print("new midi out detected ("..dev.name.."), registering into nb")
      nb.add_midi_player_seamstress(dev)
      -- update all
      for i=1,NB_VOICES do
        -- will re-write the definition of the parameter (w/ a new options list, action...)
        nb:add_param("nb_voice_" .. i, "nb Voice #" .. i)
      end
    end
  end
end

nb.lua:


-- ------------------------------------------------------------------------
-- patched fns

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

  local string_param_id = param_id .. "_hidden_string"

  -- THE PATCH IS HERE
  local id = params.lookup[param_id]
  if id then
    -- if we call current fn (`nb:add_param`) on a parameter that already exists, we override its definition w/ a new new one
    params.params[id] = make_param_option(param_id, param_name, names, 1)
  else
    params:add_option(param_id, param_name, names, 1)
    params:add_text(string_param_id, "_hidden string", "")
  end

  params:hide(string_param_id)
  local p = params:lookup_param(param_id)
  -- [...]
end

-- ------------------------------------------------------------------------
-- added fns

-- like `add_midi_players` but for registering the new device `v`
-- registers the new player but also calls `player:add_params()` at the end
function nb.add_midi_player_seamstress(v)
  local i = v.id
  for j = 1, nb.voice_count do
      (function(i, j)
          local conn = midi.connect_output(v.port)
          local player = {
            conn = conn
          }
          function player:add_params()
            params:add_group("midi_voice_" .. i .. '_' .. j, "midi " .. j .. ": " .. abbreviate(v.name), 3)
            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:add_number("midi_bend_range_" .. i .. "_" .. j, "bend range", 1, 48, 12)
            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:modulate_note(note, key, value)
            if key == "pressure" then
              self.conn:key_pressure(note, util.round(value * 127), self:ch())
            end
          end

          function player:pitch_bend(note, amount)
            local bend_range = params:get("midi_bend_range_" .. i .. '_' .. j)
            if amount < -bend_range then
              amount = -bend_range
            end
            if amount > bend_range then
              amount = bend_range
            end
            local normalized = amount / bend_range -- -1 to 1
            local send = util.round(((normalized + 1) / 2) * 16383)
            self.conn:pitchbend(send, 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 = true,
              supports_slew = false,
              note_mod_targets = { "pressure" },
              modulate_description = mod_d
            }
          end

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

-- copy of `ParamSet:add_option` but without the "register in paramset" at the end
local function make_param_option(id, name, options, default)
    local cs = controlspec.new(1, #options, "lin", 1, default, units, 1 / (#options - 1))
  local frm = function(param)
    return options[(type(param) == "table" and param:get() or param)]
  end
  return control.new(id, name, cs, frm)
end

p3r7 avatar Sep 16 '23 13:09 p3r7

so, hum, seamstress' midi APIs got very close to norns' in the meantime and so there isn't that much difference remaining.

this is in a state where this could be squashed & merged now.

w/ this, nb works in seamstress, although only tested w/ midi devices.

it would be possible to package nb voices alongside various sound engines that could be run on a computer alongside seamstress (which would be neato).

p3r7 avatar Nov 14 '23 22:11 p3r7