Add vocal tract boundary contracts
This commit is contained in:
parent
615ed8ec7f
commit
c4d215dabe
7 changed files with 443 additions and 2 deletions
|
|
@ -137,6 +137,24 @@ authorities.
|
|||
rebuild work, but that is not exact DX7 envelope execution. Keep exactness
|
||||
pressure visible in reference features instead of smuggling it into ADSR.
|
||||
|
||||
## Phonetic Tract Boundary
|
||||
|
||||
IPA is an authoring surface, not a synth model. Weksa or another language layer
|
||||
owns text, phonology, allophones, and phonetic intent. AquaSynth owns the
|
||||
articulatory realization: gesture planning, morphology constraints, tract-area
|
||||
targets, excitation events, and Faust lowering.
|
||||
|
||||
The boundary must stay split into three inspectable records:
|
||||
|
||||
- `PhoneticIntent`: language-owned phones, features, timing, and prosody.
|
||||
- `ArticulatoryPlan`: AquaSynth-owned gestures and tract/source targets.
|
||||
- `ArticulatoryConstraintReport`: host-visible diagnostics when morphology
|
||||
cannot produce the requested articulation.
|
||||
|
||||
Anatomy has veto power. If a beaked morphology cannot form a bilabial closure,
|
||||
the planner must report the missing capability instead of silently substituting
|
||||
a fake sound and calling it expressive.
|
||||
|
||||
## Metrics
|
||||
|
||||
Analysis exists to catch regressions and support search, not to crown winners.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,24 @@ The target user feeds Weksa text as IPA or IPA-like phonetic strings. AquaSynth
|
|||
keeps the real authority below that surface: phonetic features, gestures,
|
||||
anatomy, excitation, and acoustic simulation.
|
||||
|
||||
## Boundary Bones
|
||||
|
||||
Three handoffs must stay visible in code, docs, tests, and host reports:
|
||||
|
||||
1. `PhoneticIntent` is the inspectable Weksa-to-AquaSynth contract. It carries
|
||||
IPA tokens, feature bundles, timing, and prosody. It does not know tract
|
||||
section areas or Faust.
|
||||
2. `ArticulatoryPlan` is the AquaSynth-owned realization layer. It carries
|
||||
gestures and tract targets after morphology constraints have had a chance to
|
||||
bite. It does not pretend an impossible phone was rendered honestly.
|
||||
3. `ArticulatoryConstraintReport` is the host-visible failure surface. If a
|
||||
morphology cannot form a requested articulation, the report names the phone,
|
||||
source event, missing capability, and severity.
|
||||
|
||||
Those records are the first defense against liar-slurry: no parser, planner,
|
||||
renderer, or host should need to infer whether IPA intent, physical anatomy, or
|
||||
DSP output owns a decision.
|
||||
|
||||
## Objective
|
||||
|
||||
Build a research-grounded vocal-tract instrument for human and alien speech:
|
||||
|
|
|
|||
289
src/AquaSynth.Core/VocalTractModel.cs
Normal file
289
src/AquaSynth.Core/VocalTractModel.cs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
namespace AquaSynth.Dsl;
|
||||
|
||||
public enum PhoneticManner
|
||||
{
|
||||
Vowel,
|
||||
Stop,
|
||||
Nasal,
|
||||
Fricative,
|
||||
Affricate,
|
||||
Approximant,
|
||||
LateralApproximant,
|
||||
LateralFricative,
|
||||
Trill,
|
||||
TapFlap
|
||||
}
|
||||
|
||||
public enum PhoneticPlace
|
||||
{
|
||||
None,
|
||||
Bilabial,
|
||||
Labiodental,
|
||||
Dental,
|
||||
Alveolar,
|
||||
Postalveolar,
|
||||
Retroflex,
|
||||
Palatal,
|
||||
Velar,
|
||||
Uvular,
|
||||
Pharyngeal,
|
||||
Glottal
|
||||
}
|
||||
|
||||
public enum Phonation
|
||||
{
|
||||
Voiceless,
|
||||
Voiced,
|
||||
Breathy,
|
||||
Creaky
|
||||
}
|
||||
|
||||
public enum AirstreamMechanism
|
||||
{
|
||||
PulmonicEgressive,
|
||||
Ejective,
|
||||
Implosive,
|
||||
Click
|
||||
}
|
||||
|
||||
public enum VowelHeight
|
||||
{
|
||||
None,
|
||||
Close,
|
||||
NearClose,
|
||||
CloseMid,
|
||||
Mid,
|
||||
OpenMid,
|
||||
NearOpen,
|
||||
Open
|
||||
}
|
||||
|
||||
public enum VowelBackness
|
||||
{
|
||||
None,
|
||||
Front,
|
||||
NearFront,
|
||||
Central,
|
||||
NearBack,
|
||||
Back
|
||||
}
|
||||
|
||||
public sealed record PhoneticFeatures(
|
||||
PhoneticManner Manner,
|
||||
PhoneticPlace Place = PhoneticPlace.None,
|
||||
Phonation Phonation = Phonation.Voiced,
|
||||
AirstreamMechanism Airstream = AirstreamMechanism.PulmonicEgressive,
|
||||
VowelHeight Height = VowelHeight.None,
|
||||
VowelBackness Backness = VowelBackness.None,
|
||||
bool Rounded = false,
|
||||
bool Long = false,
|
||||
bool Nasalized = false,
|
||||
bool Lateral = false);
|
||||
|
||||
public sealed record PhoneticProsody(
|
||||
float Stress = 0,
|
||||
float PitchTarget = 0,
|
||||
float Intensity = 1,
|
||||
string Register = "");
|
||||
|
||||
public sealed record PhoneticEvent(
|
||||
string Id,
|
||||
string Ipa,
|
||||
PhoneticFeatures Features,
|
||||
double StartSeconds = 0,
|
||||
double DurationSeconds = 0,
|
||||
PhoneticProsody Prosody = null!)
|
||||
{
|
||||
public PhoneticProsody Prosody { get; init; } = Prosody ?? new();
|
||||
}
|
||||
|
||||
public sealed record PhoneticIntent(
|
||||
string Id,
|
||||
string Language,
|
||||
string SourceText,
|
||||
IReadOnlyList<PhoneticEvent> Events);
|
||||
|
||||
public enum ArticulatoryCapability
|
||||
{
|
||||
OralTract,
|
||||
PulmonicPressure,
|
||||
Voicing,
|
||||
BilabialClosure,
|
||||
LabiodentalConstriction,
|
||||
DentalConstriction,
|
||||
AlveolarConstriction,
|
||||
PostalveolarConstriction,
|
||||
RetroflexConstriction,
|
||||
PalatalConstriction,
|
||||
VelarConstriction,
|
||||
UvularConstriction,
|
||||
PharyngealConstriction,
|
||||
GlottalConstriction,
|
||||
NasalBranch,
|
||||
LateralChannel,
|
||||
EjectivePressure,
|
||||
ImplosivePressure,
|
||||
ClickCavity,
|
||||
LipRounding,
|
||||
SecondarySource
|
||||
}
|
||||
|
||||
public sealed record VocalTractMorphology(
|
||||
string Id,
|
||||
string Name,
|
||||
float TractLengthCentimeters,
|
||||
int SectionCount,
|
||||
IReadOnlyList<ArticulatoryCapability> Capabilities);
|
||||
|
||||
public enum ArticulatoryDiagnosticSeverity
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public sealed record ArticulatoryDiagnostic(
|
||||
ArticulatoryDiagnosticSeverity Severity,
|
||||
string Code,
|
||||
string Message,
|
||||
string EventId = "",
|
||||
string Ipa = "",
|
||||
ArticulatoryCapability? RequiredCapability = null);
|
||||
|
||||
public sealed record ArticulatoryConstraintReport(
|
||||
string IntentId,
|
||||
string MorphologyId,
|
||||
IReadOnlyList<ArticulatoryDiagnostic> Diagnostics)
|
||||
{
|
||||
public bool Accepted => Diagnostics.All(diagnostic => diagnostic.Severity != ArticulatoryDiagnosticSeverity.Error);
|
||||
}
|
||||
|
||||
public enum ArticulatoryGestureKind
|
||||
{
|
||||
TractAreaTarget,
|
||||
Constriction,
|
||||
Closure,
|
||||
Release,
|
||||
Turbulence,
|
||||
NasalCoupling,
|
||||
GlottalSource,
|
||||
PressureEvent
|
||||
}
|
||||
|
||||
public sealed record ArticulatoryGesture(
|
||||
string Id,
|
||||
string SourceEventId,
|
||||
ArticulatoryGestureKind Kind,
|
||||
double StartSeconds,
|
||||
double DurationSeconds,
|
||||
string Target = "",
|
||||
float Amount = 0);
|
||||
|
||||
public sealed record ArticulatoryPlan(
|
||||
string Id,
|
||||
PhoneticIntent Intent,
|
||||
VocalTractMorphology Morphology,
|
||||
IReadOnlyList<ArticulatoryGesture> Gestures,
|
||||
ArticulatoryConstraintReport Report);
|
||||
|
||||
public static class VocalTractConstraintEvaluator
|
||||
{
|
||||
public static ArticulatoryConstraintReport Evaluate(PhoneticIntent intent, VocalTractMorphology morphology)
|
||||
{
|
||||
var capabilities = morphology.Capabilities.ToHashSet();
|
||||
var diagnostics = new List<ArticulatoryDiagnostic>();
|
||||
|
||||
foreach (var phoneticEvent in intent.Events)
|
||||
{
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.OralTract, "missing_oral_tract", "Morphology has no oral tract for phonetic realization.");
|
||||
if (phoneticEvent.Features.Phonation != Phonation.Voiceless)
|
||||
{
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.Voicing, "missing_voicing", "Morphology cannot produce voiced phonation for this event.");
|
||||
}
|
||||
|
||||
if (phoneticEvent.Features.Rounded)
|
||||
{
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.LipRounding, "missing_lip_rounding", "Morphology cannot round or protrude a lip aperture for this event.");
|
||||
}
|
||||
|
||||
if (phoneticEvent.Features.Nasalized || phoneticEvent.Features.Manner == PhoneticManner.Nasal)
|
||||
{
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.NasalBranch, "missing_nasal_branch", "Morphology cannot open a nasal branch for this event.");
|
||||
}
|
||||
|
||||
if (phoneticEvent.Features.Lateral ||
|
||||
phoneticEvent.Features.Manner is PhoneticManner.LateralApproximant or PhoneticManner.LateralFricative)
|
||||
{
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.LateralChannel, "missing_lateral_channel", "Morphology cannot form a lateral channel for this event.");
|
||||
}
|
||||
|
||||
if (PlaceCapability(phoneticEvent.Features.Place) is { } placeCapability)
|
||||
{
|
||||
Require(diagnostics, capabilities, phoneticEvent, placeCapability, MissingPlaceCode(phoneticEvent.Features.Place), MissingPlaceMessage(phoneticEvent.Features.Place));
|
||||
}
|
||||
|
||||
switch (phoneticEvent.Features.Airstream)
|
||||
{
|
||||
case AirstreamMechanism.PulmonicEgressive:
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.PulmonicPressure, "missing_pulmonic_pressure", "Morphology cannot produce pulmonic pressure for this event.");
|
||||
break;
|
||||
case AirstreamMechanism.Ejective:
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.EjectivePressure, "missing_ejective_pressure", "Morphology cannot produce ejective pressure for this event.");
|
||||
break;
|
||||
case AirstreamMechanism.Implosive:
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.ImplosivePressure, "missing_implosive_pressure", "Morphology cannot produce implosive pressure for this event.");
|
||||
break;
|
||||
case AirstreamMechanism.Click:
|
||||
Require(diagnostics, capabilities, phoneticEvent, ArticulatoryCapability.ClickCavity, "missing_click_cavity", "Morphology cannot form a click cavity for this event.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new ArticulatoryConstraintReport(intent.Id, morphology.Id, diagnostics);
|
||||
}
|
||||
|
||||
private static void Require(
|
||||
List<ArticulatoryDiagnostic> diagnostics,
|
||||
HashSet<ArticulatoryCapability> capabilities,
|
||||
PhoneticEvent phoneticEvent,
|
||||
ArticulatoryCapability capability,
|
||||
string code,
|
||||
string message)
|
||||
{
|
||||
if (capabilities.Contains(capability))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
diagnostics.Add(new ArticulatoryDiagnostic(
|
||||
ArticulatoryDiagnosticSeverity.Error,
|
||||
code,
|
||||
message,
|
||||
phoneticEvent.Id,
|
||||
phoneticEvent.Ipa,
|
||||
capability));
|
||||
}
|
||||
|
||||
private static ArticulatoryCapability? PlaceCapability(PhoneticPlace place) =>
|
||||
place switch
|
||||
{
|
||||
PhoneticPlace.Bilabial => ArticulatoryCapability.BilabialClosure,
|
||||
PhoneticPlace.Labiodental => ArticulatoryCapability.LabiodentalConstriction,
|
||||
PhoneticPlace.Dental => ArticulatoryCapability.DentalConstriction,
|
||||
PhoneticPlace.Alveolar => ArticulatoryCapability.AlveolarConstriction,
|
||||
PhoneticPlace.Postalveolar => ArticulatoryCapability.PostalveolarConstriction,
|
||||
PhoneticPlace.Retroflex => ArticulatoryCapability.RetroflexConstriction,
|
||||
PhoneticPlace.Palatal => ArticulatoryCapability.PalatalConstriction,
|
||||
PhoneticPlace.Velar => ArticulatoryCapability.VelarConstriction,
|
||||
PhoneticPlace.Uvular => ArticulatoryCapability.UvularConstriction,
|
||||
PhoneticPlace.Pharyngeal => ArticulatoryCapability.PharyngealConstriction,
|
||||
PhoneticPlace.Glottal => ArticulatoryCapability.GlottalConstriction,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string MissingPlaceCode(PhoneticPlace place) =>
|
||||
$"missing_{place.ToString().ToLowerInvariant()}_capability";
|
||||
|
||||
private static string MissingPlaceMessage(PhoneticPlace place) =>
|
||||
$"Morphology cannot form the requested {place.ToString().ToLowerInvariant()} articulation.";
|
||||
}
|
||||
|
|
@ -105,3 +105,4 @@
|
|||
|
||||
|
||||
{"date":"2026-05-19","evidence":"Added docs/ipa-vocal-tract-roadmap.md with staged IPA support from tokenization/vowels through pulmonic consonants, expanded IPA, airstream/phonation, and Weksa alien morphologies; linked it from docs/reference-synth-roadmap.md and state.","lesson":"AquaSynth should treat IPA as an authoring input for phonetic intent, not as the internal synth model. The planned machine is tokenizer -\u003e feature mapper -\u003e phonology profile -\u003e gesture planner -\u003e morphology -\u003e tract plan -\u003e Faust renderer, growing supported IPA only as tract/excitation physics earn each category.","topic":"ipa-vocal-tract-roadmap"}
|
||||
{"date":"2026-05-19","topic":"vocal-tract-boundary-contracts","lesson":"Defend the Weksa/AquaSynth tract boundary with three real bones: PhoneticIntent for language-owned phones/features/timing, ArticulatoryPlan for AquaSynth-owned gesture realization, and ArticulatoryConstraintReport for host-visible anatomy vetoes. Morphology capabilities must reject impossible phones such as bilabials for a beaked speaker instead of silently substituting liar-slurry.","evidence":"Added VocalTractModel.cs contracts and VocalTractModelTests.cs coverage for inspectable Weksa intent, bilabial veto on a beaked morphology, and acceptance on a human baseline; updated audio doctrine and IPA roadmap boundary section."}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ surfaces:
|
|||
modules:
|
||||
model: src/AquaSynth.Core/Model.cs
|
||||
parser: src/AquaSynth.Core/PatchScript.cs
|
||||
vocal_tract_model: src/AquaSynth.Core/VocalTractModel.cs
|
||||
faust_emitter: src/AquaSynth.Core/FaustExport.cs
|
||||
faust_compiler: src/AquaSynth.Faust/FaustCompiler.cs
|
||||
native_faust_runtime: src/AquaSynth.Faust/NativeFaustRuntime.cs
|
||||
|
|
@ -39,6 +40,7 @@ modules:
|
|||
zyn_coverage_survey: src/AquaSynth.Core/ZynInstrument.cs
|
||||
ipa_vocal_tract_roadmap: docs/ipa-vocal-tract-roadmap.md
|
||||
package_boundary_tests: tests/AquaSynth.Dsl.Tests/PackageBoundaryTests.cs
|
||||
vocal_tract_model_tests: tests/AquaSynth.Dsl.Tests/VocalTractModelTests.cs
|
||||
sfxr: src/AquaSynth.Core/SfxrParams.cs
|
||||
analysis: src/AquaSynth.Core/AudioAnalysis.cs
|
||||
scoring: src/AquaSynth.Core/ScriptMetrics.cs
|
||||
|
|
@ -60,6 +62,7 @@ boundaries:
|
|||
- target-code generation, compile-product contracts, dynamic patch compilation policy, cache identity, generated artifact manifests, and Faust toolchain selection in AquaSynth.Faust
|
||||
- live patch compilation, native Faust factory lifetime, and offline sample-buffer render proof in AquaSynth.Faust
|
||||
- IPA-to-phonetic-feature planning, articulatory gesture planning, and human/alien vocal-tract roadmap design
|
||||
- phonetic intent, articulatory plan, morphology capability, and constraint-report contracts for vocal tract synthesis
|
||||
dev_only:
|
||||
- public-domain DX7 SysEx fixtures
|
||||
- pinned GPL ZynAddSubFX source for parity tests
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
"dynamic patch compilation policy outside the realtime callback",
|
||||
"generated DSP artifact manifests for engine hosting",
|
||||
"live patch compilation, native Faust factory lifetime, and offline sample-buffer render proof for engine hosts",
|
||||
"phonetic intent, articulatory plan, morphology capability, and constraint-report contracts for vocal tract synthesis",
|
||||
"patch script parser",
|
||||
"SFXR atoms and parameter mapping",
|
||||
"analysis/comparison metrics",
|
||||
|
|
@ -85,8 +86,8 @@
|
|||
"rule": "Add DSL abstractions only when reference rebuilds prove the missing invariant. Do not consume references that cannot be backed by parity tests; parsing alone is inventory, not evidence of expressivity."
|
||||
},
|
||||
"ipa_vocal_tract_path": {
|
||||
"status": "Roadmap drafted for an IPA-to-vocal-tract lane. IPA is an authoring input only: tokenization and feature mapping lower into language phonology profiles, timed articulatory gestures, morphology-specific tract/excitation curves, and Faust DSP. The staged support plan starts with tokenization and vowels, then core pulmonic consonants, expanded human IPA, airstream/phonation, and Weksa/alien morphologies.",
|
||||
"lesson": "Do not build a complete IPA parser before the tract can say basic syllables. The durable machine is tokenizer -> feature mapper -> phonology profile -> gesture planner -> morphology -> tract plan -> Faust renderer, with dependency-injected seams and mocks at each boundary. Weksa should use IPA-like phonetic input without letting human IPA semantics own alien anatomy.",
|
||||
"status": "Roadmap drafted for an IPA-to-vocal-tract lane, and the first Core contracts now defend the boundary: PhoneticIntent is the Weksa/language-owned handoff, ArticulatoryPlan is the AquaSynth realization layer, VocalTractMorphology declares anatomy capabilities, and ArticulatoryConstraintReport is the host-visible veto surface. IPA is an authoring input only: tokenization and feature mapping lower into language phonology profiles, timed articulatory gestures, morphology-specific tract/excitation curves, and Faust DSP. The staged support plan starts with tokenization and vowels, then core pulmonic consonants, expanded human IPA, airstream/phonation, and Weksa/alien morphologies.",
|
||||
"lesson": "Do not build a complete IPA parser before the tract can say basic syllables. The durable machine is tokenizer -> feature mapper -> phonology profile -> gesture planner -> morphology -> tract plan -> Faust renderer, with dependency-injected seams and mocks at each boundary. Weksa should use IPA-like phonetic input without letting human IPA semantics own alien anatomy. Anatomy has veto power: if a morphology lacks bilabial closure or another required capability, AquaSynth reports that failure instead of smearing over it.",
|
||||
"next": "Implement Stage 0/1 fixtures: IPA token spans, vowel feature bundles, human baseline vowel tract-area targets, and a tiny render proof before expanding consonants."
|
||||
},
|
||||
"dx7_reference_path": {
|
||||
|
|
|
|||
111
tests/AquaSynth.Dsl.Tests/VocalTractModelTests.cs
Normal file
111
tests/AquaSynth.Dsl.Tests/VocalTractModelTests.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
using AquaSynth.Dsl;
|
||||
|
||||
namespace AquaSynth.Dsl.Tests;
|
||||
|
||||
public sealed class VocalTractModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void PhoneticIntentKeepsWeksaHandoffInspectable()
|
||||
{
|
||||
var intent = new PhoneticIntent(
|
||||
"weksa-utterance-1",
|
||||
"weksa",
|
||||
"/qʼoru/",
|
||||
[
|
||||
new PhoneticEvent(
|
||||
"phone-1",
|
||||
"qʼ",
|
||||
new PhoneticFeatures(
|
||||
PhoneticManner.Stop,
|
||||
PhoneticPlace.Uvular,
|
||||
Phonation.Voiceless,
|
||||
AirstreamMechanism.Ejective),
|
||||
StartSeconds: 0.0,
|
||||
DurationSeconds: 0.08,
|
||||
Prosody: new PhoneticProsody(Stress: 1.0f, PitchTarget: 0.2f, Intensity: 0.8f))
|
||||
]);
|
||||
|
||||
var phone = Assert.Single(intent.Events);
|
||||
Assert.Equal("weksa", intent.Language);
|
||||
Assert.Equal("qʼ", phone.Ipa);
|
||||
Assert.Equal(PhoneticPlace.Uvular, phone.Features.Place);
|
||||
Assert.Equal(AirstreamMechanism.Ejective, phone.Features.Airstream);
|
||||
Assert.Equal(0.08, phone.DurationSeconds);
|
||||
Assert.Equal(1.0f, phone.Prosody.Stress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MorphologyVetoesPhysicallyImpossibleBilabial()
|
||||
{
|
||||
var intent = new PhoneticIntent(
|
||||
"beak-test",
|
||||
"weksa",
|
||||
"/pa/",
|
||||
[
|
||||
new PhoneticEvent(
|
||||
"phone-p",
|
||||
"p",
|
||||
new PhoneticFeatures(
|
||||
PhoneticManner.Stop,
|
||||
PhoneticPlace.Bilabial,
|
||||
Phonation.Voiceless),
|
||||
DurationSeconds: 0.06)
|
||||
]);
|
||||
var beak = new VocalTractMorphology(
|
||||
"beak",
|
||||
"Beaked speaker",
|
||||
22.0f,
|
||||
44,
|
||||
[
|
||||
ArticulatoryCapability.OralTract,
|
||||
ArticulatoryCapability.PulmonicPressure,
|
||||
ArticulatoryCapability.Voicing,
|
||||
ArticulatoryCapability.AlveolarConstriction,
|
||||
ArticulatoryCapability.VelarConstriction
|
||||
]);
|
||||
|
||||
var report = VocalTractConstraintEvaluator.Evaluate(intent, beak);
|
||||
|
||||
Assert.False(report.Accepted);
|
||||
var diagnostic = Assert.Single(report.Diagnostics);
|
||||
Assert.Equal("phone-p", diagnostic.EventId);
|
||||
Assert.Equal("p", diagnostic.Ipa);
|
||||
Assert.Equal("missing_bilabial_capability", diagnostic.Code);
|
||||
Assert.Equal(ArticulatoryCapability.BilabialClosure, diagnostic.RequiredCapability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HumanBaselineAcceptsBilabialWhenCapabilityExists()
|
||||
{
|
||||
var intent = new PhoneticIntent(
|
||||
"human-test",
|
||||
"ipa",
|
||||
"/pa/",
|
||||
[
|
||||
new PhoneticEvent(
|
||||
"phone-p",
|
||||
"p",
|
||||
new PhoneticFeatures(
|
||||
PhoneticManner.Stop,
|
||||
PhoneticPlace.Bilabial,
|
||||
Phonation.Voiceless),
|
||||
DurationSeconds: 0.06)
|
||||
]);
|
||||
var human = new VocalTractMorphology(
|
||||
"human",
|
||||
"Human baseline",
|
||||
17.0f,
|
||||
44,
|
||||
[
|
||||
ArticulatoryCapability.OralTract,
|
||||
ArticulatoryCapability.PulmonicPressure,
|
||||
ArticulatoryCapability.Voicing,
|
||||
ArticulatoryCapability.BilabialClosure
|
||||
]);
|
||||
|
||||
var report = VocalTractConstraintEvaluator.Evaluate(intent, human);
|
||||
|
||||
Assert.True(report.Accepted);
|
||||
Assert.Empty(report.Diagnostics);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue