@hum2music/arrangement 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@hum2music/arrangement",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Rule-based Piano/Bass/Drums/Guitar arrangers and style profiles for Hum2Music.",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://github.com/hum2music/hum2music",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/hum2music/hum2music.git",
10
+ "directory": "packages/arrangement"
11
+ },
12
+ "type": "module",
13
+ "main": "./src/index.ts",
14
+ "types": "./src/index.ts",
15
+ "files": ["src"],
16
+ "dependencies": {
17
+ "@hum2music/core": "0.1.0",
18
+ "@hum2music/music-theory": "0.1.0",
19
+ "@hum2music/expression": "0.1.0",
20
+ "@hum2music/harmony": "0.1.0"
21
+ }
22
+ }
package/src/bass.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { ChordEvent, NoteEvent } from "@hum2music/core";
2
+ import { parseChordSymbol, pitchClassOf } from "@hum2music/music-theory";
3
+ import type { ArrangementContext, InstrumentArranger } from "./types";
4
+
5
+ const ROOT_OCTAVE_BASE_PITCH = 36; // the C in "C2", i.e. lowestPitchInOctave(0, ...) = 36
6
+ const VELOCITY = 75;
7
+ const APPROACH_VELOCITY = 65;
8
+
9
+ /**
10
+ * Root-driven bass: holds the chord root through beats 1-2, repeats it on
11
+ * beat 3, then on beat 4 plays a chromatic approach note into the next
12
+ * chord's root (or holds the root again on the final bar). Lands on the
13
+ * same strong beats (1 and 3) as the drum arranger's kick, approximating
14
+ * "kick sync" without reading the drum track directly (PRD §9.3).
15
+ */
16
+ export function generateBassArrangement(chords: ChordEvent[]): NoteEvent[] {
17
+ const notes: NoteEvent[] = [];
18
+
19
+ chords.forEach((chord, index) => {
20
+ const parsed = parseChordSymbol(chord.symbol);
21
+ if (!parsed) return;
22
+
23
+ const quarter = chord.durationBeats / 4;
24
+ const rootPitch = lowestPitchInOctave(parsed.root);
25
+
26
+ notes.push(note(rootPitch, chord.startBeat, quarter * 2, VELOCITY));
27
+ notes.push(note(rootPitch, chord.startBeat + quarter * 2, quarter, VELOCITY));
28
+
29
+ const nextChord = chords[index + 1];
30
+ const nextParsed = nextChord ? parseChordSymbol(nextChord.symbol) : null;
31
+ const lastBeatPitch = nextParsed ? lowestPitchInOctave(nextParsed.root) - 1 : rootPitch;
32
+ const lastBeatVelocity = nextParsed ? APPROACH_VELOCITY : VELOCITY;
33
+
34
+ notes.push(note(lastBeatPitch, chord.startBeat + quarter * 3, quarter, lastBeatVelocity));
35
+ });
36
+
37
+ return notes;
38
+ }
39
+
40
+ function lowestPitchInOctave(pitchClass: number): number {
41
+ return ROOT_OCTAVE_BASE_PITCH + pitchClassOf(pitchClass);
42
+ }
43
+
44
+ function note(pitch: number, startBeat: number, durationBeats: number, velocity: number): NoteEvent {
45
+ return { pitch, startBeat, durationBeats, velocity };
46
+ }
47
+
48
+ export const bassArranger: InstrumentArranger = {
49
+ generate: (context: ArrangementContext) => generateBassArrangement(context.chords),
50
+ };
package/src/compose.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { ChordEvent, NoteEvent, SongSection } from "@hum2music/core";
2
+ import { generateBassArrangement } from "./bass";
3
+ import { generateDrumArrangement } from "./drums";
4
+ import { generateGuitarArrangement } from "./guitar";
5
+ import { generatePianoArrangement } from "./piano";
6
+ import { applySectionalHumanization } from "./sectional-humanize";
7
+ import { resolveStyleProfile, type StyleProfile } from "./style";
8
+
9
+ export const ARRANGEABLE_INSTRUMENTS = ["piano", "bass", "drums", "guitar"] as const;
10
+ export type ArrangeableInstrument = (typeof ARRANGEABLE_INSTRUMENTS)[number];
11
+
12
+ export interface ComposedTrack {
13
+ instrument: "melody" | ArrangeableInstrument;
14
+ notes: NoteEvent[];
15
+ channel: number;
16
+ programNumber?: number;
17
+ }
18
+
19
+ export interface ComposeTracksOptions {
20
+ melody: NoteEvent[];
21
+ chords: ChordEvent[];
22
+ instruments?: Iterable<ArrangeableInstrument>;
23
+ humanizeAmount?: number;
24
+ seed?: number;
25
+ bpm: number;
26
+ /** Defaults to the "pop" profile; see style.ts. Governs pattern choice, voicing extensions, drum density, and GM program numbers. */
27
+ style?: StyleProfile | string;
28
+ /** When given (and non-empty), humanization intensity varies per bar by repeated section (see sectional-humanize.ts) instead of one flat amount. */
29
+ songSections?: SongSection[];
30
+ }
31
+
32
+ const DEFAULT_SEED = 42;
33
+ const CHANNELS: Record<ArrangeableInstrument, number> = { piano: 1, bass: 2, drums: 9, guitar: 3 };
34
+
35
+ /**
36
+ * Builds the melody track plus any requested arranged instrument tracks
37
+ * (piano/bass/drums/guitar), optionally humanized per-track. Shared by the
38
+ * CLI's `generate` command and the web UI's export endpoints so
39
+ * arrangement + humanization logic lives in exactly one place.
40
+ */
41
+ export function composeTracks(options: ComposeTracksOptions): ComposedTrack[] {
42
+ const style = resolveStyle(options.style);
43
+ const included = new Set(options.instruments ?? ARRANGEABLE_INSTRUMENTS);
44
+
45
+ const tracks: ComposedTrack[] = [{ instrument: "melody", notes: options.melody, channel: 0 }];
46
+
47
+ if (included.has("piano")) {
48
+ const notes = generatePianoArrangement(options.chords, {
49
+ pattern: style.pianoPattern,
50
+ allowExtensions: style.pianoAllowExtensions,
51
+ });
52
+ tracks.push({ instrument: "piano", notes, channel: CHANNELS.piano, programNumber: style.programNumbers.piano });
53
+ }
54
+ if (included.has("bass")) {
55
+ const notes = generateBassArrangement(options.chords);
56
+ tracks.push({ instrument: "bass", notes, channel: CHANNELS.bass, programNumber: style.programNumbers.bass });
57
+ }
58
+ if (included.has("drums")) {
59
+ const notes = generateDrumArrangement(options.chords, { density: style.drumDensity });
60
+ tracks.push({ instrument: "drums", notes, channel: CHANNELS.drums });
61
+ }
62
+ if (included.has("guitar")) {
63
+ const notes = generateGuitarArrangement(options.chords, { pattern: style.guitarPattern });
64
+ tracks.push({ instrument: "guitar", notes, channel: CHANNELS.guitar, programNumber: style.programNumbers.guitar });
65
+ }
66
+
67
+ const humanizeAmount = options.humanizeAmount ?? 0;
68
+ if (humanizeAmount <= 0) return tracks;
69
+
70
+ const seed = options.seed ?? DEFAULT_SEED;
71
+ const sections = options.songSections ?? [];
72
+ return tracks.map((track) => ({
73
+ ...track,
74
+ notes: applySectionalHumanization(track.notes, sections, {
75
+ baseAmount: Math.min(1, humanizeAmount * style.humanizeMultiplier),
76
+ seed,
77
+ bpm: options.bpm,
78
+ instrument: track.instrument,
79
+ }),
80
+ }));
81
+ }
82
+
83
+ function resolveStyle(style: StyleProfile | string | undefined): StyleProfile {
84
+ return typeof style === "string" || style === undefined ? resolveStyleProfile(style) : style;
85
+ }
package/src/drums.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { ChordEvent, NoteEvent } from "@hum2music/core";
2
+ import type { ArrangementContext, InstrumentArranger } from "./types";
3
+
4
+ /** General MIDI percussion map note numbers. */
5
+ export const KICK_PITCH = 36;
6
+ export const SNARE_PITCH = 38;
7
+ export const CLOSED_HIHAT_PITCH = 42;
8
+
9
+ const HIT_DURATION_BEATS = 0.1;
10
+ const VELOCITY = { kick: 100, snare: 95, hihat: 70 };
11
+
12
+ export interface DrumArrangementOptions {
13
+ /** "sparse" drops to a half-time feel (kick on 1 only, snare on 3 only, quarter-note hi-hat) for e.g. a ballad. */
14
+ density?: "normal" | "sparse";
15
+ }
16
+
17
+ /**
18
+ * One bar of the basic 4/4 backbeat per bar (kick on 1 & 3, snare on 2 & 4,
19
+ * eighth-note closed hi-hat throughout), per PRD §10.3's example pattern.
20
+ * `density: "sparse"` is the one documented variation beyond this fixed
21
+ * template; further variation is left to future work.
22
+ */
23
+ export function generateDrumArrangement(chords: ChordEvent[], options: DrumArrangementOptions = {}): NoteEvent[] {
24
+ const density = options.density ?? "normal";
25
+ const notes: NoteEvent[] = [];
26
+
27
+ for (const chord of chords) {
28
+ const quarter = chord.durationBeats / 4;
29
+ const eighth = quarter / 2;
30
+
31
+ if (density === "sparse") {
32
+ notes.push(hit(KICK_PITCH, chord.startBeat, VELOCITY.kick));
33
+ notes.push(hit(SNARE_PITCH, chord.startBeat + quarter * 2, VELOCITY.snare));
34
+ for (let quarterIndex = 0; quarterIndex < 4; quarterIndex++) {
35
+ notes.push(hit(CLOSED_HIHAT_PITCH, chord.startBeat + quarterIndex * quarter, VELOCITY.hihat));
36
+ }
37
+ continue;
38
+ }
39
+
40
+ notes.push(hit(KICK_PITCH, chord.startBeat, VELOCITY.kick));
41
+ notes.push(hit(KICK_PITCH, chord.startBeat + quarter * 2, VELOCITY.kick));
42
+ notes.push(hit(SNARE_PITCH, chord.startBeat + quarter, VELOCITY.snare));
43
+ notes.push(hit(SNARE_PITCH, chord.startBeat + quarter * 3, VELOCITY.snare));
44
+
45
+ for (let eighthIndex = 0; eighthIndex < 8; eighthIndex++) {
46
+ notes.push(hit(CLOSED_HIHAT_PITCH, chord.startBeat + eighthIndex * eighth, VELOCITY.hihat));
47
+ }
48
+ }
49
+
50
+ return notes;
51
+ }
52
+
53
+ function hit(pitch: number, startBeat: number, velocity: number): NoteEvent {
54
+ return { pitch, startBeat, durationBeats: HIT_DURATION_BEATS, velocity };
55
+ }
56
+
57
+ export const drumsArranger: InstrumentArranger = {
58
+ generate: (context: ArrangementContext) => generateDrumArrangement(context.chords),
59
+ };
package/src/guitar.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { ChordEvent, NoteEvent } from "@hum2music/core";
2
+ import { chordPitchClasses, parseChordSymbol, pitchClassOf } from "@hum2music/music-theory";
3
+ import type { ArrangementContext, InstrumentArranger } from "./types";
4
+
5
+ /** Standard tuning, low to high string (open-string MIDI pitches): E2 A2 D3 G3 B3 E4. */
6
+ const OPEN_STRING_PITCHES = [40, 45, 50, 55, 59, 64];
7
+ /** Max frets apart a single hand position can comfortably cover. */
8
+ const MAX_FRET_SPAN = 3;
9
+ /** How far up the neck to search for a playable voicing before giving up on a string. */
10
+ const MAX_FRET_SEARCH = 9;
11
+
12
+ const DOWN_STROKE_VELOCITY = 80;
13
+ const UP_STROKE_VELOCITY = 60;
14
+ const STROKE_DURATION_FRACTION = 0.9;
15
+
16
+ export interface GuitarArrangementOptions {
17
+ pattern?: "strum" | "arpeggio";
18
+ }
19
+
20
+ /**
21
+ * Finds a playable chord shape (one fret-window position, each string
22
+ * either muted or fretted within that window) and plays it as either a
23
+ * strummed down/up pattern or a simple arpeggio. Never generates a shape
24
+ * that would require a fret span wider than a hand can cover (PRD §9.4).
25
+ */
26
+ export function generateGuitarArrangement(chords: ChordEvent[], options: GuitarArrangementOptions = {}): NoteEvent[] {
27
+ const pattern = options.pattern ?? "strum";
28
+ const notes: NoteEvent[] = [];
29
+
30
+ for (const chord of chords) {
31
+ const parsed = parseChordSymbol(chord.symbol);
32
+ if (!parsed) continue;
33
+
34
+ const chordTones = new Set(chordPitchClasses(parsed.root, parsed.quality));
35
+ const voicing = findPlayableVoicing(chordTones);
36
+ if (voicing.length === 0) continue;
37
+
38
+ const quarter = chord.durationBeats / 4;
39
+ if (pattern === "arpeggio") {
40
+ voicing.forEach((pitch, index) => {
41
+ const beatOffset = (index % 4) * quarter;
42
+ notes.push({
43
+ pitch,
44
+ startBeat: chord.startBeat + beatOffset,
45
+ durationBeats: quarter * STROKE_DURATION_FRACTION,
46
+ velocity: DOWN_STROKE_VELOCITY,
47
+ });
48
+ });
49
+ continue;
50
+ }
51
+
52
+ for (let beatIndex = 0; beatIndex < 4; beatIndex++) {
53
+ const isDownStroke = beatIndex % 2 === 0;
54
+ const velocity = isDownStroke ? DOWN_STROKE_VELOCITY : UP_STROKE_VELOCITY;
55
+ for (const pitch of voicing) {
56
+ notes.push({
57
+ pitch,
58
+ startBeat: chord.startBeat + beatIndex * quarter,
59
+ durationBeats: quarter * STROKE_DURATION_FRACTION,
60
+ velocity,
61
+ });
62
+ }
63
+ }
64
+ }
65
+
66
+ return notes;
67
+ }
68
+
69
+ /** Scans fret-window positions and returns the one that sounds the most strings. */
70
+ function findPlayableVoicing(chordTones: Set<number>): number[] {
71
+ let best: number[] = [];
72
+
73
+ for (let windowStart = 0; windowStart <= MAX_FRET_SEARCH; windowStart++) {
74
+ const pitches: number[] = [];
75
+ for (const openPitch of OPEN_STRING_PITCHES) {
76
+ const fret = findFretInWindow(openPitch, windowStart, chordTones);
77
+ if (fret !== null) pitches.push(openPitch + fret);
78
+ }
79
+ if (pitches.length > best.length) best = pitches;
80
+ if (best.length === OPEN_STRING_PITCHES.length) break;
81
+ }
82
+
83
+ return best;
84
+ }
85
+
86
+ function findFretInWindow(openPitch: number, windowStart: number, chordTones: Set<number>): number | null {
87
+ const frets = windowStart === 0 ? [0, 1, 2, 3] : range(windowStart, windowStart + MAX_FRET_SPAN);
88
+ for (const fret of frets) {
89
+ if (chordTones.has(pitchClassOf(openPitch + fret))) return fret;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function range(start: number, end: number): number[] {
95
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
96
+ }
97
+
98
+ export const guitarArranger: InstrumentArranger = {
99
+ generate: (context: ArrangementContext) => generateGuitarArrangement(context.chords),
100
+ };
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./types";
2
+ export * from "./piano";
3
+ export * from "./bass";
4
+ export * from "./drums";
5
+ export * from "./guitar";
6
+ export * from "./style";
7
+ export * from "./sectional-humanize";
8
+ export * from "./compose";
package/src/piano.ts ADDED
@@ -0,0 +1,105 @@
1
+ import type { ChordEvent, NoteEvent } from "@hum2music/core";
2
+ import { chordPitchClasses, parseChordSymbol, pitchClassOf } from "@hum2music/music-theory";
3
+ import type { ArrangementContext, InstrumentArranger } from "./types";
4
+
5
+ const MIN_PITCH = 48; // C3
6
+ const MAX_PITCH = 72; // C5
7
+ const VELOCITY = 70;
8
+ const NINTH_INTERVAL_SEMITONES = 2;
9
+
10
+ export interface PianoArrangementOptions {
11
+ pattern?: "block" | "arpeggio";
12
+ /** Lets the voicing search also place the 9th, for a richer/"more advanced" sound (docs/specs/style-profiles.md). */
13
+ allowExtensions?: boolean;
14
+ }
15
+
16
+ /**
17
+ * For each bar, picks the voicing (octave placement of the chord's tones,
18
+ * optionally plus the 9th) within the piano's range with the smallest total
19
+ * movement from the previous bar's voicing, rather than always using the
20
+ * same inversion (PRD §9.2). Played either as a sustained block chord or a
21
+ * simple ascending arpeggio across the bar.
22
+ */
23
+ export function generatePianoArrangement(chords: ChordEvent[], options: PianoArrangementOptions = {}): NoteEvent[] {
24
+ const pattern = options.pattern ?? "block";
25
+ const notes: NoteEvent[] = [];
26
+ let previousVoicing: number[] | null = null;
27
+
28
+ for (const chord of chords) {
29
+ const parsed = parseChordSymbol(chord.symbol);
30
+ if (!parsed) continue;
31
+
32
+ const toneClasses = chordPitchClasses(parsed.root, parsed.quality);
33
+ const availableTones = options.allowExtensions
34
+ ? [...toneClasses, pitchClassOf(parsed.root + NINTH_INTERVAL_SEMITONES)]
35
+ : toneClasses;
36
+
37
+ const voicing = chooseVoicing(availableTones, previousVoicing);
38
+ previousVoicing = voicing;
39
+
40
+ if (pattern === "arpeggio") {
41
+ const quarter = chord.durationBeats / 4;
42
+ voicing.forEach((pitch, index) => {
43
+ notes.push({
44
+ pitch,
45
+ startBeat: chord.startBeat + (index % 4) * quarter,
46
+ durationBeats: quarter * 0.9,
47
+ velocity: VELOCITY,
48
+ });
49
+ });
50
+ continue;
51
+ }
52
+
53
+ for (const pitch of voicing) {
54
+ notes.push({ pitch, startBeat: chord.startBeat, durationBeats: chord.durationBeats, velocity: VELOCITY });
55
+ }
56
+ }
57
+
58
+ return notes;
59
+ }
60
+
61
+ function chooseVoicing(toneClasses: number[], previousVoicing: number[] | null): number[] {
62
+ const candidates = enumerateVoicingsInRange(toneClasses);
63
+ if (candidates.length === 0) return toneClasses.map((pc) => pc + 60);
64
+ if (!previousVoicing) return candidates[0];
65
+
66
+ let best = candidates[0];
67
+ let bestMovement = totalVoiceMovement(previousVoicing, best);
68
+ for (const candidate of candidates.slice(1)) {
69
+ const movement = totalVoiceMovement(previousVoicing, candidate);
70
+ if (movement < bestMovement) {
71
+ best = candidate;
72
+ bestMovement = movement;
73
+ }
74
+ }
75
+ return best;
76
+ }
77
+
78
+ function enumerateVoicingsInRange(toneClasses: number[]): number[][] {
79
+ const optionsPerTone = toneClasses.map((pitchClass) => {
80
+ const options: number[] = [];
81
+ for (let pitch = MIN_PITCH; pitch <= MAX_PITCH; pitch++) {
82
+ if (pitchClassOf(pitch) === pitchClass) options.push(pitch);
83
+ }
84
+ return options;
85
+ });
86
+
87
+ let combinations: number[][] = [[]];
88
+ for (const options of optionsPerTone) {
89
+ const next: number[][] = [];
90
+ for (const combo of combinations) {
91
+ for (const option of options) next.push([...combo, option]);
92
+ }
93
+ combinations = next;
94
+ }
95
+
96
+ return combinations.map((combo) => [...combo].sort((a, b) => a - b));
97
+ }
98
+
99
+ function totalVoiceMovement(previous: number[], candidate: number[]): number {
100
+ return candidate.reduce((sum, pitch) => sum + Math.min(...previous.map((p) => Math.abs(p - pitch))), 0);
101
+ }
102
+
103
+ export const pianoArranger: InstrumentArranger = {
104
+ generate: (context: ArrangementContext) => generatePianoArrangement(context.chords),
105
+ };
@@ -0,0 +1,59 @@
1
+ import type { NoteEvent, SongSection } from "@hum2music/core";
2
+ import { deriveSectionalHumanizeAmount, humanizeMelody } from "@hum2music/expression";
3
+
4
+ const BEATS_PER_BAR = 4;
5
+
6
+ export interface SectionalHumanizeOptions {
7
+ baseAmount: number;
8
+ seed: number;
9
+ bpm: number;
10
+ instrument: string;
11
+ }
12
+
13
+ /**
14
+ * Humanizes a track bar-by-bar, varying intensity by song section (via
15
+ * deriveSectionalHumanizeAmount) instead of applying one flat amount to
16
+ * the whole track. Note: each bar re-seeds its own random stream from the
17
+ * same track sub-seed, so notes at the same position-within-bar across
18
+ * different bars can draw correlated (not fully independent) jitter —
19
+ * still fully deterministic/reproducible, just less varied than a single
20
+ * continuous stream would be. Documented tradeoff, not a bug.
21
+ */
22
+ export function applySectionalHumanization(
23
+ notes: NoteEvent[],
24
+ sections: SongSection[],
25
+ options: SectionalHumanizeOptions,
26
+ ): NoteEvent[] {
27
+ if (options.baseAmount <= 0) return notes;
28
+ if (sections.length === 0) {
29
+ return humanizeMelody(notes, {
30
+ seed: options.seed,
31
+ humanizeAmount: options.baseAmount,
32
+ bpm: options.bpm,
33
+ instrument: options.instrument,
34
+ });
35
+ }
36
+
37
+ const notesByBar = new Map<number, NoteEvent[]>();
38
+ for (const note of notes) {
39
+ const barIndex = Math.floor(note.startBeat / BEATS_PER_BAR);
40
+ const bucket = notesByBar.get(barIndex) ?? [];
41
+ bucket.push(note);
42
+ notesByBar.set(barIndex, bucket);
43
+ }
44
+
45
+ const result: NoteEvent[] = [];
46
+ for (const [barIndex, barNotes] of notesByBar) {
47
+ const amount = deriveSectionalHumanizeAmount(options.baseAmount, sections, barIndex);
48
+ result.push(
49
+ ...humanizeMelody(barNotes, {
50
+ seed: options.seed,
51
+ humanizeAmount: amount,
52
+ bpm: options.bpm,
53
+ instrument: options.instrument,
54
+ }),
55
+ );
56
+ }
57
+
58
+ return result;
59
+ }
package/src/style.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { ProgressionSearchWeights } from "@hum2music/harmony";
2
+
3
+ export type StyleName = "pop" | "ballad";
4
+ export const STYLE_NAMES: StyleName[] = ["pop", "ballad"];
5
+
6
+ export interface StyleProfile {
7
+ name: StyleName;
8
+ /** Passed through to searchChordProgression; style-dependent weighting stands in for the S term (PRD §8.3). */
9
+ harmonyWeights: ProgressionSearchWeights;
10
+ pianoPattern: "block" | "arpeggio";
11
+ /** Whether the piano voicing may add the 9th as an extra available tone, beyond the chord's own tones. */
12
+ pianoAllowExtensions: boolean;
13
+ guitarPattern: "strum" | "arpeggio";
14
+ drumDensity: "normal" | "sparse";
15
+ humanizeMultiplier: number;
16
+ programNumbers: { piano: number; bass: number; guitar: number };
17
+ }
18
+
19
+ /**
20
+ * A style is a weighting/parameter profile layered on top of the existing
21
+ * engines, not a new generation algorithm, to keep everything deterministic
22
+ * (PRD §15.1) and the engines swappable (docs/specs/style-profiles.md).
23
+ * Proposed values below are unvalidated starting points.
24
+ */
25
+ export const STYLE_PROFILES: Record<StyleName, StyleProfile> = {
26
+ pop: {
27
+ name: "pop",
28
+ harmonyWeights: { melodyFit: 0.5, harmonicNaturalness: 0.3, voiceLeading: 0.2 },
29
+ pianoPattern: "block",
30
+ pianoAllowExtensions: false,
31
+ guitarPattern: "strum",
32
+ drumDensity: "normal",
33
+ humanizeMultiplier: 1,
34
+ programNumbers: { piano: 0, bass: 33, guitar: 29 },
35
+ },
36
+ ballad: {
37
+ name: "ballad",
38
+ harmonyWeights: { melodyFit: 0.55, harmonicNaturalness: 0.25, voiceLeading: 0.2 },
39
+ pianoPattern: "arpeggio",
40
+ pianoAllowExtensions: true,
41
+ guitarPattern: "arpeggio",
42
+ drumDensity: "sparse",
43
+ humanizeMultiplier: 0.7,
44
+ programNumbers: { piano: 0, bass: 32, guitar: 24 },
45
+ },
46
+ };
47
+
48
+ export function resolveStyleProfile(name: string | undefined): StyleProfile {
49
+ if (name && isStyleName(name)) return STYLE_PROFILES[name];
50
+ return STYLE_PROFILES.pop;
51
+ }
52
+
53
+ function isStyleName(name: string): name is StyleName {
54
+ return (STYLE_NAMES as string[]).includes(name);
55
+ }
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { ChordEvent, NoteEvent } from "@hum2music/core";
2
+
3
+ /** Everything an arranger needs: a chord progression (one chord per bar, per harmony's grouping). */
4
+ export interface ArrangementContext {
5
+ chords: ChordEvent[];
6
+ }
7
+
8
+ /** Common interface so each instrument's arranger is independently swappable (PRD §9.1). */
9
+ export interface InstrumentArranger {
10
+ generate(context: ArrangementContext): NoteEvent[];
11
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.