export enum VowelHeight { Open = 'open', NearOpen = 'nearOpen', OpenMid = 'openMid', Mid = 'mid', CloseMid = 'closeMid', NearClose = 'nearClose', Close = 'close', } export enum VowelDepth { Front = 'front', Central = 'central', Back = 'back', } export interface VowelFeatures { height: VowelHeight; depth: VowelDepth; round: boolean; long: boolean; nasal: boolean; } // convert a vowel height/depth combo to an x/y position // (for use in creating nicely spaced vowel inventories) export function _vowelXY( height: VowelHeight, depth: VowelDepth ): [number, number] { const y = { [VowelHeight.Open]: 0, [VowelHeight.NearOpen]: 1, [VowelHeight.OpenMid]: 2, [VowelHeight.Mid]: 3, [VowelHeight.CloseMid]: 4, [VowelHeight.NearClose]: 5, [VowelHeight.Close]: 6, }[height]; const slope = { [VowelDepth.Front]: 1, [VowelDepth.Central]: 2, [VowelDepth.Back]: 3, }[depth]; if (slope === 3) { return [ 0, y ]; } const x = (y + 2) / slope; return [x, y]; } // convert a set of vowel features to an IPA representation export function vowelFeaturesToIpa(features: VowelFeatures): string { const ipa = () => { switch (features.height) { case VowelHeight.Open: switch (features.depth) { case VowelDepth.Front: return features.round ? 'ɶ' : 'a'; case VowelDepth.Central: return features.round ? 'ɞ' : 'ä'; case VowelDepth.Back: return features.round ? 'ɒ' : 'ɑ'; } case VowelHeight.NearOpen: switch (features.depth) { case VowelDepth.Front: return features.round ? 'ɶ' : 'æ'; case VowelDepth.Central: return features.round ? 'ɞ' : 'ɐ'; case VowelDepth.Back: return features.round ? 'ɒ' : 'ɑ'; } case VowelHeight.OpenMid: switch (features.depth) { case VowelDepth.Front: return features.round ? 'œ' : 'ɛ'; case VowelDepth.Central: return features.round ? 'ɞ' : 'ɜ'; case VowelDepth.Back: return features.round ? 'ɒ' : 'ɑ'; } case VowelHeight.Mid: switch (features.depth) { case VowelDepth.Front: return features.round ? 'ø̞' : 'e̞'; case VowelDepth.Central: return features.round ? 'ɵ' : 'ə'; case VowelDepth.Back: return features.round ? 'o̞' : 'ɤ̞'; } case VowelHeight.CloseMid: switch (features.depth) { case VowelDepth.Front: return features.round ? 'ø' : 'e'; case VowelDepth.Central: return features.round ? 'ɵ' : 'ɘ'; case VowelDepth.Back: return features.round ? 'o' : 'ɤ'; } case VowelHeight.NearClose: switch (features.depth) { case VowelDepth.Front: return features.round ? 'ʏ' : 'ɪ'; case VowelDepth.Central: return features.round ? 'ʉ' : 'ɨ'; case VowelDepth.Back: return features.round ? 'u' : 'ʊ'; } case VowelHeight.Close: switch (features.depth) { case VowelDepth.Front: return features.round ? 'y' : 'i'; case VowelDepth.Central: return features.round ? 'ʉ' : 'ɨ'; case VowelDepth.Back: return features.round ? 'u' : 'ɯ'; } default: throw new Error(`bad vowel height: ${features.height}`); } }; const nasal = features.nasal ? '̃' : ''; const vlong = features.long ? 'ː' : ''; return ipa() + nasal + vlong; } export enum ConsonantPlace { Bilabial = 'bilabial', Labiodental = 'labiodental', Linguolabial = 'labiolingual', Dental = 'dental', Alveolar = 'alveolar', Postalveolar = 'postalveolar', Retroflex = 'retroflex', Palatal = 'palatal', Velar = 'velar', Uvular = 'uvular', Pharyngeal = 'pharyngeal', Glottal = 'glottal', } export enum ConsonantManner { Nasal = 'nasal', Plosive = 'plosive', SibilantAffricate = 'sibilantAffricate', NonSibilantAffricate = 'nonSibilantAffricate', SibilantFricative = 'sibilantFricative', NonSibilantFricative = 'nonSibilantFricative', Approximant = 'approximant', Tap = 'tap', Trill = 'trill', LateralAffricate = 'lateralAffricate', LateralFricative = 'lateralFricative', LateralApproximant = 'lateralApproximant', LateralTap = 'lateralTap', } export interface ConsonantFeatures { place: ConsonantPlace; manner: ConsonantManner; voice: boolean; } // convert a set of consonant features to IPA // returns the empty string if the proposed consonant does not exist export function consonantFeaturesToIpa(features: ConsonantFeatures): string { switch (features.place) { case ConsonantPlace.Bilabial: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'm' : 'm̥'; case ConsonantManner.Plosive: return features.voice ? 'b' : 'p'; case ConsonantManner.SibilantAffricate: return ''; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'bβ' : 'pɸ'; case ConsonantManner.SibilantFricative: return ''; case ConsonantManner.NonSibilantFricative: return features.voice ? 'β' : 'ɸ'; case ConsonantManner.Approximant: return features.voice ? 'β̞' : ''; case ConsonantManner.Tap: return features.voice ? 'ⱱ̟' : ''; case ConsonantManner.Trill: return features.voice ? 'ʙ' : 'ʙ̥'; case ConsonantManner.LateralAffricate: case ConsonantManner.LateralFricative: case ConsonantManner.LateralApproximant: case ConsonantManner.LateralTap: return ''; } case ConsonantPlace.Labiodental: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'ɱ' : 'ɱ̊'; case ConsonantManner.Plosive: return features.voice ? 'b̪' : 'p̪'; case ConsonantManner.SibilantAffricate: return ''; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'b̪v' : 'p̪f'; case ConsonantManner.SibilantFricative: return ''; case ConsonantManner.NonSibilantFricative: return features.voice ? 'v' : 'f'; case ConsonantManner.Approximant: return features.voice ? 'ʋ' : ''; case ConsonantManner.Tap: return features.voice ? 'ⱱ' : ''; case ConsonantManner.Trill: case ConsonantManner.LateralAffricate: case ConsonantManner.LateralFricative: case ConsonantManner.LateralApproximant: case ConsonantManner.LateralTap: return ''; } case ConsonantPlace.Linguolabial: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'n̼' : ''; case ConsonantManner.Plosive: return features.voice ? 'd̼' : 't̼'; case ConsonantManner.SibilantAffricate: case ConsonantManner.NonSibilantAffricate: case ConsonantManner.SibilantFricative: return ''; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ð̼' : 'θ̼'; case ConsonantManner.Approximant: return ''; case ConsonantManner.Tap: return features.voice ? 'ɾ̼' : ''; case ConsonantManner.Trill: case ConsonantManner.LateralAffricate: case ConsonantManner.LateralFricative: case ConsonantManner.LateralApproximant: case ConsonantManner.LateralTap: return ''; } case ConsonantPlace.Dental: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'n' : 'n̥'; case ConsonantManner.Plosive: return features.voice ? 'd̪' : 't̪'; case ConsonantManner.SibilantAffricate: return features.voice ? 'd̪z̪' : 't̪s̪'; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'd̪ð' : 't̪θ'; case ConsonantManner.SibilantFricative: return features.voice ? 'z' : 's'; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ð' : 'θ'; case ConsonantManner.Approximant: return features.voice ? 'ð̞' : ''; case ConsonantManner.Tap: return features.voice ? 'ɾ' : 'ɾ̥'; case ConsonantManner.Trill: return features.voice ? 'r' : 'r̥'; case ConsonantManner.LateralAffricate: return features.voice ? 'dɮ' : 'tɬ'; case ConsonantManner.LateralFricative: return features.voice ? 'ɮ' : 'ɬ'; case ConsonantManner.LateralApproximant: return features.voice ? 'l̪' : ''; case ConsonantManner.LateralTap: return features.voice ? 'ɺ' : 'ɺ̥'; } case ConsonantPlace.Alveolar: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'n' : 'n̥'; case ConsonantManner.Plosive: return features.voice ? 'd' : 't'; case ConsonantManner.SibilantAffricate: return features.voice ? 'dz' : 'ts'; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'dɹ̝' : 'tɹ̝̊'; case ConsonantManner.SibilantFricative: return features.voice ? 'z' : 's'; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ð̠' : 'θ̠'; case ConsonantManner.Approximant: return features.voice ? 'ɹ' : ''; case ConsonantManner.Tap: return features.voice ? 'ɾ' : 'ɾ̥'; case ConsonantManner.Trill: return features.voice ? 'r' : 'r̥'; case ConsonantManner.LateralAffricate: return features.voice ? 'dɮ' : 'tɬ'; case ConsonantManner.LateralFricative: return features.voice ? 'ɮ' : 'ɬ'; case ConsonantManner.LateralApproximant: return features.voice ? 'l' : ''; case ConsonantManner.LateralTap: return features.voice ? 'ɺ' : 'ɺ̥'; } case ConsonantPlace.Postalveolar: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'n' : 'n̥'; case ConsonantManner.Plosive: return features.voice ? 'd' : 't'; case ConsonantManner.SibilantAffricate: return features.voice ? 'd̠ʒ' : 't̠ʃ'; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'd̠ɹ̠˔' : 't̠ɹ̠̊˔'; case ConsonantManner.SibilantFricative: return features.voice ? 'ʒ' : 'ʃ'; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ɹ̠˔' : 'ɹ̠̊˔'; case ConsonantManner.Approximant: return features.voice ? 'ɹ̠' : ''; case ConsonantManner.Tap: return features.voice ? 'r̠' : 'ɾ̥'; case ConsonantManner.Trill: return features.voice ? 'r' : 'r̥'; case ConsonantManner.LateralAffricate: return features.voice ? 'dɮ' : 'tɬ'; case ConsonantManner.LateralFricative: return features.voice ? 'ɮ' : 'ɬ'; case ConsonantManner.LateralApproximant: return features.voice ? 'l̠' : ''; case ConsonantManner.LateralTap: return features.voice ? 'ɺ' : 'ɺ̥'; } case ConsonantPlace.Retroflex: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'ɳ' : 'ɳ̊'; case ConsonantManner.Plosive: return features.voice ? 'ɖ' : 'ʈ'; case ConsonantManner.SibilantAffricate: return features.voice ? 'dʐ' : 'tʂ'; case ConsonantManner.NonSibilantAffricate: return ''; case ConsonantManner.SibilantFricative: return features.voice ? 'ʐ' : 'ʂ'; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ɻ˔' : 'ɻ̊˔'; case ConsonantManner.Approximant: return features.voice ? 'ɻ' : ''; case ConsonantManner.Tap: return features.voice ? 'ɽ' : 'ɽ̊'; case ConsonantManner.Trill: return features.voice ? 'ɽr' : 'ɽ̊r̥'; case ConsonantManner.LateralAffricate: return features.voice ? 'd𝼅' : 'tꞎ'; case ConsonantManner.LateralFricative: return features.voice ? '𝼅' : 'ꞎ'; case ConsonantManner.LateralApproximant: return features.voice ? 'ɭ' : ''; case ConsonantManner.LateralTap: return features.voice ? '𝼈' : '𝼈̥'; } case ConsonantPlace.Palatal: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'ɲ' : 'ɲ̊'; case ConsonantManner.Plosive: return features.voice ? 'ɟ' : 'c'; case ConsonantManner.SibilantAffricate: return features.voice ? 'dʑ' : 'tɕ'; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'ɟʝ' : 'cç'; case ConsonantManner.SibilantFricative: return features.voice ? 'ʑ' : 'ɕ'; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ʝ' : 'ç'; case ConsonantManner.Approximant: return features.voice ? 'j' : ''; case ConsonantManner.Tap: case ConsonantManner.Trill: return ''; case ConsonantManner.LateralAffricate: return features.voice ? 'ɟʎ̝' : 'c𝼆'; case ConsonantManner.LateralFricative: return features.voice ? 'ʎ̝' : '𝼆'; case ConsonantManner.LateralApproximant: return features.voice ? 'ʎ' : ''; case ConsonantManner.LateralTap: return features.voice ? 'ʎ̆' : ''; } case ConsonantPlace.Velar: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'ŋ' : 'ŋ̊'; case ConsonantManner.Plosive: return features.voice ? 'g' : 'k'; case ConsonantManner.SibilantAffricate: return ''; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'ɡɣ' : 'kx'; case ConsonantManner.SibilantFricative: return ''; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ɣ' : 'x'; case ConsonantManner.Approximant: return features.voice ? 'ɰ' : ''; case ConsonantManner.Tap: case ConsonantManner.Trill: return ''; case ConsonantManner.LateralAffricate: return features.voice ? 'ɡʟ̝' : 'k𝼄'; case ConsonantManner.LateralFricative: return features.voice ? 'ʟ̝' : '𝼄'; case ConsonantManner.LateralApproximant: return features.voice ? 'ʟ' : ''; case ConsonantManner.LateralTap: return features.voice ? 'ʟ̆' : ''; } case ConsonantPlace.Uvular: switch (features.manner) { case ConsonantManner.Nasal: return features.voice ? 'ɴ' : 'ɴ̥'; case ConsonantManner.Plosive: return features.voice ? 'ɢ' : 'q'; case ConsonantManner.SibilantAffricate: return ''; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'ɢʁ' : 'qχ'; case ConsonantManner.SibilantFricative: return ''; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ʁ' : 'χ'; case ConsonantManner.Approximant: return features.voice ? 'ʁ̞' : ''; case ConsonantManner.Tap: return features.voice ? 'ɢ̆' : ''; case ConsonantManner.Trill: return features.voice ? 'ʀ' : 'ʀ̥'; case ConsonantManner.LateralAffricate: case ConsonantManner.LateralFricative: return ''; case ConsonantManner.LateralApproximant: return features.voice ? 'ʟ̠' : ''; case ConsonantManner.LateralTap: return ''; } case ConsonantPlace.Pharyngeal: switch (features.manner) { case ConsonantManner.Nasal: return ''; case ConsonantManner.Plosive: return features.voice ? '' : 'ʡ'; case ConsonantManner.SibilantAffricate: return ''; case ConsonantManner.NonSibilantAffricate: return features.voice ? 'ʡʢ' : 'ʡʜ'; case ConsonantManner.SibilantFricative: return ''; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ʕ' : 'ħ'; case ConsonantManner.Approximant: return ''; case ConsonantManner.Tap: return features.voice ? 'ʡ̆' : ''; case ConsonantManner.Trill: return features.voice ? 'ʢ' : 'ʜ'; case ConsonantManner.LateralAffricate: case ConsonantManner.LateralFricative: case ConsonantManner.LateralApproximant: case ConsonantManner.LateralTap: return ''; } case ConsonantPlace.Glottal: switch (features.manner) { case ConsonantManner.Nasal: return ''; case ConsonantManner.Plosive: return features.voice ? '' : 'ʔ'; case ConsonantManner.SibilantAffricate: return ''; case ConsonantManner.NonSibilantAffricate: return features.voice ? '' : 'ʔh'; case ConsonantManner.SibilantFricative: return ''; case ConsonantManner.NonSibilantFricative: return features.voice ? 'ɦ' : 'h'; case ConsonantManner.Approximant: return features.voice ? 'ʔ̞' : ''; case ConsonantManner.Tap: case ConsonantManner.Trill: case ConsonantManner.LateralAffricate: case ConsonantManner.LateralFricative: case ConsonantManner.LateralApproximant: case ConsonantManner.LateralTap: return ''; } } } export function isConsonantPossible(features: ConsonantFeatures): boolean { return consonantFeaturesToIpa(features) !== ''; } // get all of the consonants based on the features selected export function pickConsonants( places: ConsonantPlace[], manners: ConsonantManner[], voicing: [boolean, boolean], ): ConsonantFeatures[] { return places.map( place => manners.map( manner => { const [ unvoiced, voiced ] = voicing; const result = []; if (unvoiced) { result.push({ place, manner, voice: false }); } if (voiced) { result.push({ place, manner, voice: true }); } return result; } ).flat() ).flat().filter(isConsonantPossible); }