Common Pipeline

Common Pipeline#

The common pipeline in this repository has five steps. First, a package defines musical content either directly in code or through a mix of code and configuration. Second, that material is assembled into Abjad objects. Third, the Abjad representation is serialized into LilyPond. Fourth, LilyPond compiles the score into PDF and MIDI. Fifth, optional audio rendering converts MIDI into WAV. This last step is not required for score production, but it is part of the normal build path for the finished works and the quartet studies.

The fixed-score packages and the generative packages share the same general output pipeline. They differ mainly in how the musical material is created. modus_operandi_abjad and parts of jazz_rhythm are more direct. The quartet packages are more explicitly generative. They load configuration, turn it into generation rules, create event streams on a quantized grid, and then convert those events into notation.

Reproducibility matters more in the quartet packages than in the fixed-score packages. The quartet generators expose a random seed through configuration. That means a result can be regenerated as long as the code and config stay the same. Output filenames can also include measures, tempo, seed, and timestamp, which makes it easy to track experimental runs.

The quartet audio path is a good example of the pipeline becoming more specialized when the musical needs require it. A single piano SoundFont is not enough for a quartet, and a single orchestral SoundFont does not give the best piano result. The system therefore renders the piano layer and the string layer separately and combines them afterward. The CLI is still simple from the outside, but the internal render path is more careful.

The end of the main quartet CLI shows this in two steps. First, it decides which outputs LilyPond needs to compile:

Output compilation decisions in the first quartet CLI.#
    formats_to_compile = set()
    if args.pdf:
        formats_to_compile.add("pdf")
    if args.midi:
        formats_to_compile.add("midi")
    if args.wav:
        formats_to_compile.add("midi")

    if formats_to_compile:
        compile_lilypond(ly_path, args.output_dir, stem, formats_to_compile)
    elif args.ly:
        print("Done (--ly only, skipping LilyPond compilation).")

Then it decides how WAV rendering should happen once the MIDI file exists:

WAV rendering choices in the first quartet CLI.#
    if args.wav:
        midi_path = os.path.join(args.output_dir, f"{stem}.midi")
        wav_path = os.path.join(args.output_dir, f"{stem}.wav")
        if args.soundfont:
            render_wav(
                midi_path=midi_path,
                wav_path=wav_path,
                soundfont_path=args.soundfont,
                sample_rate=config.render.sample_rate,
            )
        elif config.render.piano_soundfont and config.render.strings_soundfont:
            render_layered_wav(
                midi_path=midi_path,
                wav_path=wav_path,
                piano_soundfont_path=config.render.piano_soundfont,
                strings_soundfont_path=config.render.strings_soundfont,
                sample_rate=config.render.sample_rate,
            )
        elif config.render.soundfont:
            render_wav(
                midi_path=midi_path,
                wav_path=wav_path,
                soundfont_path=config.render.soundfont,
                sample_rate=config.render.sample_rate,
            )
        else:
            raise ValueError(
                "WAV rendering requires --soundfont, [render].soundfont, "
                "or both [render].piano_soundfont and [render].strings_soundfont."
            )