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

seamstress support #4

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

seamstress support #4

wants to merge 8 commits into from

Conversation

p3r7
Copy link

@p3r7 p3r7 commented Sep 16, 2023

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

@p3r7
Copy link
Author

p3r7 commented Sep 16, 2023

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 robbielyman/seamstress#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

lib/nb.lua Outdated Show resolved Hide resolved
@p3r7
Copy link
Author

p3r7 commented Nov 14, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant