3 Commits
1.0.0 ... main

Author SHA1 Message Date
944a33eb95 1.1.0 - TOML 2025-06-23 23:54:23 -06:00
8d86d1cbfb fix the example 2025-06-23 23:32:32 -06:00
e695c67363 Several fixes and new example 2025-06-23 23:26:42 -06:00
14 changed files with 1168 additions and 1089 deletions

37
Cargo.lock generated
View File

@ -684,7 +684,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "musicgen"
version = "1.0.0"
version = "1.1.0"
dependencies = [
"clap",
"cpal 0.16.0",
@ -693,7 +693,7 @@ dependencies = [
"rand",
"rodio",
"serde",
"serde_json",
"toml",
]
[[package]]
@ -1148,6 +1148,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -1265,11 +1274,26 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@ -1278,10 +1302,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "unicode-ident"
version = "1.0.18"

View File

@ -1,6 +1,6 @@
[package]
name = "musicgen"
version = "1.0.0"
version = "1.1.0"
edition = "2024"
authors = ["Atridad Lahiji <me@atri.dad>"]
description = "Generate electronic music without AI"
@ -15,7 +15,7 @@ hound = "3.5"
rand = "0.8"
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
# Real-time audio
rodio = "0.20"

147
README.md
View File

@ -14,47 +14,45 @@ cd music
cargo build --release
```
## JSON Configuration
## TOML Configuration
Create declaritive songs with structured JSON:
Create declaritive songs with structured TOML:
```bash
cargo run --bin musicgen json examples/fantasy.json
cargo run --bin musicgen toml examples/enchanted.toml
```
### From Binary
```bash
./target/release/musicgen json examples/fantasy.json
./target/release/musicgen toml examples/enchanted.toml
```
### Basic JSON Structure
### Basic TOML Structure
```json
{
"composition": {
"key": 60,
"scale": "minor",
"tempo": 85.0,
"measures": 16
},
"tracks": [
{
"name": "bass",
"instrument": "sine",
"volume": 0.8,
"pattern": {
"type": "custom",
"steps": [
{ "time": 0.0, "note": "C2", "duration": 0.75, "velocity": 0.9 }
]
}
}
],
"export": {
"filename": "my_song",
"format": "wav"
}
}
```toml
[composition]
key = 60
scale = "minor"
tempo = 85.0
measures = 16
[[tracks]]
name = "bass"
instrument = "sine"
volume = 0.8
[tracks.pattern]
type = "custom"
[[tracks.pattern.steps]]
time = 0.0
note = "C2"
duration = 0.75
velocity = 0.9
[export]
filename = "my_song"
format = "wav"
```
### Definitions
@ -77,7 +75,7 @@ cargo run --bin musicgen json examples/fantasy.json
### Available Options
- **Instruments**: `sine`, `square`, `sawtooth`, `triangle`, `noise`
- **Instruments**: `sine`, `square`, `sawtooth`, `triangle`, `noise`, `piano`
- **Scales**: `major`, `minor`, `dorian`, `pentatonic`, `blues`, `chromatic`
- **Keys**: MIDI numbers (60 = C4) or note names (`"C4"`, `"F#3"`)
- **Pattern Types**: `custom`, `chord`, `arpeggio`, `sequence`
@ -85,46 +83,55 @@ cargo run --bin musicgen json examples/fantasy.json
### Track Configuration
```json
{
"tracks": [
{
"name": "bass",
"instrument": "sine",
"volume": 0.8,
"pattern": {
"type": "custom",
"steps": [
{ "time": 0.0, "note": "C2", "duration": 0.75, "velocity": 0.9 },
{ "time": 2.0, "note": "G2", "duration": 0.75, "velocity": 0.8 }
],
"loop_length": 4.0
},
"effects": [
{
"type": "lowpass",
"cutoff": 400.0,
"resonance": 1.8
}
]
},
{
"name": "drums",
"instrument": "noise",
"volume": 0.6,
"pattern": {
"type": "custom",
"steps": [
{ "time": 0.0, "note": "C1", "duration": 0.1, "velocity": 1.0 },
{ "time": 1.0, "note": "E3", "duration": 0.05, "velocity": 0.7 }
]
}
}
]
}
```toml
[[tracks]]
name = "bass"
instrument = "sine"
volume = 0.8
[tracks.pattern]
type = "custom"
loop_length = 4.0
[[tracks.pattern.steps]]
time = 0.0
note = "C2"
duration = 0.75
velocity = 0.9
[[tracks.pattern.steps]]
time = 2.0
note = "G2"
duration = 0.75
velocity = 0.8
[[tracks.effects]]
type = "lowpass"
cutoff = 400.0
resonance = 1.8
[[tracks]]
name = "drums"
instrument = "noise"
volume = 0.6
[tracks.pattern]
type = "custom"
[[tracks.pattern.steps]]
time = 0.0
note = "C1"
duration = 0.1
velocity = 1.0
[[tracks.pattern.steps]]
time = 1.0
note = "E3"
duration = 0.05
velocity = 0.7
```
See `examples` for complete examples and the full schema.
See `composition-schema` for a complete example and the full schema.
## Output

385
composition-schema.toml Normal file
View File

@ -0,0 +1,385 @@
# MusicGen TOML Configuration Schema
# Complete reference for all available fields and options
# ============================================================================
# METADATA SECTION
# Optional information about your composition
# ============================================================================
[metadata]
title = "My Composition" # Title of the track
artist = "Artist Name" # Your name or band name
description = "An electronic song" # Description of the composition
tags = ["electronic", "ambient", "chill"] # Genre tags or keywords
# ============================================================================
# COMPOSITION SECTION
# Core musical settings that define the overall structure
# ============================================================================
[composition]
# Musical key - can be MIDI note number (60 = C4) or note name
key = "C4" # Options: MIDI numbers 0-127 OR note names like "C4", "F#3", "Bb5"
# Scale type - defines which notes are used
scale = "major" # Options: "major", "minor", "dorian", "phrygian", "lydian",
# "mixolydian", "aeolian", "locrian", "pentatonic",
# "blues", "chromatic"
# Tempo in beats per minute
tempo = 120.0 # Range: 40.0 - 300.0 BPM
# Time signature - how many beats per measure
[composition.time_signature]
numerator = 4 # Beats per measure (1-16)
denominator = 4 # Note value for one beat (2, 4, 8, or 16)
# Composition length and complexity
measures = 32 # Number of measures (1-1000)
complexity = 0.7 # Overall complexity (0.0-1.0, default: 0.6)
harmony_density = 0.8 # How much harmony/chords (0.0-1.0, default: 0.7)
rhythmic_density = 0.6 # How busy the rhythm is (0.0-1.0, default: 0.8)
seed = 12345 # Random seed for reproducible generation (optional)
# ============================================================================
# TRACKS SECTION
# Each track is like one instrument in your band
# ============================================================================
# Example 1: Custom melody track
[[tracks]]
name = "main_melody"
instrument = "piano" # Options: "sine", "square", "sawtooth", "triangle", "noise", "piano"
volume = 0.8 # Track volume (0.0-1.0)
[tracks.pattern]
type = "custom" # Pattern type: "custom", "chord", "arpeggio", "sequence"
loop_length = 16.0 # How long before pattern repeats (in beats)
# Individual notes in the pattern
[[tracks.pattern.steps]]
time = 0.0 # When to play (in beats from start of loop)
note = "C5" # MIDI number or note name
duration = 1.5 # How long to hold the note (in beats)
velocity = 0.7 # How loud to play this note (0.0-1.0)
[[tracks.pattern.steps]]
time = 2.0
note = "E5"
duration = 1.0
velocity = 0.6
[[tracks.pattern.steps]]
time = 4.0
note = "G5"
duration = 2.0
velocity = 0.8
# Effects for this track
[[tracks.effects]]
type = "reverb"
room_size = 0.7 # Size of the reverb space (0.0-1.0)
damping = 0.5 # How much high frequencies are absorbed (0.0-1.0)
mix = 0.3 # How much effect to blend in (0.0-1.0)
[[tracks.effects]]
type = "delay"
time = 0.25 # Delay time in seconds (0.01-2.0)
feedback = 0.3 # How much delay repeats (0.0-0.95)
mix = 0.2 # How much effect to blend in (0.0-1.0)
# Example 2: Chord progression track
[[tracks]]
name = "harmony"
instrument = "sawtooth"
volume = 0.6
[tracks.pattern]
type = "chord"
loop_length = 16.0
voicing = "spread" # Options: "close", "spread", "drop2", "drop3"
octave = 3 # Which octave to play chords in (0-8)
# Chord progression
[[tracks.pattern.chord_progression]]
time = 0.0
chord = "Cmaj7" # Chord name (e.g., "C", "Am", "F#dim", "Bbmaj7")
duration = 4.0
[[tracks.pattern.chord_progression]]
time = 4.0
chord = "Am7"
duration = 4.0
[[tracks.pattern.chord_progression]]
time = 8.0
chord = "Fmaj7"
duration = 4.0
[[tracks.pattern.chord_progression]]
time = 12.0
chord = "G7"
duration = 4.0
# Effects for chord track
[[tracks.effects]]
type = "lowpass"
cutoff = 2000.0 # Cutoff frequency in Hz (20-20000)
resonance = 1.2 # Filter resonance (0.1-10.0)
# Example 3: Bass track
[[tracks]]
name = "bass"
instrument = "sine"
volume = 0.9
[tracks.pattern]
type = "custom"
loop_length = 8.0
[[tracks.pattern.steps]]
time = 0.0
note = "C2"
duration = 1.0
velocity = 0.9
[[tracks.pattern.steps]]
time = 2.0
note = "G2"
duration = 1.0
velocity = 0.8
[[tracks.pattern.steps]]
time = 4.0
note = "A2"
duration = 1.0
velocity = 0.8
[[tracks.pattern.steps]]
time = 6.0
note = "F2"
duration = 1.0
velocity = 0.9
# Bass effects
[[tracks.effects]]
type = "highpass"
cutoff = 80.0 # Remove very low frequencies
resonance = 0.7
# Example 4: Percussion track
[[tracks]]
name = "drums"
instrument = "noise"
volume = 0.7
[tracks.pattern]
type = "custom"
loop_length = 4.0
# Kick drum
[[tracks.pattern.steps]]
time = 0.0
note = "C1" # Low note for kick
duration = 0.1
velocity = 1.0
[[tracks.pattern.steps]]
time = 2.0
note = "C1"
duration = 0.1
velocity = 1.0
# Snare/hi-hat
[[tracks.pattern.steps]]
time = 1.0
note = "D4" # Higher note for snare
duration = 0.05
velocity = 0.7
[[tracks.pattern.steps]]
time = 3.0
note = "D4"
duration = 0.05
velocity = 0.7
# Drum effects
[[tracks.effects]]
type = "distortion"
drive = 1.5 # Amount of distortion (1.0-10.0)
tone = 0.6 # Tone control (0.0-1.0)
[[tracks.effects]]
type = "chorus"
rate = 1.2 # Modulation rate in Hz (0.1-10.0)
depth = 0.4 # Modulation depth (0.0-1.0)
layers = 3 # Number of chorus layers (1-8)
mix = 0.3
# ============================================================================
# SECTIONS SECTION (Optional)
# For structured compositions with different parts
# ============================================================================
[[sections]]
name = "intro"
measures = 8
tempo = 100.0 # Optional tempo change for this section
complexity = 0.3 # Lower complexity for intro
repeat = 1 # How many times to repeat this section
[[sections]]
name = "verse"
measures = 16
complexity = 0.6
repeat = 2
[[sections]]
name = "chorus"
measures = 16
tempo = 125.0 # Faster tempo for chorus
complexity = 0.8
key = "F4" # Optional key change
scale = "major" # Optional scale change
repeat = 2
[[sections]]
name = "bridge"
measures = 8
complexity = 0.9
key = "Am" # Relative minor
repeat = 1
[[sections]]
name = "outro"
measures = 8
tempo = 90.0 # Slower outro
complexity = 0.2
repeat = 1
# ============================================================================
# EXPORT SECTION
# How to save your finished composition
# ============================================================================
[export]
filename = "my_song" # Output filename (without extension)
format = "wav" # Only "wav" currently supported
sample_rate = 44100 # Options: 22050, 44100, 48000, 96000
bit_depth = 24 # Options: 16, 24, 32
stereo = true # true for stereo, false for mono
max_duration = 180.0 # Maximum length in seconds (optional)
# Optional: Generate variations of your composition
[export.variations]
count = 3 # Number of variations to generate
vary_complexity = true # Vary the complexity between versions
vary_rhythm = true # Vary the rhythmic patterns
vary_harmony = false # Vary the chord progressions
vary_tempo = false # Vary the tempo
# ============================================================================
# EFFECT TYPES REFERENCE
# Complete list of all available effects and their parameters
# ============================================================================
# FILTERS
# [[tracks.effects]]
# type = "lowpass" # Removes high frequencies (makes sound warmer/duller)
# cutoff = 1000.0 # Frequency cutoff in Hz (20-20000)
# resonance = 1.0 # Emphasis at cutoff frequency (0.1-10.0)
# [[tracks.effects]]
# type = "highpass" # Removes low frequencies (makes sound thinner/brighter)
# cutoff = 100.0 # Frequency cutoff in Hz (20-20000)
# resonance = 1.0 # Emphasis at cutoff frequency (0.1-10.0)
# TIME-BASED EFFECTS
# [[tracks.effects]]
# type = "delay" # Echo effect
# time = 0.25 # Delay time in seconds (0.01-2.0)
# feedback = 0.3 # How much delay feeds back (0.0-0.95)
# mix = 0.3 # Wet/dry mix (0.0-1.0)
# [[tracks.effects]]
# type = "reverb" # Spatial reverb
# room_size = 0.5 # Size of reverb space (0.0-1.0)
# damping = 0.5 # High frequency absorption (0.0-1.0)
# mix = 0.3 # Wet/dry mix (0.0-1.0)
# [[tracks.effects]]
# type = "chorus" # Thickening/modulation effect
# rate = 1.0 # Modulation rate in Hz (0.1-10.0)
# depth = 0.5 # Modulation depth (0.0-1.0)
# layers = 3 # Number of voices (1-8)
# mix = 0.3 # Wet/dry mix (0.0-1.0)
# DISTORTION
# [[tracks.effects]]
# type = "distortion" # Hard clipping distortion
# drive = 2.0 # Amount of distortion (1.0-10.0)
# tone = 0.5 # Tone control (0.0-1.0, 0=dark, 1=bright)
# LO-FI EFFECTS
# [[tracks.effects]]
# type = "vinyl" # Vinyl crackle and surface noise
# intensity = 0.5 # Crackle intensity (0.0-1.0)
# frequency = 0.5 # Crackle frequency (0.0-1.0)
# mix = 0.3 # Wet/dry mix (0.0-1.0)
# [[tracks.effects]]
# type = "tape" # Tape saturation and warmth
# drive = 2.0 # Saturation amount (1.0-10.0)
# warmth = 0.7 # Warmth/color (0.0-1.0)
# mix = 0.5 # Wet/dry mix (0.0-1.0)
# [[tracks.effects]]
# type = "bitcrusher" # Digital degradation
# bit_depth = 8.0 # Bit depth reduction (1.0-16.0)
# sample_rate_reduction = 2.0 # Sample rate reduction factor (1.0-10.0)
# mix = 0.5 # Wet/dry mix (0.0-1.0)
# ============================================================================
# INSTRUMENT TYPES REFERENCE
# ============================================================================
# instrument = "sine" # Pure sine wave - smooth, warm, flute-like
# instrument = "square" # Square wave - classic video game sound, punchy
# instrument = "sawtooth" # Sawtooth wave - bright, buzzy, good for leads
# instrument = "triangle" # Triangle wave - mellow, soft, like filtered square
# instrument = "noise" # White noise - for percussion and texture
# instrument = "piano" # Natural piano with harmonics
# ============================================================================
# PATTERN TYPES REFERENCE
# ============================================================================
# type = "custom" # Specify exact notes and timing
# type = "chord" # Play chord progressions
# type = "arpeggio" # Arpeggiate chords (play notes one after another)
# type = "sequence" # Step sequencer patterns
# ============================================================================
# CHORD NAMES REFERENCE
# ============================================================================
# Basic triads: "C", "Dm", "E", "F#", "Bb"
# Seventh chords: "Cmaj7", "Dm7", "E7", "F#m7b5", "Bbmaj7"
# Extended chords: "C9", "Dm11", "E13"
# Altered chords: "C7#11", "Dm7b5", "E7alt"
# Diminished/Augmented: "Cdim", "Caug", "C°7", "C+7"
# ============================================================================
# NOTE NAMES REFERENCE
# ============================================================================
# Format: [Note][Accidental][Octave]
# Notes: C, D, E, F, G, A, B
# Accidentals: # (sharp), b (flat), ♯, ♭
# Octaves: 0-9 (C4 = middle C = MIDI 60)
# Examples: "C4", "F#3", "Bb5", "A0", "G9"
# MIDI equivalents:
# C4 = 60, C#4 = 61, D4 = 62, D#4 = 63, E4 = 64, F4 = 65
# F#4 = 66, G4 = 67, G#4 = 68, A4 = 69, A#4 = 70, B4 = 71
# Add/subtract 12 for different octaves

View File

@ -1,666 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/schemas/composition-config.json",
"title": "Generate electronic music without AI",
"description": "JSON schema for configuring musicgen",
"type": "object",
"properties": {
"metadata": {
"type": "object",
"description": "Metadata about the composition",
"properties": {
"title": {
"type": "string",
"description": "Composition title",
"default": ""
},
"artist": {
"type": "string",
"description": "Artist or composer name",
"default": ""
},
"description": {
"type": "string",
"description": "Description of the composition",
"default": ""
},
"tags": {
"type": "array",
"description": "Tags or genres",
"items": {
"type": "string"
},
"default": []
}
},
"additionalProperties": false
},
"composition": {
"type": "object",
"description": "Global composition settings",
"required": ["key", "scale", "tempo", "measures"],
"properties": {
"key": {
"description": "Musical key",
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 127,
"description": "MIDI note number (60 = C4)"
},
{
"type": "string",
"pattern": "^[A-G][#b♯♭]?[0-9]$",
"description": "Note name (e.g., 'C4', 'F#3', 'Bb5')"
}
]
},
"scale": {
"type": "string",
"description": "Scale type",
"enum": [
"major",
"minor",
"dorian",
"phrygian",
"lydian",
"mixolydian",
"aeolian",
"locrian",
"pentatonic",
"blues",
"chromatic"
]
},
"tempo": {
"type": "number",
"description": "Tempo in beats per minute (BPM)",
"minimum": 40,
"maximum": 300
},
"time_signature": {
"type": "object",
"properties": {
"numerator": {
"type": "integer",
"minimum": 1,
"maximum": 16,
"description": "Number of beats per measure"
},
"denominator": {
"type": "integer",
"enum": [2, 4, 8, 16],
"description": "Note value that gets one beat"
}
},
"required": ["numerator", "denominator"],
"additionalProperties": false,
"default": {
"numerator": 4,
"denominator": 4
}
},
"measures": {
"type": "integer",
"description": "Number of measures in the composition",
"minimum": 1,
"maximum": 1000
},
"complexity": {
"type": "number",
"description": "Musical complexity level (0.0 = simple, 1.0 = complex)",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.6
},
"harmony_density": {
"type": "number",
"description": "Harmonic density (0.0 = sparse, 1.0 = dense)",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.7
},
"rhythmic_density": {
"type": "number",
"description": "Rhythmic density (0.0 = sparse, 1.0 = dense)",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.8
},
"seed": {
"type": "integer",
"description": "Random seed for reproducible generation",
"minimum": 0,
"maximum": 18446744073709551615
}
},
"additionalProperties": false
},
"tracks": {
"type": "array",
"description": "Individual track definitions",
"items": {
"type": "object",
"required": ["name", "instrument", "pattern"],
"properties": {
"name": {
"type": "string",
"description": "Track name (e.g., 'bass', 'kick', 'hihat', 'pad')"
},
"instrument": {
"type": "string",
"description": "Instrument/waveform type",
"enum": ["sine", "square", "sawtooth", "triangle", "noise"]
},
"volume": {
"type": "number",
"description": "Track volume level (0.0 = silent, 1.0 = full)",
"minimum": 0.0,
"maximum": 1.0,
"default": 1.0
},
"pattern": {
"type": "object",
"description": "Pattern definition for this track",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"description": "Pattern type",
"enum": ["custom", "chord", "arpeggio", "sequence"]
},
"loop_length": {
"type": "number",
"description": "Pattern loop length in beats",
"minimum": 0.1,
"maximum": 64.0,
"default": 4.0
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "custom"
}
}
},
"then": {
"properties": {
"steps": {
"type": "array",
"description": "Individual pattern steps",
"items": {
"type": "object",
"required": ["time", "note", "duration", "velocity"],
"properties": {
"time": {
"type": "number",
"description": "Time in beats when note starts",
"minimum": 0.0
},
"note": {
"description": "Note to play",
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 127,
"description": "MIDI note number"
},
{
"type": "string",
"pattern": "^[A-G][#b♯♭]?[0-9]$",
"description": "Note name (e.g., 'C4', 'F#3')"
}
]
},
"duration": {
"type": "number",
"description": "Note duration in beats",
"minimum": 0.01,
"maximum": 16.0
},
"velocity": {
"type": "number",
"description": "Note velocity/volume (0.0 to 1.0)",
"minimum": 0.0,
"maximum": 1.0
}
},
"additionalProperties": false
}
}
},
"required": ["steps"]
}
},
{
"if": {
"properties": {
"type": {
"const": "chord"
}
}
},
"then": {
"properties": {
"chord_progression": {
"type": "array",
"description": "Chord progression steps",
"items": {
"type": "object",
"required": ["time", "chord", "duration"],
"properties": {
"time": {
"type": "number",
"description": "Time in beats when chord starts",
"minimum": 0.0
},
"chord": {
"type": "string",
"description": "Chord name (e.g., 'Cm7', 'F', 'G#maj7')"
},
"duration": {
"type": "number",
"description": "Chord duration in beats",
"minimum": 0.1,
"maximum": 16.0
}
},
"additionalProperties": false
}
},
"voicing": {
"type": "string",
"description": "Chord voicing style",
"enum": ["close", "spread", "drop2", "drop3"],
"default": "close"
},
"octave": {
"type": "integer",
"description": "Base octave for chord",
"minimum": 0,
"maximum": 8,
"default": 4
}
},
"required": ["chord_progression"]
}
}
],
"additionalProperties": false
},
"effects": {
"type": "array",
"description": "Audio effects to apply to this track",
"items": {
"type": "object",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"description": "Effect type",
"enum": [
"lowpass",
"highpass",
"bandpass",
"delay",
"reverb",
"chorus",
"distortion",
"compressor"
]
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "lowpass"
}
}
},
"then": {
"properties": {
"cutoff": {
"type": "number",
"description": "Cutoff frequency in Hz",
"minimum": 20,
"maximum": 20000,
"default": 1000
},
"resonance": {
"type": "number",
"description": "Resonance factor",
"minimum": 0.1,
"maximum": 10.0,
"default": 1.0
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "highpass"
}
}
},
"then": {
"properties": {
"cutoff": {
"type": "number",
"description": "Cutoff frequency in Hz",
"minimum": 20,
"maximum": 20000,
"default": 100
},
"resonance": {
"type": "number",
"description": "Resonance factor",
"minimum": 0.1,
"maximum": 10.0,
"default": 1.0
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "delay"
}
}
},
"then": {
"properties": {
"time": {
"type": "number",
"description": "Delay time in seconds",
"minimum": 0.001,
"maximum": 2.0,
"default": 0.25
},
"feedback": {
"type": "number",
"description": "Feedback amount",
"minimum": 0.0,
"maximum": 0.95,
"default": 0.3
},
"mix": {
"type": "number",
"description": "Wet/dry mix",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.3
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "reverb"
}
}
},
"then": {
"properties": {
"room_size": {
"type": "number",
"description": "Room size",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.5
},
"damping": {
"type": "number",
"description": "High frequency damping",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.5
},
"mix": {
"type": "number",
"description": "Wet/dry mix",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.3
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "chorus"
}
}
},
"then": {
"properties": {
"rate": {
"type": "number",
"description": "LFO rate in Hz",
"minimum": 0.1,
"maximum": 10.0,
"default": 1.0
},
"depth": {
"type": "number",
"description": "Modulation depth",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.5
},
"layers": {
"type": "integer",
"description": "Number of chorus layers",
"minimum": 1,
"maximum": 4,
"default": 2
},
"mix": {
"type": "number",
"description": "Wet/dry mix",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.5
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "distortion"
}
}
},
"then": {
"properties": {
"drive": {
"type": "number",
"description": "Distortion amount",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.5
},
"tone": {
"type": "number",
"description": "Tone control (0.0 = dark, 1.0 = bright)",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.5
}
}
}
}
],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"sections": {
"type": "array",
"description": "Section definitions for structured compositions",
"items": {
"type": "object",
"required": ["name", "measures"],
"properties": {
"name": {
"type": "string",
"description": "Section name (e.g., 'intro', 'verse', 'chorus', 'bridge', 'outro')"
},
"measures": {
"type": "integer",
"description": "Number of measures in this section",
"minimum": 1,
"maximum": 1000
},
"tempo": {
"type": "number",
"description": "Tempo override for this section",
"minimum": 40,
"maximum": 300
},
"key": {
"description": "Key change for this section",
"oneOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 127
},
{
"type": "string",
"pattern": "^[A-G][#b♯♭]?[0-9]$"
}
]
},
"scale": {
"type": "string",
"description": "Scale override for this section",
"enum": [
"major",
"minor",
"dorian",
"phrygian",
"lydian",
"mixolydian",
"aeolian",
"locrian",
"pentatonic",
"blues",
"chromatic"
]
},
"complexity": {
"type": "number",
"description": "Complexity override for this section",
"minimum": 0.0,
"maximum": 1.0
},
"repeat": {
"type": "integer",
"description": "Number of times to repeat this section",
"minimum": 1,
"maximum": 100,
"default": 1
}
},
"additionalProperties": false
}
},
"export": {
"type": "object",
"description": "Export settings",
"properties": {
"filename": {
"type": "string",
"description": "Output filename (without extension)",
"default": "output"
},
"format": {
"type": "string",
"description": "Export format",
"enum": ["wav", "mp3", "flac"],
"default": "wav"
},
"sample_rate": {
"type": "integer",
"description": "Sample rate in Hz",
"enum": [22050, 44100, 48000, 88200, 96000],
"default": 44100
},
"bit_depth": {
"type": "integer",
"description": "Bit depth",
"enum": [16, 24, 32],
"default": 16
},
"stereo": {
"type": "boolean",
"description": "Export in stereo (true) or mono (false)",
"default": false
},
"max_duration": {
"type": "number",
"description": "Maximum duration in seconds (null for full composition)",
"minimum": 1.0,
"maximum": 3600.0
},
"variations": {
"type": "object",
"description": "Variation generation settings",
"properties": {
"count": {
"type": "integer",
"description": "Number of variations to generate",
"minimum": 1,
"maximum": 100
},
"vary_complexity": {
"type": "boolean",
"description": "Vary complexity between variations",
"default": false
},
"vary_rhythm": {
"type": "boolean",
"description": "Vary rhythm between variations",
"default": false
},
"vary_harmony": {
"type": "boolean",
"description": "Vary harmony between variations",
"default": false
},
"vary_tempo": {
"type": "boolean",
"description": "Vary tempo between variations",
"default": false
}
},
"required": ["count"],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": ["composition"],
"additionalProperties": false
}

View File

@ -1,231 +0,0 @@
{
"metadata": {
"title": "Fantasy",
"artist": "Atridad Lahiji",
"description": "A generic fantasy beat."
},
"composition": {
"key": 60,
"scale": "minor",
"tempo": 85.0,
"time_signature": {
"numerator": 4,
"denominator": 4
},
"measures": 18
},
"tracks": [
{
"name": "deep_bass",
"instrument": "sine",
"volume": 0.7,
"pattern": {
"type": "custom",
"steps": [
{ "time": 0.0, "note": "C2", "duration": 0.75, "velocity": 0.9 },
{ "time": 1.0, "note": "C2", "duration": 0.75, "velocity": 0.8 },
{ "time": 2.0, "note": "G1", "duration": 0.75, "velocity": 0.85 },
{ "time": 3.0, "note": "G1", "duration": 0.75, "velocity": 0.8 }
],
"loop_length": 4.0
},
"effects": [
{
"type": "lowpass",
"cutoff": 400.0,
"resonance": 1.8
}
]
},
{
"name": "sub_bass",
"instrument": "square",
"volume": 0.5,
"pattern": {
"type": "custom",
"steps": [
{ "time": 0.5, "note": "C3", "duration": 0.25, "velocity": 0.6 },
{ "time": 1.5, "note": "Eb3", "duration": 0.25, "velocity": 0.6 },
{ "time": 2.5, "note": "G3", "duration": 0.25, "velocity": 0.6 },
{ "time": 3.5, "note": "F3", "duration": 0.25, "velocity": 0.6 }
],
"loop_length": 4.0
},
"effects": [
{
"type": "lowpass",
"cutoff": 800.0,
"resonance": 2.0
}
]
},
{
"name": "lofi_kick",
"instrument": "sine",
"volume": 0.6,
"pattern": {
"type": "custom",
"steps": [
{ "time": 0.0, "note": "C1", "duration": 0.1, "velocity": 1.0 },
{ "time": 1.0, "note": "C1", "duration": 0.1, "velocity": 0.8 },
{ "time": 2.0, "note": "C1", "duration": 0.1, "velocity": 0.9 },
{ "time": 3.0, "note": "C1", "duration": 0.1, "velocity": 0.7 }
],
"loop_length": 4.0
},
"effects": [
{
"type": "lowpass",
"cutoff": 200.0,
"resonance": 1.0
},
{
"type": "distortion",
"drive": 0.3,
"tone": 0.2
}
]
},
{
"name": "lofi_snare",
"instrument": "noise",
"volume": 0.3,
"pattern": {
"type": "custom",
"steps": [
{ "time": 1.0, "note": "E3", "duration": 0.05, "velocity": 0.7 },
{ "time": 3.0, "note": "E3", "duration": 0.05, "velocity": 0.8 }
],
"loop_length": 4.0
},
"effects": [
{
"type": "highpass",
"cutoff": 200.0,
"resonance": 1.0
},
{
"type": "lowpass",
"cutoff": 600.0,
"resonance": 1.5
}
]
},
{
"name": "lofi_hihat",
"instrument": "noise",
"volume": 0.2,
"pattern": {
"type": "custom",
"steps": [
{ "time": 0.5, "note": "A4", "duration": 0.02, "velocity": 0.4 },
{ "time": 1.5, "note": "A4", "duration": 0.02, "velocity": 0.4 },
{ "time": 2.5, "note": "A4", "duration": 0.02, "velocity": 0.5 },
{ "time": 3.5, "note": "A4", "duration": 0.02, "velocity": 0.3 }
],
"loop_length": 4.0
},
"effects": [
{
"type": "highpass",
"cutoff": 1000.0,
"resonance": 1.0
},
{
"type": "lowpass",
"cutoff": 2000.0,
"resonance": 1.0
}
]
},
{
"name": "warm_pad",
"instrument": "sine",
"volume": 0.2,
"pattern": {
"type": "chord",
"chord_progression": [
{ "time": 0.0, "chord": "Cm7", "duration": 4.0 },
{ "time": 4.0, "chord": "Fm7", "duration": 4.0 },
{ "time": 8.0, "chord": "Gm7", "duration": 4.0 },
{ "time": 12.0, "chord": "Cm7", "duration": 4.0 }
],
"voicing": "spread",
"octave": 3
},
"effects": [
{
"type": "lowpass",
"cutoff": 600.0,
"resonance": 1.2
},
{
"type": "reverb",
"room_size": 0.8,
"damping": 0.8,
"mix": 0.4
}
]
},
{
"name": "peaceful_melody",
"instrument": "sine",
"volume": 0.9,
"pattern": {
"type": "custom",
"steps": [
{ "time": 16.0, "note": "Eb4", "duration": 1.0, "velocity": 0.6 },
{ "time": 17.5, "note": "G4", "duration": 1.0, "velocity": 0.5 },
{ "time": 19.0, "note": "C5", "duration": 1.5, "velocity": 0.7 },
{ "time": 21.0, "note": "Bb4", "duration": 0.75, "velocity": 0.5 },
{ "time": 22.0, "note": "Ab4", "duration": 1.0, "velocity": 0.6 },
{ "time": 23.5, "note": "F4", "duration": 0.75, "velocity": 0.5 },
{ "time": 24.0, "note": "G4", "duration": 2.0, "velocity": 0.6 },
{ "time": 26.5, "note": "Bb4", "duration": 0.75, "velocity": 0.5 },
{ "time": 27.5, "note": "D5", "duration": 1.0, "velocity": 0.7 },
{ "time": 29.0, "note": "C5", "duration": 1.5, "velocity": 0.6 },
{ "time": 31.0, "note": "G4", "duration": 1.0, "velocity": 0.5 },
{ "time": 32.5, "note": "Eb4", "duration": 1.5, "velocity": 0.6 },
{ "time": 34.5, "note": "C5", "duration": 1.0, "velocity": 0.7 },
{ "time": 36.0, "note": "G4", "duration": 2.0, "velocity": 0.5 },
{ "time": 38.5, "note": "Eb4", "duration": 1.0, "velocity": 0.4 },
{ "time": 40.0, "note": "C5", "duration": 1.5, "velocity": 0.6 },
{ "time": 42.0, "note": "Bb4", "duration": 0.75, "velocity": 0.5 },
{ "time": 43.0, "note": "Ab4", "duration": 1.0, "velocity": 0.6 },
{ "time": 44.5, "note": "F4", "duration": 1.25, "velocity": 0.5 },
{ "time": 46.0, "note": "G4", "duration": 1.0, "velocity": 0.6 },
{ "time": 47.5, "note": "Bb4", "duration": 0.75, "velocity": 0.5 },
{ "time": 48.5, "note": "D5", "duration": 1.0, "velocity": 0.7 },
{ "time": 50.0, "note": "C5", "duration": 1.5, "velocity": 0.6 },
{ "time": 52.0, "note": "G4", "duration": 1.0, "velocity": 0.5 },
{ "time": 53.5, "note": "Eb4", "duration": 1.5, "velocity": 0.6 },
{ "time": 55.5, "note": "C5", "duration": 1.0, "velocity": 0.7 },
{ "time": 57.0, "note": "G4", "duration": 2.0, "velocity": 0.5 },
{ "time": 60.0, "note": "Eb4", "duration": 1.0, "velocity": 0.4 },
{ "time": 62.0, "note": "C5", "duration": 2.0, "velocity": 0.6 },
{ "time": 65.0, "note": "G4", "duration": 2.0, "velocity": 0.5 },
{ "time": 68.0, "note": "Eb4", "duration": 4.0, "velocity": 0.4 }
],
"loop_length": 72.0
},
"effects": [
{
"type": "highpass",
"cutoff": 2000.0,
"resonance": 1.1
},
{
"type": "reverb",
"room_size": 0.9,
"damping": 0.7,
"mix": 0.5
}
]
}
],
"export": {
"filename": "fantasy",
"format": "wav",
"stereo": true
}
}

View File

@ -242,7 +242,7 @@ impl AudioExporter {
let spec = hound::WavSpec {
channels: 1, // Mono for simplicity
sample_rate: self.sample_rate as u32,
bits_per_sample: 16,
bits_per_sample: 24,
sample_format: hound::SampleFormat::Int,
};
@ -276,11 +276,11 @@ impl AudioExporter {
return Err(format!("Audio processing error: {}", e));
}
// Convert to i16 and write to file
// Convert to i32 (24-bit) and write to file
for &sample in &buffer {
let sample_i16 = (sample * i16::MAX as f32) as i16;
let sample_i32 = (sample * 8388607.0) as i32; // 24-bit max value
writer
.write_sample(sample_i16)
.write_sample(sample_i32)
.map_err(|e| format!("Failed to write sample: {}", e))?;
}

View File

@ -7,6 +7,7 @@ use crate::core::{
Composition as CoreComposition, CompositionParams, CompositionStyle, InstrumentType, Note,
Track,
};
use crate::scales::{Scale, ScaleType};
use crate::synthesis::Waveform;
@ -76,6 +77,18 @@ impl Composition {
}
};
// Use configured instrument instead of guessing from name
let waveform = match track.instrument.to_lowercase().as_str() {
"sine" => Waveform::Sine,
"square" => Waveform::Square,
"sawtooth" => Waveform::Sawtooth,
"triangle" => Waveform::Triangle,
"noise" => Waveform::Noise,
"piano" => Waveform::Piano,
_ => return Err(format!("Unknown instrument: {}", track.instrument)),
};
// Determine instrument type from name for backwards compatibility
let instrument_type = match track.name.to_lowercase().as_str() {
name if name.contains("bass") => InstrumentType::Bass,
name if name.contains("lead") => InstrumentType::Lead,
@ -91,15 +104,6 @@ impl Composition {
_ => InstrumentType::Lead,
};
let waveform = match instrument_type {
InstrumentType::Lead => Waveform::Sawtooth,
InstrumentType::Bass => Waveform::Square,
InstrumentType::Pad => Waveform::Sine,
InstrumentType::Arp => Waveform::Triangle,
InstrumentType::Percussion => Waveform::Noise,
InstrumentType::Drone => Waveform::Sine,
};
Ok(Track {
name: track.name.clone(),
octave: 4,
@ -107,6 +111,7 @@ impl Composition {
volume: track.volume,
instrument_type,
waveform,
effect_configs: track.effects.clone(),
})
}

View File

@ -1,6 +1,6 @@
//! JSON configuration module for composition specifications
//! TOML configuration module for composition specifications
//!
//! This module provides structures and parsing for JSON-based composition definitions,
//! This module provides structures and parsing for TOML-based composition definitions,
//! allowing users to specify complete compositions in a declarative format.
use crate::core::{CompositionParams, CompositionStyle};
@ -235,10 +235,18 @@ pub struct PatternConfig {
/// Pattern-specific parameters
#[serde(flatten)]
pub params: serde_json::Value,
pub params: toml::Value,
}
/// Effect configuration
///
/// Available effect types:
/// - `lowpass`, `highpass` - Frequency filters
/// - `delay`, `reverb`, `chorus` - Time-based effects
/// - `distortion` - Hard distortion/clipping
/// - `vinyl` - Vinyl crackle and surface noise (lofi)
/// - `tape` - Tape saturation and warmth (lofi)
/// - `bitcrusher` - Bit depth and sample rate reduction (lofi)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectConfig {
/// Effect type
@ -247,7 +255,7 @@ pub struct EffectConfig {
/// Effect parameters
#[serde(flatten)]
pub params: serde_json::Value,
pub params: toml::Value,
}
/// Export settings
@ -401,29 +409,28 @@ fn parse_note_name(name: &str) -> Result<u8, String> {
}
impl CompositionConfig {
/// Load configuration from a JSON file
/// Load configuration from a TOML file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
let content =
fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?;
Self::from_json(&content)
Self::from_toml(&content)
}
/// Parse configuration from JSON string
pub fn from_json(json: &str) -> Result<Self, String> {
serde_json::from_str(json).map_err(|e| format!("Failed to parse JSON: {}", e))
/// Parse configuration from TOML string
pub fn from_toml(toml_str: &str) -> Result<Self, String> {
toml::from_str(toml_str).map_err(|e| format!("Failed to parse TOML: {}", e))
}
/// Save configuration to a JSON file
/// Save configuration to a TOML file
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
let json = self.to_json_pretty()?;
fs::write(path, json).map_err(|e| format!("Failed to write config file: {}", e))
let toml_str = self.to_toml_pretty()?;
fs::write(path, toml_str).map_err(|e| format!("Failed to write config file: {}", e))
}
/// Serialize configuration to pretty JSON
pub fn to_json_pretty(&self) -> Result<String, String> {
serde_json::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize to JSON: {}", e))
/// Serialize configuration to pretty TOML
pub fn to_toml_pretty(&self) -> Result<String, String> {
toml::to_string_pretty(self).map_err(|e| format!("Failed to serialize to TOML: {}", e))
}
/// Convert to composition parameters (deprecated - use track-based generation instead)
@ -466,68 +473,46 @@ impl CompositionConfig {
pub fn example() -> Self {
Self {
metadata: Metadata {
title: "Example Track Composition".to_string(),
title: "Example Track".to_string(),
artist: "Track Composer".to_string(),
description: "An example track-based composition".to_string(),
tags: vec!["lofi".to_string(), "chill".to_string()],
description: "Basic example composition".to_string(),
tags: vec!["example".to_string()],
},
composition: CompositionSettings {
key: KeySpec::NoteName("C4".to_string()),
scale: "minor".to_string(),
tempo: 85.0,
tempo: 120.0,
time_signature: TimeSignature {
numerator: 4,
denominator: 4,
},
measures: 16,
complexity: 0.6,
harmony_density: 0.7,
rhythmic_density: 0.7,
seed: Some(42),
measures: 8,
complexity: 0.5,
harmony_density: 0.5,
rhythmic_density: 0.5,
seed: None,
},
tracks: vec![TrackConfig {
name: "bass".to_string(),
instrument: "sine".to_string(),
volume: 0.9,
volume: 0.8,
pattern: TrackPattern {
pattern_type: "custom".to_string(),
steps: vec![
PatternStep {
steps: vec![PatternStep {
time: 0.0,
note: "C2".to_string(),
duration: 0.75,
velocity: 0.9,
},
PatternStep {
time: 2.0,
note: "G2".to_string(),
duration: 0.75,
duration: 1.0,
velocity: 0.8,
},
],
}],
loop_length: 4.0,
chord_progression: vec![],
voicing: None,
octave: None,
},
effects: vec![EffectConfig {
effect_type: "lowpass".to_string(),
params: serde_json::json!({
"cutoff": 400.0,
"resonance": 1.8
}),
}],
effects: vec![],
}],
sections: vec![],
export: ExportSettings {
filename: "track_composition".to_string(),
format: "wav".to_string(),
sample_rate: 44100,
bit_depth: 16,
stereo: true,
max_duration: None,
variations: None,
},
export: ExportSettings::default(),
}
}
}
@ -548,8 +533,8 @@ mod tests {
#[test]
fn test_config_serialization() {
let config = CompositionConfig::example();
let json = config.to_json_pretty().unwrap();
let parsed = CompositionConfig::from_json(&json).unwrap();
let toml_str = config.to_toml_pretty().unwrap();
let parsed = CompositionConfig::from_toml(&toml_str).unwrap();
assert_eq!(parsed.composition.tempo, config.composition.tempo);
}

View File

@ -3,6 +3,7 @@
//! This module contains the fundamental data structures used throughout
//! the musicgen library for representing notes, tracks, and compositions.
use crate::config::EffectConfig;
use crate::scales::ScaleType;
use crate::synthesis::Waveform;
@ -44,6 +45,7 @@ pub enum InstrumentType {
}
/// A track represents a single instrument part
/// Track represents a single instrument part in a composition
#[derive(Debug, Clone)]
pub struct Track {
pub name: String,
@ -52,6 +54,7 @@ pub struct Track {
pub volume: f32,
pub instrument_type: InstrumentType,
pub waveform: Waveform,
pub effect_configs: Vec<EffectConfig>,
}
/// Composition styles (kept for compatibility)
@ -299,6 +302,7 @@ mod tests {
volume: 0.8,
instrument_type: InstrumentType::Lead,
waveform: Waveform::Sawtooth,
effect_configs: Vec::new(),
};
composition.tracks.push(track);

View File

@ -8,7 +8,7 @@ use std::collections::VecDeque;
use std::f32::consts::PI;
/// Trait for audio effects that process samples
pub trait AudioEffect {
pub trait AudioEffect: Send + Sync + std::fmt::Debug {
/// Process a single audio sample
fn process_sample(&mut self, input: f32) -> f32;
@ -19,11 +19,14 @@ pub trait AudioEffect {
}
}
/// Reset the effect's internal state
/// Reset the effect state
fn reset(&mut self);
/// Set a parameter of the effect
/// Set a parameter value
fn set_parameter(&mut self, param: &str, value: f32);
/// Clone this effect into a new boxed trait object
fn clone_box(&self) -> Box<dyn AudioEffect>;
}
/// Low-pass filter effect
@ -132,6 +135,10 @@ impl AudioEffect for LowPassFilter {
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// High-pass filter effect
@ -208,12 +215,16 @@ impl HighPassFilter {
impl AudioEffect for HighPassFilter {
fn process_sample(&mut self, input: f32) -> f32 {
// Store input
self.x2 = self.x1;
self.x1 = input;
// Calculate output
let output = self.a0 * input + self.a1 * self.x1 + self.a2 * self.x2
- self.b1 * self.y1
- self.b2 * self.y2;
self.x2 = self.x1;
self.x1 = input;
// Store output
self.y2 = self.y1;
self.y1 = output;
@ -234,10 +245,14 @@ impl AudioEffect for HighPassFilter {
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Delay effect with feedback
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Delay {
pub delay_time: f32, // Delay time in seconds
pub feedback: f32, // Feedback amount (0.0 to 0.95)
@ -322,10 +337,14 @@ impl AudioEffect for Delay {
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Simple reverb effect using multiple delay lines
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Reverb {
pub room_size: f32,
pub damping: f32,
@ -451,6 +470,10 @@ impl AudioEffect for Reverb {
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Distortion effect
@ -504,17 +527,17 @@ impl Distortion {
impl AudioEffect for Distortion {
fn process_sample(&mut self, input: f32) -> f32 {
// Pre-filtering to reduce aliasing
// Apply pre-filter
let filtered_input = self.pre_filter.process_sample(input);
// Apply waveshaping distortion
let distorted = self.waveshaper(filtered_input);
let distorted = self.waveshaper(filtered_input * self.drive);
// Post-filtering for tone shaping
let shaped = self.post_filter.process_sample(distorted);
// Apply post-filter
let filtered_output = self.post_filter.process_sample(distorted);
// Apply output gain compensation
shaped * self.output_gain
// Apply output gain
filtered_output * self.output_gain
}
fn reset(&mut self) {
@ -526,14 +549,18 @@ impl AudioEffect for Distortion {
match param {
"drive" => self.set_drive(value),
"tone" => self.set_tone(value),
"output_gain" => self.output_gain = value.clamp(0.0, 2.0),
"gain" => self.output_gain = value.clamp(0.0, 2.0),
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Chorus effect using multiple delay lines with LFO modulation
#[derive(Debug)]
/// Chorus effect with multiple delay lines and LFO modulation
#[derive(Debug, Clone)]
pub struct Chorus {
pub rate: f32, // LFO rate in Hz
pub depth: f32, // Modulation depth (0.0 to 1.0)
@ -662,9 +689,14 @@ impl AudioEffect for Chorus {
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Effects chain that can combine multiple effects
#[derive(Debug)]
pub struct EffectsChain {
effects: Vec<Box<dyn AudioEffect>>,
}
@ -735,6 +767,10 @@ impl AudioEffect for EffectsChain {
// For now, we'll just ignore it
let _ = (param, value);
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
impl Default for EffectsChain {
@ -743,6 +779,276 @@ impl Default for EffectsChain {
}
}
impl Clone for EffectsChain {
fn clone(&self) -> Self {
let mut cloned_chain = EffectsChain::new();
for effect in &self.effects {
cloned_chain.effects.push(effect.clone_box());
}
cloned_chain
}
}
/// Vinyl crackle effect for lofi authenticity
#[derive(Debug, Clone)]
pub struct VinylCrackle {
pub intensity: f32, // Crackle intensity (0.0 to 1.0)
pub frequency: f32, // How often crackles occur
pub mix: f32, // Wet/dry mix
crackle_state: f32,
sample_counter: usize,
}
impl VinylCrackle {
/// Create a new vinyl crackle effect
///
/// # Arguments
/// * `intensity` - Crackle intensity (0.0 to 1.0)
/// * `frequency` - Crackle frequency (0.0 to 1.0)
/// * `mix` - Wet/dry mix (0.0 to 1.0)
pub fn new(intensity: f32, frequency: f32, mix: f32) -> Self {
Self {
intensity: intensity.clamp(0.0, 1.0),
frequency: frequency.clamp(0.0, 1.0),
mix: mix.clamp(0.0, 1.0),
crackle_state: 0.0,
sample_counter: 0,
}
}
pub fn set_intensity(&mut self, intensity: f32) {
self.intensity = intensity.clamp(0.0, 1.0);
}
pub fn set_frequency(&mut self, frequency: f32) {
self.frequency = frequency.clamp(0.0, 1.0);
}
fn generate_crackle(&mut self) -> f32 {
use rand::Rng;
let mut rng = rand::thread_rng();
self.sample_counter += 1;
// Generate occasional pops and crackles
if rng.gen_range(0.0..1.0) < self.frequency * 0.001 {
self.crackle_state = rng.gen_range(-1.0..1.0) * self.intensity;
}
// Add continuous surface noise
let surface_noise = rng.gen_range(-0.1..0.1) * self.intensity * 0.3;
// Decay the crackle
self.crackle_state *= 0.95;
self.crackle_state + surface_noise
}
}
impl AudioEffect for VinylCrackle {
fn process_sample(&mut self, input: f32) -> f32 {
let crackle = self.generate_crackle();
input + crackle * self.mix
}
fn reset(&mut self) {
self.crackle_state = 0.0;
self.sample_counter = 0;
}
fn set_parameter(&mut self, param: &str, value: f32) {
match param {
"intensity" => self.set_intensity(value),
"frequency" => self.set_frequency(value),
"mix" => self.mix = value.clamp(0.0, 1.0),
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Tape saturation effect for warm, analog sound
#[derive(Debug, Clone)]
pub struct TapeSaturation {
pub drive: f32, // Input drive (1.0 to 5.0)
pub warmth: f32, // Warmth amount (0.0 to 1.0)
pub mix: f32, // Wet/dry mix
pre_filter: LowPassFilter,
post_filter: LowPassFilter,
dc_filter_state: f32,
}
impl TapeSaturation {
/// Create a new tape saturation effect
///
/// # Arguments
/// * `drive` - Input drive amount (1.0 to 5.0)
/// * `warmth` - Warmth/tone control (0.0 to 1.0)
/// * `mix` - Wet/dry mix (0.0 to 1.0)
pub fn new(drive: f32, warmth: f32, mix: f32) -> Self {
Self {
drive: drive.clamp(1.0, 5.0),
warmth: warmth.clamp(0.0, 1.0),
mix: mix.clamp(0.0, 1.0),
pre_filter: LowPassFilter::new(8000.0, 0.7),
post_filter: LowPassFilter::new(6000.0 + warmth * 2000.0, 0.5),
dc_filter_state: 0.0,
}
}
pub fn set_drive(&mut self, drive: f32) {
self.drive = drive.clamp(1.0, 5.0);
}
pub fn set_warmth(&mut self, warmth: f32) {
self.warmth = warmth.clamp(0.0, 1.0);
self.post_filter.set_cutoff(6000.0 + self.warmth * 2000.0);
}
fn tape_saturation(&self, input: f32) -> f32 {
let driven = input * self.drive;
// Soft tape-like saturation using asymmetric clipping
let saturated = if driven >= 0.0 {
driven / (1.0 + driven * 0.5)
} else {
driven / (1.0 - driven * 0.3) // Slightly different for negative values
};
// Add subtle even harmonics for warmth
let harmonics = saturated + saturated * saturated * 0.1 * self.warmth;
harmonics.clamp(-1.0, 1.0)
}
fn dc_filter(&mut self, input: f32) -> f32 {
// Simple DC blocking filter
let output = input - self.dc_filter_state + 0.995 * self.dc_filter_state;
self.dc_filter_state = input;
output
}
}
impl AudioEffect for TapeSaturation {
fn process_sample(&mut self, input: f32) -> f32 {
// Pre-filter
let pre_filtered = self.pre_filter.process_sample(input);
// Apply tape saturation
let saturated = self.tape_saturation(pre_filtered * self.drive);
// Post-filter
let post_filtered = self.post_filter.process_sample(saturated);
// DC filter
let dc_filtered = self.dc_filter(post_filtered);
// Mix wet/dry
input * (1.0 - self.mix) + dc_filtered * self.mix
}
fn reset(&mut self) {
self.pre_filter.reset();
self.post_filter.reset();
self.dc_filter_state = 0.0;
}
fn set_parameter(&mut self, param: &str, value: f32) {
match param {
"drive" => self.set_drive(value),
"warmth" => self.set_warmth(value),
"mix" => self.mix = value.clamp(0.0, 1.0),
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
/// Bit crusher effect for digital degradation
#[derive(Debug, Clone)]
pub struct BitCrusher {
pub bit_depth: f32, // Effective bit depth (1.0 to 16.0)
pub sample_rate_reduction: f32, // Sample rate reduction factor (1.0 to 8.0)
pub mix: f32, // Wet/dry mix
hold_sample: f32,
hold_counter: i32,
}
impl BitCrusher {
/// Create a new bit crusher effect
///
/// # Arguments
/// * `bit_depth` - Bit depth reduction (1.0 to 16.0, lower = more crushed)
/// * `sample_rate_reduction` - Sample rate reduction (1.0 = no reduction, higher = more reduction)
/// * `mix` - Wet/dry mix (0.0 to 1.0)
pub fn new(bit_depth: f32, sample_rate_reduction: f32, mix: f32) -> Self {
Self {
bit_depth: bit_depth.clamp(1.0, 16.0),
sample_rate_reduction: sample_rate_reduction.clamp(1.0, 8.0),
mix: mix.clamp(0.0, 1.0),
hold_sample: 0.0,
hold_counter: 0,
}
}
pub fn set_bit_depth(&mut self, bit_depth: f32) {
self.bit_depth = bit_depth.clamp(1.0, 16.0);
}
pub fn set_sample_rate_reduction(&mut self, reduction: f32) {
self.sample_rate_reduction = reduction.clamp(1.0, 8.0);
}
fn crush_bits(&self, input: f32) -> f32 {
let levels = 2.0_f32.powf(self.bit_depth);
let step = 2.0 / levels;
// Quantize to reduced bit depth
((input / step).round() * step).clamp(-1.0, 1.0)
}
}
impl AudioEffect for BitCrusher {
fn process_sample(&mut self, input: f32) -> f32 {
// Sample rate reduction (sample and hold)
if self.hold_counter <= 0 {
self.hold_sample = self.crush_bits(input);
self.hold_counter = self.sample_rate_reduction as i32;
}
self.hold_counter -= 1;
// Mix with dry signal
input * (1.0 - self.mix) + self.hold_sample * self.mix
}
fn reset(&mut self) {
self.hold_sample = 0.0;
self.hold_counter = 0;
}
fn set_parameter(&mut self, param: &str, value: f32) {
match param {
"bit_depth" => self.set_bit_depth(value),
"sample_rate_reduction" => self.set_sample_rate_reduction(value),
"mix" => self.mix = value.clamp(0.0, 1.0),
_ => {}
}
}
fn clone_box(&self) -> Box<dyn AudioEffect> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -763,16 +1069,23 @@ mod tests {
#[test]
fn test_delay() {
let mut delay = Delay::new(0.1, 0.5, 0.5);
let output = delay.process_sample(1.0);
assert!(output.is_finite());
let mut delay = Delay::new(0.01, 0.5, 0.5); // Shorter delay for testing
// After enough samples, we should get some delayed signal
for _ in 0..(0.1 * SAMPLE_RATE) as usize {
// Process an impulse
let output1 = delay.process_sample(1.0);
assert!(output1.is_finite());
// The immediate output should include the dry signal
assert!(output1 > 0.0);
// Process some zeros to advance through the delay
for _ in 0..(0.01 * SAMPLE_RATE) as usize + 1 {
delay.process_sample(0.0);
}
// After the delay time, we should still get finite output
let delayed_output = delay.process_sample(0.0);
assert!(delayed_output.abs() > 0.0);
assert!(delayed_output.is_finite());
}
#[test]
@ -812,11 +1125,46 @@ mod tests {
#[test]
fn test_filter_parameter_setting() {
let mut filter = LowPassFilter::new(1000.0, 1.0);
let mut filter = LowPassFilter::new(1000.0, 0.5);
filter.set_parameter("cutoff", 2000.0);
assert_eq!(filter.cutoff_frequency, 2000.0);
filter.set_parameter("resonance", 2.0);
assert_eq!(filter.resonance, 2.0);
filter.set_parameter("resonance", 0.8);
assert_eq!(filter.resonance, 0.8);
}
#[test]
fn test_effects_chain_cloning() {
// Create original effects chain
let mut original_chain = EffectsChain::new();
original_chain.add_effect(Box::new(LowPassFilter::new(1000.0, 0.5)));
original_chain.add_effect(Box::new(Delay::new(0.1, 0.3, 0.5)));
// Clone the chain
let mut cloned_chain = original_chain.clone();
// Both chains should have the same number of effects
assert_eq!(original_chain.len(), cloned_chain.len());
assert_eq!(original_chain.len(), 2);
// Test that both chains process audio independently
let test_sample = 0.5f32;
let original_output = original_chain.process_sample(test_sample);
let cloned_output = cloned_chain.process_sample(test_sample);
// The outputs should be the same since they're identical chains
assert!((original_output - cloned_output).abs() < 0.001);
// Test that they remain independent after processing
original_chain.process_sample(0.8);
cloned_chain.process_sample(-0.3);
// Process the same sample again - they might differ now due to internal state
let original_output2 = original_chain.process_sample(test_sample);
let cloned_output2 = cloned_chain.process_sample(test_sample);
// The chains should still function properly (no panics or errors)
assert!(original_output2.is_finite());
assert!(cloned_output2.is_finite());
}
}

View File

@ -19,16 +19,16 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
/// Generate composition from JSON configuration file
Json {
/// Path to JSON configuration file
/// Generate composition from TOML configuration file
Toml {
/// Path to TOML configuration file
config: PathBuf,
/// Override output filename
#[arg(short, long)]
output: Option<String>,
/// Print example JSON configuration
/// Print example TOML configuration
#[arg(long)]
example: bool,
},
@ -53,11 +53,11 @@ fn main() {
let cli = Cli::parse();
let result = match cli.command {
Commands::Json {
Commands::Toml {
config,
output,
example,
} => handle_json_command(config, output, example),
} => handle_toml_command(config, output, example),
Commands::Info {
scales,
@ -96,6 +96,7 @@ fn show_info(scales: bool, instruments: bool, all: bool) -> Result<(), Box<dyn s
println!(" sawtooth - Sawtooth wave (bright, buzzy)");
println!(" triangle - Triangle wave (mellow, soft)");
println!(" noise - White noise (for percussion, textures)");
println!(" piano - Natural piano sound with harmonics");
println!();
}
@ -125,16 +126,16 @@ fn show_info(scales: bool, instruments: bool, all: bool) -> Result<(), Box<dyn s
Ok(())
}
fn handle_json_command(
fn handle_toml_command(
config_path: PathBuf,
output_override: Option<String>,
show_example: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if show_example {
let example = CompositionConfig::example();
println!("{}", example.to_json_pretty()?);
println!("\n# Save this to a .json file and run:");
println!("# cargo run --bin musicgen json your_config.json");
println!("{}", example.to_toml_pretty()?);
println!("\n# Save this to a .toml file and run:");
println!("# cargo run --bin musicgen toml your_config.toml");
return Ok(());
}

View File

@ -4,7 +4,12 @@
//! handle timing, and coordinate multiple tracks and patterns.
use crate::bpm_to_samples_per_beat;
use crate::core::{Composition, InstrumentType};
use crate::config::EffectConfig;
use crate::core::Composition;
use crate::effects::{
AudioEffect, BitCrusher, Chorus, Delay, Distortion, EffectsChain, HighPassFilter,
LowPassFilter, Reverb, TapeSaturation, VinylCrackle,
};
use crate::patterns::MelodicPattern;
use crate::synthesis::{PolySynth, Waveform};
use std::collections::VecDeque;
@ -56,17 +61,193 @@ struct TrackState {
pub volume: f32,
pub is_muted: bool,
pub current_notes: Vec<u8>, // Currently playing MIDI notes
pub effects: EffectsChain,
}
impl TrackState {
fn new(waveform: Waveform, max_tracks: usize) -> Self {
fn new(waveform: Waveform, max_tracks: usize, effect_configs: &[EffectConfig]) -> Self {
let effects =
Self::create_effects_chain(effect_configs).unwrap_or_else(|_| EffectsChain::new());
Self {
synth: PolySynth::new(max_tracks, waveform),
volume: 1.0,
is_muted: false,
current_notes: Vec::new(),
effects,
}
}
/// Create an effects chain from effect configurations
fn create_effects_chain(effect_configs: &[EffectConfig]) -> Result<EffectsChain, String> {
let mut effects_chain = EffectsChain::new();
for effect_config in effect_configs {
let effect: Box<dyn AudioEffect> = match effect_config.effect_type.as_str() {
"lowpass" => {
let cutoff = effect_config
.params
.get("cutoff")
.and_then(|v| v.as_float())
.unwrap_or(1000.0) as f32;
let resonance = effect_config
.params
.get("resonance")
.and_then(|v| v.as_float())
.unwrap_or(1.0) as f32;
Box::new(LowPassFilter::new(cutoff, resonance))
}
"highpass" => {
let cutoff = effect_config
.params
.get("cutoff")
.and_then(|v| v.as_float())
.unwrap_or(1000.0) as f32;
let resonance = effect_config
.params
.get("resonance")
.and_then(|v| v.as_float())
.unwrap_or(1.0) as f32;
Box::new(HighPassFilter::new(cutoff, resonance))
}
"delay" => {
let time = effect_config
.params
.get("time")
.and_then(|v| v.as_float())
.unwrap_or(0.3) as f32;
let feedback = effect_config
.params
.get("feedback")
.and_then(|v| v.as_float())
.unwrap_or(0.3) as f32;
let mix = effect_config
.params
.get("mix")
.and_then(|v| v.as_float())
.unwrap_or(0.3) as f32;
Box::new(Delay::new(time, feedback, mix))
}
"reverb" => {
let room_size = effect_config
.params
.get("room_size")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
let damping = effect_config
.params
.get("damping")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
let mix = effect_config
.params
.get("mix")
.and_then(|v| v.as_float())
.unwrap_or(0.3) as f32;
Box::new(Reverb::new(room_size, damping, mix))
}
"distortion" => {
let drive = effect_config
.params
.get("drive")
.and_then(|v| v.as_float())
.unwrap_or(2.0) as f32;
let tone = effect_config
.params
.get("tone")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
Box::new(Distortion::new(drive, tone))
}
"chorus" => {
let rate = effect_config
.params
.get("rate")
.and_then(|v| v.as_float())
.unwrap_or(1.0) as f32;
let depth = effect_config
.params
.get("depth")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
let mix = effect_config
.params
.get("mix")
.and_then(|v| v.as_float())
.unwrap_or(0.3) as f32;
let layers = effect_config
.params
.get("layers")
.and_then(|v| v.as_integer())
.unwrap_or(3) as usize;
Box::new(Chorus::new(rate, depth, mix, layers))
}
"vinyl" => {
let intensity = effect_config
.params
.get("intensity")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
let frequency = effect_config
.params
.get("frequency")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
let mix = effect_config
.params
.get("mix")
.and_then(|v| v.as_float())
.unwrap_or(0.3) as f32;
Box::new(VinylCrackle::new(intensity, frequency, mix))
}
"tape" => {
let drive = effect_config
.params
.get("drive")
.and_then(|v| v.as_float())
.unwrap_or(2.0) as f32;
let warmth = effect_config
.params
.get("warmth")
.and_then(|v| v.as_float())
.unwrap_or(0.7) as f32;
let mix = effect_config
.params
.get("mix")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
Box::new(TapeSaturation::new(drive, warmth, mix))
}
"bitcrusher" => {
let bit_depth = effect_config
.params
.get("bit_depth")
.and_then(|v| v.as_float())
.unwrap_or(8.0) as f32;
let sample_rate_reduction = effect_config
.params
.get("sample_rate_reduction")
.and_then(|v| v.as_float())
.unwrap_or(2.0) as f32;
let mix = effect_config
.params
.get("mix")
.and_then(|v| v.as_float())
.unwrap_or(0.5) as f32;
Box::new(BitCrusher::new(bit_depth, sample_rate_reduction, mix))
}
_ => {
return Err(format!(
"Unknown effect type: {}",
effect_config.effect_type
));
}
};
effects_chain.add_effect(effect);
}
Ok(effects_chain)
}
}
impl Sequencer {
@ -98,16 +279,9 @@ impl Sequencer {
// Create track states for each track in the composition
for track in &composition.tracks {
let waveform = match track.instrument_type {
InstrumentType::Lead => Waveform::Sawtooth,
InstrumentType::Bass => Waveform::Square,
InstrumentType::Pad => Waveform::Sine,
InstrumentType::Arp => Waveform::Triangle,
InstrumentType::Percussion => Waveform::Noise,
InstrumentType::Drone => Waveform::Sine,
};
let waveform = track.waveform;
let mut track_state = TrackState::new(waveform, 8);
let mut track_state = TrackState::new(waveform, 8, &track.effect_configs);
track_state.volume = track.volume;
self.tracks.push(track_state);
}
@ -258,7 +432,8 @@ impl Sequencer {
for track in &mut self.tracks {
if !track.is_muted {
let track_sample = track.synth.next_sample();
sample += track_sample * track.volume;
let processed_sample = track.effects.process_sample(track_sample);
sample += processed_sample * track.volume;
}
}

View File

@ -14,6 +14,7 @@ pub enum Waveform {
Sawtooth,
Triangle,
Noise,
Piano,
}
/// A basic oscillator that generates waveforms at a given frequency
@ -67,6 +68,7 @@ impl Oscillator {
Waveform::Sawtooth => self.sawtooth_wave(),
Waveform::Triangle => self.triangle_wave(),
Waveform::Noise => self.noise_wave(),
Waveform::Piano => self.piano_wave(),
};
// Advance phase
@ -115,6 +117,37 @@ impl Oscillator {
let mut rng = rand::thread_rng();
rng.gen_range(-1.0..1.0)
}
fn piano_wave(&self) -> f32 {
// Piano sound using additive synthesis with harmonics
// Fundamental frequency with slight attack shaping
let fundamental = self.phase.sin() * 1.0;
// Piano-specific harmonic series with realistic amplitudes
let harmonic2 = (self.phase * 2.0).sin() * 0.4; // Octave - prominent in piano
let harmonic3 = (self.phase * 3.0).sin() * 0.3; // Fifth - moderate
let harmonic4 = (self.phase * 4.0).sin() * 0.2; // Second octave
let harmonic5 = (self.phase * 5.0).sin() * 0.15; // Major third
let harmonic6 = (self.phase * 6.0).sin() * 0.1; // Fifth again
let harmonic7 = (self.phase * 7.0).sin() * 0.08; // Minor seventh
let harmonic8 = (self.phase * 8.0).sin() * 0.06; // Third octave
// Combine harmonics
let mut sample = fundamental
+ harmonic2
+ harmonic3
+ harmonic4
+ harmonic5
+ harmonic6
+ harmonic7
+ harmonic8;
// Apply subtle saturation for warmth
sample = sample * (1.0 + 0.05 * sample.abs());
// Piano-appropriate volume level
sample * 0.6
}
}
/// An envelope generator for controlling amplitude over time