alda-lang / alda

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

Export to WAV, MP3 #342

Closed daveyarwood closed 1 year ago

daveyarwood commented 7 years ago

Moved from https://github.com/alda-lang/alda/issues/62.

See the original issue for some ideas on how to do this.

daveyarwood commented 7 years ago

The AudioSynthesizer class looks promising.

There are various Midi2WavRenderer implementations out there that use it to render a Sequence to an audio stream.

How we would integrate this with other (non-MIDI) types of Alda instruments in the future is an open question, but in theory, it seems like it ought to be possible to render multiple audio streams (MIDI and non-MIDI) and combine them into a single audio file.

Another open question is whether we will be limited to the Sun Java runtime if we go this route. If so, I think it might be worth it.

daveyarwood commented 7 years ago

If we are limited to the Sun Java runtime, maybe one way we could make things easier for the end user would be to bundle the Java runtime into the executable.

jgkamat commented 6 years ago

I really think it's rediculous that alda dosen't have any form of export, and a bunch of my friends needed alda to have an export feature for a hackathon project, so I spent a bunch of time messing with this. Right now it's terrible quality (which is why it's not in a PR) and not even close to completion, but it does work, somewhat.

I'll try to encode everything that I learned here, since resources on java midi export are incredibly rare (?!?!?)

Very basic googling will quickly center to this page, which describes the method java uses for midi recording and export. It's a little tough to understand, but it's better than the API (although still not great). Any "MidiDevice" has support for both 'receiving' and transmitting' audio, if set up correctly. Two relevant subclasses of MidiDevice are Sequencer, which is able to write and read from a sequence, the "official" method of storing midi "scores" in java. The second is the Synthesizer, which is capable of producing sounds when written to or when commanded manually via function calls.

The Midi Wave exports mentioned before all work with sequences (they read midi files from disk into a Sequence) so I wanted to re-direct the Synthesizer to 'transmit' into a Sequencer 'receiver'. However, when you try to do this, you get a MidiUnavailableException, which lead me to a dead end (no one seems to have overcome this, there are several SO posts with no answers with the same problem as me). While I haven't looked at the source, the 'getMaxTransmitters' function returns 0 on a Synthesizer, leading me to believe this is route is impossible (if someone knows how to do this, please let me know).

Because of that, I decided to go the second route, which is building Midi Sequences from alda scores manually, then writing those scores to disk. I'm actually a little bit confused at the design for this system. It seems to be scheduling every note start and note end via a callback (via jsyn), and using those callbacks to call functions on the midi Synthesizer. Isn't this fragile (and can lead to timing problems?) From what I can tell, java highly recommends building a sequence if timing is important.

This web page was incredibly helpful during this process, I probably couldn't have done anything without it.

