alda-lang / alda

A music programming language for musicians. :notes:
https://alda.io
Eclipse Public License 2.0
5.59k stars 286 forks source link

Alda 2 dropped support for --history #367

Closed jgkamat closed 3 years ago

jgkamat commented 3 years ago

Description

In previous versions of alda, we had a flag to pass history to the client (or alda append), but this seems to have been dropped in alda 2. The migration guide dosen't seem to mention it at all as well.

Examples

See implementation from before.

Motivation

When using alda play to play notes, sometimes, there are bits that you might want to set but not play (such as a preface which messes around with octaves, tempo, etc). The emacs alda plugin uses --history in order to accurately allow playback of individual notes without audibly playing the preceding notes - without this feature, composing becomes much more complicated than it did before (either micromanaging a preface that does not play notes, or losing the ability to accurately play a section of notes).

Alternatives

Something could probably be hacked with markers (and I'll probably do that for now). However, defining markers outside a part causes alda to refuse to play anything (why?), so this is a poor substitute.

daveyarwood commented 3 years ago

Hi @jgkamat! I'm sorry that I caught you off guard with this. I should have given you a heads up about this breaking change to the way that tooling on top of Alda works in Alda 2.

The --history flag in Alda 1 was a workaround for the fact that the Alda 1 server/worker processes didn't keep track of the state of a score "in progress", which meant that you had to manage the state (history) of the score yourself and send the entire thing along as context each time you wanted to iterate on a score in progress. I was never very happy with that workaround, because it's kind of cumbersome to manage the state of the score on the client side, and it also means that it takes longer and longer for the server to parse, eval and play your score as the score gets longer and longer, because it's having to parse and evaluate the entire history every single time.

This was a big part of my motivation to re-work the architecture as part of the Alda 2 rewrite.

In Alda 2, you can start a REPL server and take note of the port it's running on:

$ alda repl --server
nREPL server started on port 32863 on host localhost - nrepl://localhost:32863

# Also, if I keep that running, I can open another terminal and read this file to see the port:
$ cat .alda-nrepl-port
32863

The Alda REPL server maintains the state of the score, and there is a stable API that you can use to interact with it. Under the hood, it uses the nREPL protocol, but for convenience, alda repl has a --message flag that you can use to send JSON messages, and it will print the response from the server.

Here are some example interactions:

# Play `piano: c8 d e f`
$ alda repl --client --port 32863 --message '{"op": "eval-and-play", "code": "piano: c8 d e f"}'
{"id":"cb814413-780e-4d8d-a7b3-06255d5da340","session":"3b0b89fd-f0ea-44bf-b635-6925d54
cb8a8","status":["done"]}

# Play `g f e d c2` in the context of the current score
$ alda repl --client --port 32863 --message '{"op": "eval-and-play", "code": "g f e d c2"}'
{"id":"0855b6fe-d4b8-481a-be55-197bebc030d7","session":"c9f637c0-4a97-407c-80e9-9ed7b77cf79d","status":["done"]}

# Ask the server for the score text
$ alda repl --client --port 32863 --message '{"op": "score-text"}'
{"id":"cad9500e-1362-4f18-87b4-98a7e06dbc10","session":"98fb07b1-47bb-4d80-9963-0b5976bfe6ef","status":["done"],"text":"piano: c8 d e f\ng f e d c2\n"}

# Reset the REPL state by starting a new score
$ alda repl --client --port 32863 --message '{"op": "new-score"}'
{"id":"311748d9-ef12-4d81-b5f6-31ef196162cd","session":"12f0b657-1632-451a-9fd3-bb800579d422","status":["done"]}

# If we fetch the score text again, we can see that it's blank
$ alda repl --client --port 32863 --message '{"op": "score-text"}'
{"id":"cbbad87e-9347-41ae-8fcd-071179b1cbf1","session":"79e0d413-ec1b-4b06-8668-49b9df87be51","status":["done"],"text":""}

For reference about what other operations are available, what they require and what they return, I've written some REPL server API docs.

I think this section of the Implementing an Alda Library article is also a good reference if you're interested in building tooling on top of Alda that integrates with Alda REPL servers.

You may also find it helpful if you have a look at how I implemented this new workflow in vim-alda and alda-clj.

I'm sorry again that I didn't give you a heads up about this sooner!

jgkamat commented 3 years ago

Hmm, this method does seem nicer, especially on the server side, and it means I won't need to keep passing the history to the server (only when the history file is updated).

Right now there seems to be a 'load' op (which can be used to update the history in the server) - but there dosen't seem to be a way to play sections without appending them to the history. It would be nice to have a 'play' op to do that.

However, this feels like relying on more internal details of the alda implementation - even if it is stable for now, if there is another rewrite later down the road, it would require more changes on the client end.

Is there a particular reason defining a marker outside a part (ie: %marker piano:) is not allowed? If that can be supported, I could just place a marker right before the notes I want to actually play and (I think) get the same result.

daveyarwood commented 3 years ago

Right now there seems to be a 'load' op (which can be used to update the history in the server) - but there dosen't seem to be a way to play sections without appending them to the history. It would be nice to have a 'play' op to do that.

Do you mean like being able to play snippets of Alda code in a separate context, without altering the REPL server state? If so, you could achieve that by providing "connect" and "disconnect" functions to the user. When disconnected, you would simply pipe isolated code into alda play instead of sending messages to the REPL server. This is the approach that I used in vim-alda and alda-clj. The "play" operation first checks to see if you are connected, and if so, it sends a message to the REPL server, otherwise it uses alda play.

Another idea is that you could provide a "new score" function that sends a new-score command to the REPL server, which resets the state. This is useful because you can feel free to eval and play whatever code you want without thinking too much about the state of the REPL server, and then whenever you want to ensure a clean state, you can call "new score" to reset the state. This is also something that I did in both vim-alda and alda-clj.

Is there a particular reason defining a marker outside a part (ie: %marker piano:) is not allowed? If that can be supported, I could just place a marker right before the notes I want to actually play and (I think) get the same result.

The issue here is that markers are placed at an offset, which is a point in time defined as a number of milliseconds into the score. When you place a marker like %foo, the marker foo is recorded at the point in time where the current part(s) is. For example, in this score:

trumpet: c d e f %foo g
trombone: @foo c

The trumpet part plays four quarter notes, which, at the default tempo of 120 BPM, takes 2000 ms. The marker foo is therefore placed at an offset of 2000 ms into the score.

The trombone part starts with a reference to the foo marker, which sets the current offset of the trombone part to 2000 ms, and so the c note in the trombone part occurs at 2000 ms, in sync with the g in the trumpet part.

daveyarwood commented 3 years ago

@jgkamat I'm closing this issue for now, but please let me know if you need any help working through how to update alda-mode to work with Alda 2! I'd be happy to talk through it on Slack or Zoom, if that would be helpful.

jgkamat commented 3 years ago

I think the documentation should be sufficient for this, I just need to block some time to work on this. The main difficulty isn't actually supporting this, but doing so in a way that dosen't break Alda 1 completely or cause commands to block on alda version.

I don't particularly enjoy using/supporting nonfree software, so I'll pass on the offer.