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 |
#394 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 Element
s as children. These children then have their
Renderer
s 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 Segment
s, 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 Note
s from each
Chord
within an octave range are play
ed over the
Chord
segment's timing.
Creating the Composer
In essence, a Composer
is just a set of Renderer
s, 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 Element
s and MIDI converter for
Composition
s.
serde
default
Enables serialization and deserialization of Composition
outputs via (as you may have guessed)
serde
.
Dependencies
~2.4–3.5MB
~63K SLoC