Essentially all I'm doing is looping through all the events in a score, and sending corresponding notestart and noteend events (instead of registering callbacks) into a sequencer. Finally, I tell the sequencer to record what I'm transmitting, and it builds a midi sequence for me! Writing it out to a file then is fairly easy, and converting it to a wav is just one more step which should be easy (I didn't do it for now, since I was able to just launch another java process to do that for me).

One of the problems with this method is that (because I didn't understand how instruments work) all the events are played with the piano. However, I think this should be pretty easy to add with a bit more playing around.

Also because I was super crunched on time, it encodes midi's whenever it plays, to a hardcoded file, but that's not really the point of this :P.

I don't know if there's another way to do this 'cleanly'. I really despise listening to audio out (since recording much be done in realtime, and it feels really hacky), so I think that midi export should be a feature in alda even if jsyn features are added (for pure midi tracks). I couldn't find any way to write jsyn out to a file, so I don't know how much luck we'll have with that.

Would you like this to be developed further (because it does subvert the core internals of the alda player), or do you think there's a better way to do it? If you think it's good approach, I'll clean it up (eventually......) and make a PR with proper integration in the client/server as well (but this will probably take a while).

Just to prove it works, here's a midi (piano only, as mentioned before) of my favorite alda score ever!

potforanything.zip

daveyarwood commented 6 years ago

@jgkamat Wow! Thanks a lot for digging into how all this stuff works. I think this is a great first step for this feature.

I took a brief look at the code on your branch, and in general I think it looks like the right direction to go in. It's very encouraging that you were able to get something roughly working already!

Some quick comments:


So, at this point, I can think of a few possible next steps:

jgkamat commented 6 years ago

Thanks! :smile:

We are using a library called JSyn to do high-precision audio scheduling, which is why using a Synthesizer works for us without running into note timing issues. I ended up doing it this way because JSyn gives you a highly precise, general purpose event scheduling system.

Ah that makes a lot more sense now! I think it's fine to leave the existing JSyn scheduling system (since it's more extensible) unless timing issues are found (which they havent). It's extremely trivial to get a Sequence played back via java midi, so maybe I'll add a switch to choose which one to play back from (once this is done).

I think it would be worthwhile for us to explore / experiment with using a Sequencer to do all of the scheduling. I'm curious if there is some kind of custom MIDI message we could send that we could use to schedule non-MIDI note on/off events when performing a score that uses non-MIDI instruments.

I really doubt this is possible, which is why I think that we should probably stay with JSyn as a primary. Making a sequence is essentially just writing a MIDI file out by hand, so encoding custom things would probably not be possible (unless they are in the midi file format). You can take a look at ShortMessage to see the 'official' things that are supported. I think writing raw MIDI control codes works as well.

Is it really the case that to record a sequence, you have to wait the length of the sequence?

Nope, this isn't tied to playback at all, and saving to midi is incredibly fast! The song still plays afterwards (because it's a hack), but the MIDI is fully exported before the song even begins playing. For longer songs I do notice a delay (I'm not sure what the most expensive part is), but it's nowhere near the length of the song.

I think that exporting a score should be a separate action from playing it. I'm imagining running a command something like alda export -f myscore.alda -o midi > myscore.mid

Nitpick: I really don't think we should output to stdout by default (since the target is non-programmers, and exporting is something everyone needs to do). Running without the redirect would cause binary to be spit out, which is annoying. I would feel a lot more comfortable with a alda export --input myscore.alda --output myscore.mid --format midi (the flags don't matter so much), and adding a flag to output to stdout. That's a pretty minor detail though, and we can figure it out (and maybe have a poll) later.

On Next Steps:

I think replacing the JSyn Scheduling might be too big of a change for something this untested (and obscure) and it's not really needed at first. I'll (try to) implement a Sequencer+Synthesizer playback mechanism, and make it possible to switch between them with a constant flag. That way we can test out the Midi export easily, and still have the existing method (in case it's useful for non-midi audio). Later on, we can always decide to remove one.

I don't think custom events is possible with Sequencer (but I definitely could be wrong), which is one of the reasons I don't want to touch the existing playback code for now.

So my priority will be figuring out how to encode different instruments into midi (since everything else hinges on that), and once that's done, the rest should be fairly straightforward! :D

daveyarwood commented 6 years ago

That all sounds totally reasonable to me!

Thanks again for looking into this. Feel free to ping me here or on Slack if you want to discuss things further!

nblumoe commented 5 years ago

There is mp3 and wav export in https://github.com/oakes/edna, which is based on alda: https://github.com/oakes/edna/blob/ceb9ebd9b2d05169e008793a57b5bca64603cbb7/src/edna/core.clj#L201

dirkk0 commented 2 years ago

A different approach might be the conversion to midi, and a subsequent conversion to wav/mp3.

I just tested this and it works:

alda export -f test.alda -o test.mid

# brew install fluidsynth sox
fluidsynth sf.sf2 test.mid -F raw_audio # -g 1.9
sox -t raw -r 44100 -e signed -b 16 -c 2 raw_audio out_midi.wav
daveyarwood commented 2 years ago

That's great! I still think it would be useful if Alda had a "batteries included" way of exporting a score to WAV or MP3 built into Alda itself, but it's awesome that there is a workflow that folks can use in the meantime. I'll probably use this, myself.

fwindpeak commented 1 year ago

A different approach might be the conversion to midi, and a subsequent conversion to wav/mp3.

I just tested this and it works:

alda export -f test.alda -o test.mid

# brew install fluidsynth sox
fluidsynth sf.sf2 test.mid -F raw_audio # -g 1.9
sox -t raw -r 44100 -e signed -b 16 -c 2 raw_audio out_midi.wav

Thank you! I found that the file output with fluidsynth plus the extension is a playable file.

Tested on a mac.

fluidsynth ~/.gervill/soundbank-emg.sf2  helloworld.mid -F helloword.wav

Supported file types:

$ fluidsynth -T help
-T options (audio file type):
   'aiff','au','auto','avr','caf','flac','htk','iff','mat','mpc','oga','paf','pvf','raw','rf64','sd2','sds','sf','voc','w64','wav','wve','xi'

auto: Determine type from file name extension, defaults to "wav"