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. The bird packages add a third path. bird_im_migration derives material from curated spectral data, while bird_im_migration_ensemble takes those extracted phrases and recombines them with transformations and instrumental assignment. The same phrase library now supports both the main ensemble movements and a final appendix-style study that demonstrates single transforms, repeated augmentation, transform pairs, and longer transform chains on each curated bird region.

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 ensemble bird piece pushes this a bit further by separating piano hands, melodic ensemble, and percussion into different rendering layers before recombination. Its appendix study reuses the same transformed bird-note material, but renders it as a single treble staff so the transformations themselves can be inspected more directly.

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