| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | |
|
| | 5 | | namespace MusicTheory.Theory.Harmony; |
| | 6 | |
|
| | 7 | | public readonly record struct FourPartVoicing(int S, int A, int T, int B) |
| | 8 | | { |
| | 9 | | public IEnumerable<(Voice voice, int midi)> Notes() => new[] |
| | 10 | | { |
| | 11 | | (Voice.Soprano, S), (Voice.Alto, A), (Voice.Tenor, T), (Voice.Bass, B) |
| | 12 | | }; |
| | 13 | |
|
| | 14 | | public bool IsOrderedTopDown() => S >= A && A >= T && T >= B; |
| | 15 | | } |
| | 16 | |
|
| | 17 | | public static class VoiceLeadingRules |
| | 18 | | { |
| | 19 | | public static bool HasSpacingViolations(FourPartVoicing v) |
| | 20 | | { |
| | 21 | | // Spacing: S-A and A-T <= octave; T-B no hard limit (common rule: <= octave too, but looser) |
| 206 | 22 | | return (v.S - v.A) > 12 || (v.A - v.T) > 12; |
| | 23 | | } |
| | 24 | |
|
| | 25 | | public static bool HasRangeViolation(FourPartVoicing v) |
| | 26 | | { |
| 1769 | 27 | | foreach (var (voice, midi) in v.Notes()) |
| | 28 | | { |
| 725 | 29 | | var r = VoiceRanges.ForVoice(voice); |
| 818 | 30 | | if (!r.InWarnRange(midi)) return true; // hard out of warn range |
| | 31 | | } |
| 113 | 32 | | return false; |
| 93 | 33 | | } |
| | 34 | |
|
| | 35 | | public static bool HasOverlap(FourPartVoicing prev, FourPartVoicing next) |
| | 36 | | { |
| | 37 | | // No voice overlapping: each voice must stay within its neighbors' previous positions. |
| 93 | 38 | | return next.S < prev.A || next.A > prev.S || next.A < prev.T || next.T > prev.A || next.T < prev.B || next.B > p |
| | 39 | | } |
| | 40 | |
|
| | 41 | | public static bool HasParallelPerfects(FourPartVoicing a, FourPartVoicing b) |
| | 42 | | { |
| | 43 | | // Check pairs for P5/P8 parallels by motion direction and interval equality. |
| 93 | 44 | | var pairs = new (int a1, int a2, int b1, int b2)[] |
| 93 | 45 | | { |
| 93 | 46 | | (a.S, a.A, b.S, b.A), (a.S, a.T, b.S, b.T), (a.S, a.B, b.S, b.B), |
| 93 | 47 | | (a.A, a.T, b.A, b.T), (a.A, a.B, b.A, b.B), |
| 93 | 48 | | (a.T, a.B, b.T, b.B) |
| 93 | 49 | | }; |
| | 50 | |
|
| 1171 | 51 | | foreach (var (x1,y1,x2,y2) in pairs) |
| | 52 | | { |
| 506 | 53 | | var intv1 = Math.Abs((x1 - y1) % 12); |
| 506 | 54 | | var intv2 = Math.Abs((x2 - y2) % 12); |
| | 55 | | // Treat only P8 and P5 as perfect for parallel checks |
| 506 | 56 | | bool isPerfect1 = (intv1 % 12) == 0 || (intv1 % 12) == 7; |
| 506 | 57 | | bool isPerfect2 = (intv2 % 12) == 0 || (intv2 % 12) == 7; |
| 506 | 58 | | if (!isPerfect1 || !isPerfect2) continue; |
| 71 | 59 | | int dir1 = Math.Sign(x2 - x1); |
| 71 | 60 | | int dir2 = Math.Sign(y2 - y1); |
| 98 | 61 | | if (dir1 == dir2 && dir1 != 0) return true; |
| | 62 | | } |
| 66 | 63 | | return false; |
| | 64 | | } |
| | 65 | | } |