#midi #convert #encode #redact-composer

redact-composer-midi

redact-composer的MIDI域库和转换器

10个版本

0.1.9 2024年4月28日
0.1.8 2024年4月19日
0.1.5 2024年3月9日
0.1.3 2024年1月18日

#342 in 音频


2 crates 中使用

MIT 协议

165KB
3.5K SLoC

图标 RedACT Composer

docs-badge crates.io-badge ci-badge license-badge

用于构建模块化音乐作曲家的Rust库。

通过创建一组作曲元素,并定义每个元素如何生成进一步子元素来构建作曲家。在这个库的领域中,这些对应于ElementRenderer特质。

本项目遵循语义化版本控制。最重要的是目前的规范项#4



设置

cargo add redact-composer

如果使用serde 功能,还需要typetag

cargo add typetag

示例

可以通过创建一个简单的I-IV-V-I和弦作曲家来演示基本功能。完整的代码示例位于redact-composer/examples/simple.rs

构建块

这个示例作曲家将使用一些库提供的元素(ChordPartPlayNote)和两个新元素

#[derive(Element, Serialize, Deserialize, Debug)]
pub struct CompositionRoot;

#[derive(Element, Serialize, Deserialize, Debug)]
struct PlayChords;

在继续之前,一些背景信息:一个作曲是一个n元树结构,它通过从根Element开始,并调用其关联的Renderer来“组成”,该Renderer生成额外的Element作为子元素。然后调用这些子元素的Renderer,这个过程一直持续到树叶子(即不生成进一步子元素的元素)。

此作曲家将使用 CompositionRoot 元素作为根元素。为该元素定义一个 Renderer 看起来像这样

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(),
        )
    }
}

注意:Part::instrument(...) 仅仅是对另一个元素的包装,表示在包装元素内生成的音符将由单个乐器一次演奏。

Renderer 接收一个 CompositionRoot 元素(通过一个 SegmentRef),并生成多个子元素,包括 Chord 元素(在整首曲子中以每两拍一次的节奏),以及新定义的 PlayChords 元素。这些子元素作为 Segment 返回,它们定义了它们在作品时间线中的位置。

在这个阶段,ChordPlayChords 元素仍然是抽象概念,需要产生具体的东西。这是通过为 PlayChords 创建另一个 Renderer 来完成的。

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)
    }
}

在这里,使用 CompositionContext 来引用之前创建的 Chord 段落。然后在每个八度范围内,从每个 Chord 中播放 Note,并在 Chord 段落的时序上播放。

创建作曲家

本质上,一个 Composer 只是一组 Renderer,可以用一点粘合剂构建。

let composer = Composer::from(
    RenderEngine::new() + CompositionRenderer + PlayChordsRenderer,
);

最后,通过将其根 Segment 传递给其 compose() 方法来展开魔法。

// 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();

当连接到您最喜欢的 MIDI 播放器时,composition.mid 文件应该听起来像这样

https://github.com/dousto/redact-composer/assets/5882189/9928539f-2e15-4049-96ad-f536784ee7a1

此外,作曲输出支持序列化和反序列化(通过 serde 功能,默认启用)。

// Write the composition output in json format
fs::write("./composition.json", serde_json::to_string_pretty(&composition).unwrap()).unwrap();

更大的示例

查看 此存储库 以获取更深入的示例,该示例利用了其他功能来创建完整的作品。

检查器

随着作品越来越大,调试作曲输出可能会变得难以控制。redact-composer-inspector 是一个简单的网页工具,有助于可视化并导航 Composition 输出的结构(目前仅兼容 json 输出)。

例如,这里是在检查器中加载的 简单示例

功能标志

默认

derivemusicalmidiserde

derive 默认

Element 启用 derive 宏。

musical 默认

包含 musical 领域模块。(包括 KeyChordRhythm 等..)。

midi 默认

包含 midi 模块,其中包含与 MIDI 相关的 Element 和用于 Composition 的 MIDI 转换器。

serde 默认

启用通过(正如你可能已经猜到的)serdeComposition 输出的序列化和反序列化。

依赖关系

~2.3–3.5MB
~63K SLoC