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

Allow for more control over interaction with system MIDI #59

Open
5 of 53 tasks
Tracked by #52
oubiwann opened this issue Sep 20, 2024 · 3 comments
Open
5 of 53 tasks
Tracked by #52

Allow for more control over interaction with system MIDI #59

oubiwann opened this issue Sep 20, 2024 · 3 comments
Milestone

Comments

@oubiwann
Copy link
Contributor

oubiwann commented Sep 20, 2024

Currently, when a sequence of notes is sent to undermidi to play, there is no way to interrupt that, except for:

  • killing the Erlang process that is scheduling calls (timer:apply_after for notes off) or making the (hacky) timer:sleep calls for pauses between notes, or
  • exiting out of the REPL (killing the OS beam process)

That's not really tenable, long-term.

There are a couple of use cases that are driving this work:

  1. When playing a long sequence of notes or a song, I need to be able to stop it
  2. When playing a long sequence of notes or a song, I need to be able to pause and resume it
  3. I would like to be able to add more sequences and/or songs to what is already getting played

To support these use cases, we need to do a couple of significant refactors:

  • 🚫 Consider using MIDI-based note durations, timings, rests, delays, etc. #60
  • Refactor existing code so that messages are encoded to binary by the top-level devices gen_server
  • Add new features/capabilities to the devices gen_server (sibling to the device manager supervision tree)
    • support the processing of incoming Erlang term MIDI messages concurrently
      • receive {midi, ...} and determine wether to:
      • process immediately (e.g., system messages), or
      • write encoded, binary MIDI messages to time-ordered play queue
    • support receiving command/control Erlang term messages (non-MIDI)
      • start playback
      • pause playback
      • continue playback
      • stop playback
      • next measure
      • previous measure
      • next song/sequence
      • previous song/sequence
      • increment repeat count for current song/sequence
      • delete current song/sequence
      • delete all songs/sequences
  • Add a time-tracking capability
    • support various musico-temporal abstractions, such as:
    • tempo, bpm, beats -- these should be understood by all, managed by the top-level devices gen_server
    • time signatures, measures
      • different devices should be able to play in different time sigs, so this should be device-level
      • however, most will use the same
      • the top-level should be able to set a global time sig
      • if the device-levels don't set a time sig, the top one will be used / assumed
      • measures should support arbitrary voices
    • note and rest durations
      • note duration will be calculated from note type / value and bpm (see uth.note:duration-fn)
      • update uth.note with note and rest names
      • the point at which a note will be played will depend upon what measure it's in and how many rests or notes are in front if it (in its voice array)
  • Add a scheduled dispatcher
    • read from play queue to a specific depth
      • maybe the depth should be configurable
      • select a sane default, possibly one measure
      • the choice should be least surprise, facilitating play/pause/continue
      • e.g.:
        • before every message is sent to the device, check to see if there's been a "transport" action (pause, stop, etc.)
        • send messages at the minimum rate to ensure that:
          • it's possible to pause as soon as possible, and
          • there is no delay or lag between played notes
          • this will be different for every system, so it should be configurable
          • additionally, there should be instructions for developers to tune these config options for their own systems
    • use Erlang NIF to write to system MIDI device(s)
  • Rename undermidi.device.conn to undermidi.device.client
    Related tasks:
  • Don't block when playing #15
  • Add support for playing note timings #17
  • Delete beat tracker
  • Delete timeclock server prototypes
  • Delete liveplayer
  • Delete phraseplayer prototype
@oubiwann oubiwann added this to the 0.3.0 milestone Sep 20, 2024
@oubiwann
Copy link
Contributor Author

oubiwann commented Sep 21, 2024

Copied from #60 after deciding not to use MIDI, and instead contemplate a better Erlang solution:

The Erlang VM is accurate to the millisecond. In 4/4 time a fast song at say 350 bpm would have the following note durations in any given measure:

  • whole note (4 beats) --> 350/60=4/x --> (/ 1 (/ (/ 350 60) 4)) --> 0.69s --> 685.7 ms
  • half note (2 beats) --> 342.9 ms
  • quarter note (1 beat) --> 171.4 ms
  • 8th note --> 85.7 ms
  • 16th note --> 42.9 ms
  • 32nd note --> 21.5 ms
  • 64th note --> 10.7 ms

A 64th note at 350 bpm is insane and probably not something that's going to happen very often, if at all. If it does, then Erlang is probably not the target platform for the developer who needs this capability. That being said, we should test this with actual notes + human ears over time. But we'll need to build something to test, first.

Instead of creating separate services for this, with code running in new Erlang processes in this or some other supervision tree, it would be nice to run a timer + queue at the same place where notes are being sent to the system MIDI device.

However ...

As soon as we need to write synchronised timings to multiple devices simultaneously, we need a little separation from timer-and-sender in the same Erlang process. Two instruments on separate devices should be able to play completely in-time with each other, so whatever is keeping time (and tracking beats, time sig, tempo, measures, etc.) should be able to provide clock into fast enough that the multiple devices can play in-time.

However ...

The Erlang NIF doesn't offer any native concurrency support for writing to system MIDI devices, so having a bunch of processes that request timing info from a central source and then send messages themselves doesn't really match up well with the lowest level dependency. It might make more sense to have a single Erlang process that dispatches to system MIDI. If that's the case, then we probably want something like the following:

  • for every MIDI device a developer wants to send MIDI data to, create a child process (per MIDI device) under the device manager supervision tree (as is done now)
  • instead of sending directly to the MIDI devices from this child process, though, create Erlang term MIDI messages for notes on/off to a central dispatching process
  • the central dispatching process will maintain a queue as well as a clock
  • for each message it receives, it will calculate the time at which the note needs to be played, at millisecond resolution, convert it to MIDI wire protocol binary, and schedule note on/off
  • pausing or stopping playback will cause the queue reader to stop reading Erlang term messages
  • resuming or starting will cause the queue ready to start reading Erlang term messsages

To support this, we would need the following:

  • a new gen_server, sibling to the device manager supervision tree
  • the ability to receive and process incoming Erlang term MIDI messages concurrently
  • a configurable, manageable message queue for the new gen_server
  • abstractions for tempo/bpm, time signatures, measures, beats, note/rest durations
  • a scheduler

@oubiwann
Copy link
Contributor Author

Instead of creating a new gen_server, we might just be able to re-use the current undermidi.devices gen_server? If there's no conflict in what's being managed, and it makes sense to do this ... This gen_server is currently responsible for the following:

  • maintaining an in-memory DB of all devices used (active and otherwise; note that the only field currently being stored in a device row is the currently active MIDI channel)
  • creating a new device Erlang process (calls to the device.supervisor tree)

So I guess the question is: "Do the gen_server's current responsibilities conflict with those of queuing MIDI messages on behalf of all device connections and making time calculations such that those messages get sent at the right time and in the right order?"

Feels like a good fit. Don't think we'd even need to rename from the current devices ...

@oubiwann
Copy link
Contributor Author

Hrm, the architecture proposed in this ticket does call into question the current setup of the supervision tree being used to model MIDI device connections ... at the very least, I think we'll need to rename the worker gen_server (since it will be even less of a real "connection" that before).

If the gen_server is going to take all messages, do time maths on them, encode them, queue them up, and ultimately send them to the OS' underlying MIDI system, does it still make sense for the worker processes (the undermidi.device.supervisor and undermidi.device.conn) to still exist?

Yeah, I think so. It's not only a good model, it's a good implementation (separation of concerns, etc.).

I think we just need to rename undermidi.device.conn to undermidi.device.client and we're good.

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

No branches or pull requests

1 participant