Expand description
§
RedACT Composer
A Rust library for building modular musical composers.
Composers are built by creating a set of composition elements, and defining how each of these elements will generate
further sub-elements. In this library’s domain, these correspond to the
Element and Renderer traits respectively.
This project adheres to Semantic Versioning. Most importantly at this time would be spec item #4.
Jump to: [ Setup | Example | Bigger Example | Inspector | Feature Flags ]
§Setup
cargo add redact-composerIf using the serde feature, typetag is also required:
cargo add typetag
§Example
The basic capabilities can be demonstrated by creating a simple I-IV-V-I chord composer. The full code example is
located at
redact-composer/examples/simple.rs.
§Building Blocks
This example composer will use some library-provided elements (Chord,
Part, PlayNote) and two new elements:
#[derive(Element, Serialize, Deserialize, Debug)]
pub struct CompositionRoot;
#[derive(Element, Serialize, Deserialize, Debug)]
struct PlayChords;Before moving ahead, some background: A composition is an n-ary tree structure and is “composed” by starting with a
root Element, and calling its associated Renderer which
generates additional Elements as children. These children then have their
Renderers called, and this process continues until tree leaves are reached (i.e. elements that do
not generate further children).
This composer will use the CompositionRoot element as a root. Defining a Renderer for this
then looks like:
struct CompositionRenderer;
impl Renderer for CompositionRenderer {
type Element = CompositionRoot;
fn render(
&self, composition: SegmentRef<CompositionRoot>, context: CompositionContext,
) -> Result<Vec<Segment>> {
let chords: [Chord; 4] = [
(C, maj).into(),
(F, maj).into(),
(G, maj).into(),
(C, maj).into(),
];
Ok(
// Repeat the four chords over the composition -- one every two beats
Rhythm::from([2 * context.beat_length()])
.iter_over(composition)
.zip(chords.into_iter().cycle())
.map(|(subdivision, chord)| chord.over(subdivision))
.chain([
// Also include the new component, spanning the whole composition
Part::instrument(PlayChords).over(composition),
])
.collect(),
)
}
}Note:
Part::instrument(...)is just a wrapper for another element, indicating that notes generated within the wrapped element are to be played by a single instrument at a time.
This Renderer takes a CompositionRoot element (via a SegmentRef) and generates several
children including Chord elements (with a Rhythm of one every two beats over the composition), and
newly defined PlayChords element. These children are returned as Segments, which defines where they
are located in the composition’s timeline.
At this stage, the Chord and PlayChords elements are just abstract concepts
however, and need to produce something concrete. This is done with another Renderer for
PlayChords:
struct PlayChordsRenderer;
impl Renderer for PlayChordsRenderer {
type Element = PlayChords;
fn render(
&self, play_chords: SegmentRef<PlayChords>, context: CompositionContext,
) -> Result<Vec<Segment>> {
// `CompositionContext` enables finding previously rendered elements
let chord_segments = context.find::<Chord>()
.with_timing(Within, play_chords)
.require_all()?;
// As well as random number generation
let mut rng = context.rng();
// Map Chord notes to PlayNote elements, forming a triad
let notes = chord_segments
.iter()
.flat_map(|chord| {
chord.element
.iter_notes_in_range(Note::from((C, 4))..Note::from((C, 5)))
.map(|note|
// Add subtle nuance striking the notes with different velocities
note.play(rng.gen_range(80..110) /* velocity */)
.over(chord))
.collect::<Vec<_>>()
})
.collect();
Ok(notes)
}
}Here, CompositionContext is used to reference the previously created
Chord segments. Then the Notes from each
Chord within an octave range are played over the
Chord segment’s timing.
§Creating the Composer
In essence, a Composer is just a set of Renderers, and can be constructed with
just a little bit of glue:
let composer = Composer::from(
RenderEngine::new() + CompositionRenderer + PlayChordsRenderer,
);And finally the magic unfolds by passing a root Segment to its
compose() method.
// Create a 16-beat length composition
let composition_length = composer.options.ticks_per_beat * 16;
let composition = composer.compose(CompositionRoot.over(0..composition_length));
// Convert it to a MIDI file and save it
MidiConverter::convert(&composition).save("./composition.mid").unwrap();
// And/or synthesize it to audio with a SoundFont
let synth = SF2Synthesizer::new("./sounds/sound_font.sf2").unwrap();
synth.synthesize(&composition).to_file("./composition.wav").unwrap();Note:
SF2Synthesizerdoes not have any default/embedded SoundFont so you’ll have to supply your own. (FluidR3, created by Frank Wen, is a great general-purpose, high-quality, MIT licensed option)
The output should sound somewhat like this:
https://github.com/dousto/redact-composer/assets/5882189/aeed4e7a-5543-4cf1-839d-d5f62c55fea9
Additionally, composition outputs support serialization/deserialization (with serde feature, enabled by default).
// Write the composition output in json format
fs::write("./composition.json", serde_json::to_string_pretty(&composition).unwrap()).unwrap();
§Much bigger example
Check out this repo for a more in depth example which utilizes additional features to create a full length composition.
§Inspector
Debugging composition outputs can quickly get unwieldy with larger compositions.
redact-composer-inspector is a simple web tool that helps to
visualize and navigate the structure of Composition outputs (currently only compatible with
json output).
For example, here is the simple example loaded in the inspector.
§Feature Flags
§default
derive, musical, midi, synthesis, serde
§derive default
Enable derive macro for Element.
§musical default
Include musical domain module. (Key, Chord,
Rhythm, etc..).
§midi default
Include midi module containing MIDI-related Elements and MIDI converter for
Compositions.
§synthesis default
Include synthesis module to synthesize Compositions into audio.
§serde default
Enables serialization and deserialization of Composition outputs via (as you may have guessed)
serde.
Modules§
- elements
- Core types implementing
Element. - error
- Error types.
- midi
feature = midi (default)MIDI-related types,Elements andCompositionoutput converter.- musical
feature = musical (default)Musical domain library.- render
- Types and traits used for and during composition rendering.
- synthesis
feature = synthesis (default)Audio synthesis utilities.- timing
- Timing related structs and elements.
- util
- Utility traits and types.
Structs§
- Composer
- Provides methods to create compositions using a
RenderEngineand itsRenderers. - Composer
Options - Options used by a
Composer. - Composition
- A composition output, including the tree of rendered segments, produced from
Composer::compose. - Composition
Options - Options used during the rendering of a
Composition. - Segment
- A (type-erased)
Elementspanning aTiminginterval. - Segment
Ref - A typed view of a
Segment(references to its fields). - Timing
- A start-inclusive, end-exclusive
i32range (likeRange<i32>) that is copyable, and implements several utility methods.
Traits§
- Element
- Marker trait for any type that will be used as a composition element.
- Renderer
- Defines render behavior for a specific
Element.
Functions§
- renderers
- Default renderers for
midielements ifmidifeature is enabled (default). Otherwise, just an emptyRenderEngine.
Derive Macros§
- Element
feature = derive (default)Derives aredact-composerElementimpl for this type.