10 releases
| 0.1.9 | Apr 28, 2024 |
|---|---|
| 0.1.8 | Apr 19, 2024 |
| 0.1.5 | Mar 9, 2024 |
| 0.1.3 | Jan 18, 2024 |
#826 in Audio
683 downloads per month
Used in 2 crates
165KB
3.5K
SLoC
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-composer
If 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();
When plugged into your favorite midi player, the composition.mid file should sound somewhat like this:
https://github.com/dousto/redact-composer/assets/5882189/9928539f-2e15-4049-96ad-f536784ee7a1
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, 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.
serde default
Enables serialization and deserialization of Composition outputs via (as you may have guessed)
serde.
Dependencies
~2.1–3MB
~56K SLoC