Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 79 additions & 41 deletions src/voiceLeading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,29 @@ import * as note from './note';
import type * as pitch from './pitch';

import { Music21Object } from './base';
import { Music21Exception } from './exceptions21';

// imports just for type checking
import { ConcreteScale } from './scale';


const intervalCache = [];
const intervalCache: interval.Interval[] = [];

export const MotionType = {
antiParallel: 'Anti-Parallel',
contrary: 'Contrary',
noMotion: 'No Motion',
oblique: 'Oblique',
parallel: 'Parallel',
similar: 'Similar',
};
type IntervalLoose =
interval.Interval
| interval.DiatonicInterval
| interval.GenericInterval
| number
| string;

export enum MotionType {
antiParallel = 'Anti-Parallel',
contrary = 'Contrary',
noMotion = 'No Motion',
oblique = 'Oblique',
parallel = 'Parallel',
similar = 'Similar',
}

export class VoiceLeadingQuartet extends Music21Object {
static get className() { return 'music21.voiceLeading.VoiceLeadingQuartet'; }
Expand All @@ -47,7 +55,7 @@ export class VoiceLeadingQuartet extends Music21Object {
v1n2?: note.Note,
v2n1?: note.Note,
v2n2?: note.Note,
analyticKey?: key.Key
analyticKey?: key.Key | string
) {
super();
if (!intervalCache.length) {
Expand Down Expand Up @@ -139,13 +147,18 @@ export class VoiceLeadingQuartet extends Music21Object {
return this._key;
}

// turning off string entry because of Typescript deficiency
set key(keyValue: key.Key) {
// if (typeof keyValue === 'string') {
// keyValue = new key.Key(
// key.convertKeyStringToMusic21KeyString(keyValue)
// );
// }
set key(keyValue: key.Key | string) {
if (typeof keyValue === 'string') {
try {
keyValue = new key.Key(
key.convertKeyStringToMusic21KeyString(keyValue)
);
} catch {
throw new Music21Exception(
`got a key signature string that is not supported: ${keyValue}`
);
}
}
this._key = keyValue;
}

Expand All @@ -160,17 +173,20 @@ export class VoiceLeadingQuartet extends Music21Object {
];
}

motionType() {
/**
* Returns the motion type, optionally classifying anti-parallel motion distinctly.
*/
motionType(allowAntiParallel: boolean = false): MotionType | undefined {
if (this.obliqueMotion()) {
return MotionType.oblique;
} else if (this.parallelMotion()) {
return MotionType.parallel;
} else if (this.similarMotion()) {
return MotionType.similar;
} else if (allowAntiParallel && this.antiParallelMotion()) {
return MotionType.antiParallel;
} else if (this.contraryMotion()) {
return MotionType.contrary;
} else if (this.antiParallelMotion()) {
return MotionType.antiParallel;
} else if (this.noMotion()) {
return MotionType.noMotion;
}
Expand Down Expand Up @@ -211,27 +227,57 @@ export class VoiceLeadingQuartet extends Music21Object {
}
}

parallelMotion(requiredInterval: interval.Interval|string|undefined=undefined): boolean {
/**
* Returns true for parallel motion, optionally treating octave-displaced parallels as equivalent.
*/
parallelMotion(
requiredInterval: IntervalLoose | undefined = undefined,
allowOctaveDisplacement: boolean = false
): boolean {
const firstGeneric = this.vIntervals[0].generic;
const secondGeneric = this.vIntervals[1].generic;
if (!this.similarMotion()) {
return false;
}
if (
this.vIntervals[0].directedSimpleName
!== this.vIntervals[1].directedSimpleName
firstGeneric.directed !== secondGeneric.directed
&& !allowOctaveDisplacement
) {
return false;
}
if (
firstGeneric.semiSimpleUndirected
!== secondGeneric.semiSimpleUndirected
) {
return false;
}
if (requiredInterval === undefined) {
return true;
}
if (
requiredInterval instanceof interval.GenericInterval
|| typeof requiredInterval === 'number'
) {
const genericInterval = typeof requiredInterval === 'number'
? new interval.GenericInterval(requiredInterval)
: requiredInterval;
return (
firstGeneric.semiSimpleUndirected
=== genericInterval.semiSimpleUndirected
);
}
let specificInterval: interval.Interval;
if (typeof requiredInterval === 'string') {
requiredInterval = new interval.Interval(requiredInterval);
}
if (this.vIntervals[0].simpleName === requiredInterval.simpleName) {
return true;
specificInterval = new interval.Interval(requiredInterval);
} else if (requiredInterval instanceof interval.Interval) {
specificInterval = requiredInterval;
} else {
return false;
specificInterval = new interval.Interval(requiredInterval);
}
return (
this.vIntervals[0].semiSimpleName === specificInterval.semiSimpleName
&& this.vIntervals[1].semiSimpleName === specificInterval.semiSimpleName
);
}

contraryMotion(): boolean {
Expand Down Expand Up @@ -286,18 +332,10 @@ export class VoiceLeadingQuartet extends Music21Object {
}

parallelInterval(thisInterval: interval.Interval|string): boolean {
if (!(this.parallelMotion() || this.antiParallelMotion())) {
return false;
}
if (typeof thisInterval === 'string') {
thisInterval = new interval.Interval(thisInterval);
}

if (this.vIntervals[0].semiSimpleName === thisInterval.semiSimpleName) {
return true;
} else {
return false;
}
return (
this.parallelMotion(thisInterval, true)
|| this.antiParallelMotion(thisInterval)
);
}

parallelFifth(): boolean {
Expand All @@ -317,7 +355,7 @@ export class VoiceLeadingQuartet extends Music21Object {
}

hiddenInterval(thisInterval: interval.Interval|string): boolean {
if (this.parallelMotion()) {
if (this.parallelMotion(undefined, true)) {
return false;
}
if (!this.similarMotion()) {
Expand Down
30 changes: 30 additions & 0 deletions tests/moduleTests/voiceLeading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,36 @@ export default function tests() {

const vlq3 = new VLQ(new N('C4'), new N('D4'), new N('A3'), new N('F3'));
assert.ok(vlq3.contraryMotion(), 'contrary motion set w/ strings');

const antiParallel = new VLQ(new N('A5'), new N('C5'), new N('D4'), new N('F4'));
assert.equal(
antiParallel.motionType(),
music21.voiceLeading.MotionType.contrary,
'anti-parallel defaults to contrary for compatibility'
);
assert.equal(
antiParallel.motionType(true),
music21.voiceLeading.MotionType.antiParallel,
'anti-parallel can be requested explicitly'
);

const displacedFifths = new VLQ(new N('A4'), new N('B5'), new N('D3'), new N('E3'));
assert.ok(!displacedFifths.parallelMotion(), '5th to 12th not parallel by default');
assert.ok(
displacedFifths.parallelMotion(undefined, true),
'5th to 12th counts with octave displacement'
);
assert.ok(displacedFifths.parallelFifth(), 'parallelFifth allows octave displacement');

const hiddenDisplaced = new VLQ(new N('C4'), new N('G5'), new N('E3'), new N('C4'));
assert.ok(
!hiddenDisplaced.hiddenOctave(),
'octave-displaced parallel motion is not counted as hidden octave'
);

const keyedByString = new VLQ(new N('D4'), new N('G4'), new N('B3'), new N('G3'), 'd');
assert.equal(keyedByString.key.tonic.name, 'D');
assert.equal(keyedByString.key.mode, 'minor');
});
test(
'music21.voiceLeading.VoiceLeadingQuartet proper resolution',
Expand Down
Loading