Extend C API with component-level access functions + memory bug fixes#59
Merged
Conversation
…ons handling Tube::operator= copied the 93 sections plus three scalars but silently lost staticPartsInitialized, the subglottal/nasal cavity lengths and both piriform fossa dimensions, so copies of a Tube could disagree with the original on static geometry. vtlGetTransferFunction had two issues: tfOpts was declared inside the if (opts == NULL) block but used through the opts pointer after the block ended (dangling pointer), and opts->staticPressureDrops was never copied into TlModel::Options, so that option was silently ignored.
…nals Adds 21 vtl* API functions that expose more of the backend's internals through the C API, so that component-level access (driving individual models step by step, reading intermediate values) is possible from language bindings without going through full synthesis runs: - Glottis models: vtlGlottisCalcGeometry, vtlGlottisIncTime, vtlGlottisResetMotion, vtlGetGlottisStaticParamInfo (also added to the Windows export table) - Time-domain simulation: vtlTdsSetTubeAndRun, vtlTdsSetOptions, vtlTdsResetMotion - Anatomy scaling: vtlGetAnatomyParams, vtlSetAnatomyParams, vtlSetAnatomyFromAge, vtlSetFossaDims - Geometry / visualization: vtlGetSurfaceVertices, vtlGetCenterline, vtlGetOutlines, vtlGetCrossSections, vtlGetCuts, vtlGetProfiles, vtlGetTongueRibData, vtlGetTongueWidthBounds, vtlTractToFullTube - Transmission-line introspection: vtlGetTLIntermediateValues, backed by new read-only TlModel accessors (getMatrixProduct, getFossaInputImpedance, getNose-/getMouthRadiationImpedance) Supporting backend changes: - VocalTract::getRawCuts() plus public outline line strips for the geometry export functions - Tube: new rawVelumOpening_cm2 member; getVelumOpening_cm2() now returns the raw requested opening instead of noseSection[0].area_cm2 (NOTE: behavior change for existing callers - see PR description)
AnatomyParams::adaptArticulation() scaled four tongue side parameters (TS1+0 .. TS1+3), but the vocal tract model only has TS1..TS3; TS1+3 == NUM_PARAMS, so the loop read and wrote one element past the oldParams/newParams arrays (both double[NUM_PARAMS] on the caller's stack in setFor). The stray write corrupts an adjacent stack slot, which can crash the process later (observed as a malloc abort in VocalTract::~VocalTract() during vtlClose() after using vtlSetAnatomyFromAge / vtlSetAnatomyParams). The loop bound is now derived from the enum. The three valid parameters are scaled exactly as before, so computed results are unchanged. VocalTract::calcTongueRibs() started its right-edge scan at k = NUM_PROFILE_SAMPLES, reading upperProfile[i][k] and lowerProfile[i][k] one element past the end (arrays have NUM_PROFILE_SAMPLES entries). The scan now starts at the last valid index. The out-of-bounds sample is rejected by the INVALID checks in the normal case, so no behavior change is expected.
VtlBackendTests.vcxproj and VtlApiTests.vcxproj hard-pinned WindowsTargetPlatformVersion 10.0.19041.0, which the windows-latest runner image no longer ships, so MSBuild failed with MSB8036 at the test-build steps. Use "10.0" (resolves to the latest installed SDK), matching what VocalTractLabBackend.vcxproj and VocalTractLabApi.vcxproj already use in this repo. Pre-existing CI rot, independent of the C-API additions.
timoataltavo
approved these changes
Jun 14, 2026
timoataltavo
added a commit
that referenced
this pull request
Jun 14, 2026
Follow-up to #59: export the new C-API functions in the Windows .def
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds C-API surface for component-level access to the backend, plus
three pre-existing memory/correctness bug fixes found along the way.
Structured as three commits — easiest reviewed commit-by-commit.
Commit 1 — correctness fixes (small, ~20 lines)
Tube::operator=copied the 93 sections plus three scalars butsilently dropped
staticPartsInitialized, the subglottal/nasalcavity lengths and both piriform fossa dimensions, so a copied
Tubecould disagree with the original on static geometry.vtlGetTransferFunction: the fallbackTransferFunctionOptionswas declared inside the
if (opts == NULL)block but used throughthe
optspointer after the block ended (dangling pointer; under-O3the stack slot was reused, corrupting the options). Alsoopts->staticPressureDropswas never copied intoTlModel::Options,so that option was silently ignored.
Commit 2 — new C-API surface (the bulk; mechanical)
21 new
vtl*functions exposing backend internals for component-leveluse from language bindings (driving individual models step by step,
reading intermediate values) without full synthesis runs:
vtlGlottisCalcGeometry,vtlGlottisIncTime,vtlGlottisResetMotion,vtlGetGlottisStaticParamInfo(also addedto the Windows export table).
vtlTdsSetTubeAndRun,vtlTdsSetOptions,vtlTdsResetMotion.vtlGetAnatomyParams,vtlSetAnatomyParams,vtlSetAnatomyFromAge,vtlSetFossaDims.vtlGetSurfaceVertices,vtlGetCenterline,vtlGetOutlines,vtlGetCrossSections,vtlGetCuts,vtlGetProfiles,vtlGetTongueRibData,vtlGetTongueWidthBounds,vtlTractToFullTube.vtlGetTLIntermediateValues, backed by newread-only
TlModelaccessors (getMatrixProduct,getFossaInputImpedance,getNose-/getMouthRadiationImpedance).Supporting changes:
VocalTract::getRawCuts(), the outline linestrips made public, 4 entries in the Windows export table.
This commit also folds in one small correctness fix (bundled here
because it's consumed by the new
vtlTractToFullTube):Tube::getVelumOpening_cm2()returnednoseSection[0].area_cm2— thenose-section area after shaping and the
MIN_AREA_CM2clamp —instead of the velopharyngeal opening the caller requested. It now
returns that raw value, stored in a new
rawVelumOpening_cm2memberset in
setVelumOpening()(and copied inoperator=). The two agreefor any normally-open velum and differ only at/near a closed port —
e.g. a requested opening of
0.0previously read back as0.0001(the clamp floor), now reads back as
0.0. Affected readers: thevelumOpening_cm2output ofvtlTractToTube/vtlFastTractToTube/vtlTractToFullTube, tube interpolation, and the Synthesizer textexport.
Commit 3 — off-by-one array overruns found with AddressSanitizer
AnatomyParams::adaptArticulation()scaled four tongue-sideparams but the model has three (
TS1..TS3);TS1+3 == NUM_PARAMS,so the loop read and wrote one element past the
oldParams/newParamsstack arrays per call. The stray write can crash theprocess later (malloc abort in
~VocalTract()duringvtlClose()after using the anatomy API, depending on stack layout). Loop bound
is now derived from the enum.
VocalTract::calcTongueRibs()right-edge scan started atk = NUM_PROFILE_SAMPLES, readingupperProfile/lowerProfileoneelement past the end (read-only; rejected by the
INVALIDguards inthe normal case → no behavior change expected). Now starts at the
last valid index.
Testing
Builds clean on macOS arm64 (cmake Release).
VtlApiTests19/19 andVtlBackendTests1/1 pass. Each commit compiles standalone; new APIpaths exercised under AddressSanitizer.
Known pre-existing issue (NOT addressed here, for the record)
AddressSanitizer shows
EmaPoint::namestrings sharing buffers withXML-parser strings freed at the end of
VocalTract::init()(
XmlNode.cpp); using the anatomy API then makesvtlClose()double-free, so a process can abort at exit (work/results unaffected).
Pre-dates this PR (reproduces on current
main+ these additions) andlives in the XML/EMA ownership code — worth its own issue/fix.