| Crates.io | redact-composer-midi |
| lib.rs | redact-composer-midi |
| version | 0.1.9 |
| created_at | 2024-01-14 08:04:25.726986+00 |
| updated_at | 2024-04-28 00:33:14.671709+00 |
| description | Midi domain library and converter for redact-composer |
| homepage | |
| repository | https://github.com/dousto/redact-composer |
| max_upload_size | |
| id | 1099262 |
| size | 62,998 |
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 ]
cargo add redact-composer
If using the serde feature, typetag is also required:
cargo add typetag
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.
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.
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();
Check out this repo for a more in depth example which utilizes additional features to create a full length composition.
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.
defaultderive, musical, midi, serde
derive defaultEnable derive macro for Element.
musical defaultInclude musical domain module. (Key, Chord,
Rhythm, etc..).
midi defaultInclude midi module containing MIDI-related Elements and MIDI converter for
Compositions.
serde defaultEnables serialization and deserialization of Composition outputs via (as you may have guessed)
serde.