first commit

This commit is contained in:
David Ali
2026-01-28 22:20:52 +01:00
commit c913cacf8d
13 changed files with 1172 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Build Artifacts (Object files and dependencies)
obj/
*.o
*.d
# Generated Source Code
# This is created by the mapgen tool
src/ortho_maps.h
# Executables
mapgen
example_babble
example_names
# System and Editor files (Optional but recommended)
.vscode/
export.sh
export-*.txt

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dávid Ali
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

53
Makefile Normal file
View File

@@ -0,0 +1,53 @@
CC = gcc
CFLAGS = -Wall -Wextra -O2 -MMD -MP -Isrc
LDFLAGS = -lm
CORE_SRCS = src/articulator.c src/articulator_db.c src/transcriber.c src/visualizer.c
CORE_OBJS = $(patsubst src/%.c, obj/%.o, $(CORE_SRCS))
GEN_TARGET = mapgen
GEN_OBJS = obj/mapgen.o obj/articulator_db.o
EX1_OBJ = obj/example_babble.o
EX1_TARGET = example_babble
EX2_OBJ = obj/example_names.o
EX2_TARGET = example_names
ALL_OBJS = $(CORE_OBJS) obj/mapgen.o $(EX1_OBJ) $(EX2_OBJ)
DEPS = $(ALL_OBJS:.o=.d)
all: $(GEN_TARGET) $(EX1_TARGET) $(EX2_TARGET)
$(GEN_TARGET): $(GEN_OBJS)
$(CC) $(CFLAGS) $(GEN_OBJS) -o $@
src/ortho_maps.h: $(GEN_TARGET)
./$(GEN_TARGET) > src/ortho_maps.h
obj/%.o: src/%.c | obj
$(CC) $(CFLAGS) -c $< -o $@
obj/%.o: examples/%.c | obj
$(CC) $(CFLAGS) -c $< -o $@
obj/mapgen.o: tools/mapgen.c | obj
$(CC) $(CFLAGS) -c $< -o $@
obj/transcriber.o: src/ortho_maps.h
obj:
mkdir -p obj
$(EX1_TARGET): $(EX1_OBJ) $(CORE_OBJS)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
$(EX2_TARGET): $(EX2_OBJ) $(CORE_OBJS)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
-include $(DEPS)
clean:
rm -rf obj $(GEN_TARGET) $(EX1_TARGET) $(EX2_TARGET) src/ortho_maps.h
.PHONY: all clean

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
# babbler
**Babbler** is an experimental articulatory speech synthesizer written in C. Unlike traditional Text-to-Speech systems that use pre-recorded samples, Babbler simulates the physics of a vocal tract. It models the mechanics of the human mouth to produce phonemes procedurally.
The engine calculates muscle movements, air pressure, and inertia to "speak" dynamically. It includes an ASCII visualizer to show the mouth state in real time.
## Core Concepts
The simulation relies on a 6-dimensional vector (`f6_t`) that represents the state of the vocal tract.
### The 6 Dimensions of the Voice
1. **tx (Tongue Place):** Position of the tongue from lips (0.0) to glottis (1.0).
2. **ty (Tongue Height):** How close the tongue is to the roof of the mouth.
3. **la (Lip Aperture):** How open or closed the lips are.
4. **lr (Lip Rounding):** Whether the lips are spread or protruding.
5. **gt (Glottal Tension):** Controls voicing, breathing, or stopping airflow.
6. **nz (Velum/Nasality):** Controls airflow through the nose.
### Physics Engine
The system does not just jump between states. It simulates physical constraints:
* **Inertia:** Muscles take time to move. The tongue follows targets with weighted inertia.
* **Aerodynamics:** If the lips close while air is pushed, intra-oral pressure builds up (`pressure`). When released, it creates a "burst" (plosive sounds like P, B, T, K) .
* **Jitter:** Biological noise is added to targets to simulate natural imperfection.
## Features
* **Real-time ASCII Visualizer:** A terminal-based view of the vocal tract showing lips, tongue position, and velum state.
* **Fantasy Language Generation:** Includes generators for distinct linguistic styles:
* **Common:** Standard phonemes.
* **Orcish:** Guttural sounds using uvular and pharyngeal articulators.
* **Elvish:** Soft sounds with liquids and front vowels.
* **Slavic:** Palatalized consonants and specific vowel systems.
* **IPA Transcription:** The system attempts to "hear" itself and transcribe the raw motor output back into IPA or specific orthographies (Polish, German, etc.).
## Build and Run
The project has no external dependencies beyond the standard C library.
### Compilation
Use the provided Makefile to build the synthesizer and tools:
```bash
make
```
### Running the Examples
The project includes several demo modes:
* **`./synth`**: Runs the main simulation. It generates random names in different fantasy languages and visualizes pronunciation.
* **`./example_3`**: Demonstrates "Motor Babbling". This simulates an infant learning to control mouth muscles by exploring attractor states (vowels vs consonants).
## Project Structure
* `src/articulator.c`: The core physics engine handling inertia and aerodynamics.
* `src/articulator_db.c`: Database of phoneme targets (definitions of where the tongue goes for 'a', 'k', 's', etc.).
* `src/visualizer.c`: Code for drawing the ASCII face.
* `src/transcriber.c`: Logic for converting raw motor output into text.
* `tools/mapgen.c`: A utility that automatically generates orthography headers based on anchor points.
## Example Output
When running, the visualizer displays the current state of the mouth:
```text
[MMM] Roof:[ ^ ] G:[~] P:[#####]
```
* **Lips:** `[MMM]` (Closed) vs `[---]` (Open).
* **Roof:** Shows the tongue's position relative to the palate.
* **G:** Glottis state (`~` for vibrating/voiced).
* **P:** Internal air pressure gauge.
## License
MIT

107
examples/example_babble.c Normal file
View File

@@ -0,0 +1,107 @@
/*
* EXAMPLE: Pure Random Motor Babbling
*
* This example bypasses all linguistic rules, phoneme definitions, and attractor states.
* It sends completely random vectors to the articulatory physics engine.
*
* That is the sound of raw, uncoordinated motor commands! Without a 'brain'
* (linguistic rules) to smooth things out and coordinate the glottis with the tongue,
* you essentially get the acoustic equivalent of a seizure or extreme distress.
*
* It proves that human speech is less about having a mouth and more about the
* incredibly precise, learned choreography of closing it.
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "articulator.h"
#include "transcriber.h"
extern char RAW_OUTPUT_BUFFER[];
extern const ortho_rule_t ORTHO_IPA[];
extern const ortho_rule_t ORTHO_POLISH[];
extern const ortho_rule_t ORTHO_HUNGARIAN[];
extern const ortho_rule_t ORTHO_GERMAN[];
extern const ortho_rule_t ORTHO_CYRILLIC[];
extern const ortho_rule_t ORTHO_ORCISH[];
float randf(float min, float max) { return min + ((float)rand() / (float)RAND_MAX) * (max - min); }
// PURE RANDOM GENERATOR
// No concept of vowels, consonants, or linguistic structure.
// Just random points in the 6-dimensional muscle space.
f6_t generate_random_target(void) {
f6_t t;
// 1. Randomize Articulators (0.0 - 1.0)
t.tx = randf(0.0, 1.0); // Tongue Place (Front <-> Back)
t.ty = randf(0.0, 1.0); // Tongue Height (Roof <-> Open)
t.la = randf(0.0, 1.0); // Lips (Closed <-> Wide)
t.lr = randf(0.0, 1.0); // Rounding (Spread <-> Round)
// Velum (Oral vs Nasal)
// We use discrete values (0.0 or 1.0) because continuous random values (e.g. 0.5)
// create physically ambiguous targets that result in constant air leakage.
t.nz = (rand() % 5 == 0) ? 1.0f : 0.0f; // 20% chance of nasal
// 2. Randomize Glottis (Voicing)
// We bias slightly towards voicing (0.5-1.0) to ensure audible output.
// Using pure 0.0-1.0 would result in ~40% silent breathing.
if (rand() % 10 < 2) {
t.gt = randf(0.0, 0.4); // Breath/Silence
} else {
t.gt = randf(0.5, 0.9); // Voicing/Creak
}
return t;
}
void motor_babble(int count) {
printf("\n=== Random Motor Babble (%d gestures) ===\n", count);
// Allocate memory for the stream (1 chunk per gesture)
f6_t* stream = malloc(sizeof(f6_t) * count);
for (int i = 0; i < count; i++) {
stream[i] = generate_random_target();
}
// Run the physics simulation
simulate_motor_stream(stream, count);
char word[256];
printf("RAW: \"%s\"\n", RAW_OUTPUT_BUFFER);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_IPA);
printf("IPA: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_POLISH);
printf(" PL: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_HUNGARIAN);
printf(" HU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_GERMAN);
printf(" DE: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_CYRILLIC);
printf(" RU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_ORCISH);
printf("ORC: \"%s\"\n", word);
free(stream);
}
int main(void) {
srand(time(NULL));
// Generate 5 random sequences of varying length
for (int i = 0; i < 5; i++) {
motor_babble(8 + rand() % 8);
}
return 0;
}

178
examples/example_names.c Normal file
View File

@@ -0,0 +1,178 @@
/*
* EXAMPLE: Structured Name Generation (The "Brain")
*
* Unlike the random motor babbler, this program applies high-level linguistic
* constraints to the articulatory physics engine. It defines specific phoneme
* subsets (Languages) and grammar rules (Syllable Structure) to guide the mouth.
*
* It demonstrates that by constraining the chaos of the physics engine with
* simple rules (Attractor States), we can generate distinct, recognizable
* "accents" or "languages".
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "articulator.h"
#include "transcriber.h"
extern char RAW_OUTPUT_BUFFER[];
extern const phoneme_t PHONEME_DB[];
extern const ortho_rule_t ORTHO_IPA[];
extern const ortho_rule_t ORTHO_POLISH[];
extern const ortho_rule_t ORTHO_HUNGARIAN[];
extern const ortho_rule_t ORTHO_GERMAN[];
extern const ortho_rule_t ORTHO_CYRILLIC[];
extern const ortho_rule_t ORTHO_ORCISH[];
/* --- LANGUAGE DEFINITIONS --- */
/* Standard Fantasy/English-ish */
const char* LANG_COMMON[] = {"p", "b", "t", "d", "k", "g", "m", "n", "f", "v", "s", "z", "ʃ", "h", "r",
"l", "w", "j", "i", "u", "e", "o", "a", "ə", "ɪ", "ɛ", "æ", "ɔ", NULL};
const char* LANG_SLAVIC[] = {
// Vowels (Simple 6 system)
"i", "u", "e", "o", "a", "ɨ", // ɨ is Polish 'y'
// Consonants
"p", "b", "t", "d", "k", "g", "m", "n", "ɲ", // n, ń
"f", "v", "s", "z", "x", // f, w, s, z, ch
"ts", "dz", // c, dz
"ʃ", "ʒ", "", "", // sz, ż, cz, dż
"ɕ", "ʑ", "", "", // ś, ź, ć, dź
"l", "r", "j", "w", // l, r, j, ł
NULL};
/* Harsh/Orcish (Back of throat, Guttural) */
const char* LANG_ORCISH[] = {"k", "g", "q", "ɢ", "ʔ", "x", "χ", "ʁ", "h", "p", "b", "t", "d", "u", "o", "ɑ", "ɔ", NULL};
/* Soft/Elvish (Liquids, Fricatives, Front Vowels) */
const char* LANG_ELVISH[] = {"p", "b", "t", "d", "m", "n", "f", "v", "s", "z", "ʃ", "ʒ", "θ", "ð",
"l", "ʎ", "r", "w", "j", "i", "y", "e", "ø", "a", "ɛ", "œ", NULL};
/* --- FILTER HELPER --- */
/* Check if an IPA symbol exists in the allowed list */
int is_allowed(const char* ipa, const char** allowed_list) {
for (int i = 0; allowed_list[i] != NULL; i++) {
if (strcmp(ipa, allowed_list[i]) == 0) return 1;
}
return 0;
}
/* --- UPDATED GENERATOR --- */
void generate_name(const char* race_name, const char** allowed_sounds, int syllables) {
printf("\n=== Generating %s Name (%d syl) ===\n", race_name, syllables);
// 1. Build Index Cache for this Language
int v_indices[100], v_count = 0;
int c_indices[100], c_count = 0;
for (int i = 0; PHONEME_DB[i].ipa != NULL; i++) {
// Only add if it's in the allowed list
if (is_allowed(PHONEME_DB[i].ipa, allowed_sounds)) {
if (is_vowel_index(i)) {
if (v_count < 100) v_indices[v_count++] = i;
} else {
if (c_count < 100) c_indices[c_count++] = i;
}
}
}
if (v_count == 0 || c_count == 0) {
printf("Error: No sounds found for %s\n", race_name);
return;
}
// 2. Build Sequence (C-V-C pattern)
int len = 0;
int seq[100];
for (int s = 0; s < syllables; s++) {
// Onset
if (rand() % 10 < 7) seq[len++] = c_indices[rand() % c_count];
// Nucleus
seq[len++] = v_indices[rand() % v_count];
// Coda
if (rand() % 10 < 4) seq[len++] = c_indices[rand() % c_count];
}
// 3. Simulate & Transcribe
simulate_sequence(seq, len);
}
int main(void) {
srand(time(NULL));
char word[256];
for (int i = 0; i < 3; i++) {
generate_name("Human", LANG_COMMON, 2 + i);
printf("RAW: \"%s\"\n", RAW_OUTPUT_BUFFER);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_IPA);
printf("IPA: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_POLISH);
printf(" PL: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_HUNGARIAN);
printf(" HU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_GERMAN);
printf(" DE: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_CYRILLIC);
printf(" RU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_ORCISH);
printf("ORC: \"%s\"\n", word);
}
for (int i = 0; i < 3; i++) {
generate_name("Slavic", LANG_SLAVIC, 2 + i);
printf("RAW: \"%s\"\n", RAW_OUTPUT_BUFFER);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_IPA);
printf("IPA: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_POLISH);
printf(" PL: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_HUNGARIAN);
printf(" HU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_GERMAN);
printf(" DE: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_CYRILLIC);
printf(" RU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_ORCISH);
printf("ORC: \"%s\"\n", word);
}
for (int i = 0; i < 3; i++) {
generate_name("Orcish", LANG_ORCISH, 2 + i);
printf("RAW: \"%s\"\n", RAW_OUTPUT_BUFFER);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_IPA);
printf("IPA: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_POLISH);
printf(" PL: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_HUNGARIAN);
printf(" HU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_GERMAN);
printf(" DE: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_CYRILLIC);
printf(" RU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_ORCISH);
printf("ORC: \"%s\"\n", word);
}
for (int i = 0; i < 3; i++) {
generate_name("Elvish", LANG_ELVISH, 2 + i);
printf("RAW: \"%s\"\n", RAW_OUTPUT_BUFFER);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_IPA);
printf("IPA: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_POLISH);
printf(" PL: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_HUNGARIAN);
printf(" HU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_GERMAN);
printf(" DE: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_CYRILLIC);
printf(" RU: \"%s\"\n", word);
transcribe(RAW_OUTPUT_BUFFER, word, ORTHO_ORCISH);
printf("ORC: \"%s\"\n", word);
}
return 0;
}

186
src/articulator.c Normal file
View File

@@ -0,0 +1,186 @@
#include "articulator.h"
#include <stdio.h>
#include <string.h>
#include <float.h>
#include <unistd.h>
extern const phoneme_t PHONEME_DB[];
// Global buffer for raw sound stream
char RAW_OUTPUT_BUFFER[4096];
// Inertia Constants: Tuned for Syllabic Speed
// Higher values = faster movement, Lower values = more drag/slurring
const f6_t INERTIA = {
.tx = 0.45f, // Tongue Place
.ty = 0.45f, // Tongue Height
.la = 0.6f, // Lips (Move faster than tongue)
.lr = 0.5f,
.gt = 0.6f,
.nz = 0.3f // Velum (Nasal Lag)
};
static void append_to_stream(char** ptr, const char* str) {
size_t len = strlen(str);
memcpy(*ptr, str, len);
*ptr += len;
*(*ptr)++ = '\x1F'; // Add unit separator
**ptr = '\0';
}
// Check if airflow is physically blocked (Lips closed or tongue touching roof)
static inline int is_blocked(f6_t s) {
if (s.la < 0.1f) return 1; // Lips closed
if (s.ty < 0.1f) return 1; // Tongue touching roof
return 0;
}
const char* sample_sound(f6_t current) {
float min_dist = FLT_MAX;
const char* best_ipa = "";
// 1. First Pass: Standard Strict Matching
// Effective for consonants which require specific articulatory precision
for (int i = 0; PHONEME_DB[i].ipa; i++) {
float d = f6_dist_sq(current, PHONEME_DB[i].target, PHONEME_DB[i].weights);
// 0.05 is a strict threshold for "clean" sounds
if (d < 0.05f && d < min_dist) {
min_dist = d;
best_ipa = PHONEME_DB[i].ipa;
}
}
// 2. Second Pass: Fuzzy Vowel Matching
// If no strict match found, but the tract is open and voiced, it must be a vowel.
// We check if the vocal tract is Voiced (gt > 0.4) and Open (not blocked).
if (best_ipa[0] == '\0' && current.gt > 0.4f && !is_blocked(current)) {
float min_vowel_dist = FLT_MAX;
for (int i = 0; PHONEME_DB[i].ipa; i++) {
// Vowels in DB have low lip aperture weight (< 1.0)
if (PHONEME_DB[i].weights.la < 1.0f) {
float d = f6_dist_sq(current, PHONEME_DB[i].target, PHONEME_DB[i].weights);
if (d < min_vowel_dist) {
min_vowel_dist = d;
best_ipa = PHONEME_DB[i].ipa;
}
}
}
}
// 3. Dynamic Transition: Breathy H
// Occurs when glottis is slightly tense but lips are open
if (current.gt > 0.1f && current.gt < 0.4f && current.la > 0.8f) return "h";
return best_ipa;
}
// Core simulation step for a single frame
// Returns the sound character, or "." for silence
const char* process_physics_frame(vocal_tract_t* vt, f6_t target) {
// 1. Physics Integration (Apply Inertia)
f6_t diff = f6_sub(target, vt->vector);
vt->vector.tx += diff.tx * INERTIA.tx;
vt->vector.ty += diff.ty * INERTIA.ty;
vt->vector.la += diff.la * INERTIA.la;
vt->vector.lr += diff.lr * INERTIA.lr;
vt->vector.gt += diff.gt * INERTIA.gt;
vt->vector.nz += diff.nz * INERTIA.nz;
// 2. Aerodynamics Engine
int mouth_sealed = is_blocked(vt->vector);
int nasal_vent = (vt->vector.nz > 0.4f);
const char* sound_out = ".";
if (mouth_sealed && !nasal_vent) {
// BLOCKED: Build Pressure (Plosive preparation)
vt->pressure += 0.05f;
if (vt->pressure > 1.0f) vt->pressure = 1.0f;
sound_out = ".";
} else {
// OPEN: Check for Burst release
if (vt->pressure > 0.2f) {
// Burst Event: Determine sound based on where the release happened
if (vt->vector.la < 0.2f) {
// Labial Burst
sound_out = (vt->vector.gt > 0.5f) ? "b" : "p";
} else {
// Lingual Burst (Tongue)
if (vt->vector.tx > 0.7f)
sound_out = (vt->vector.gt > 0.5f) ? "g" : "k"; // Back
else
sound_out = (vt->vector.gt > 0.5f) ? "d" : "t"; // Front
}
vt->pressure = 0.0f; // Vent pressure
} else {
// Normal Flow
sound_out = sample_sound(vt->vector);
if (sound_out[0] == '\0') sound_out = ".";
}
}
return sound_out;
}
void simulate_sequence(int* target_indices, int count) {
vocal_tract_t vt = {.vector = {0.5, 0.5, 0.5, 0.0, 0.0, 0.0}, .pressure = 0.0};
char* buf_ptr = RAW_OUTPUT_BUFFER;
RAW_OUTPUT_BUFFER[0] = '\0';
for (int i = 0; i < count; i++) {
f6_t target = PHONEME_DB[target_indices[i]].target;
// Run 30 frames per phoneme (~300ms)
for (int step = 0; step < 30; step++) {
const char* sound_out = process_physics_frame(&vt, target);
print_face(vt.vector, vt.pressure);
usleep(5000); // 5ms sleep for visualizer
if (sound_out[0] != '.') {
append_to_stream(&buf_ptr, sound_out);
} else {
append_to_stream(&buf_ptr, ".");
}
}
append_to_stream(&buf_ptr, "|"); // Phoneme boundary
}
// Handle trailing pressure (release check)
if (vt.pressure > 0.1f) {
// Target a neutral open state to release pressure
f6_t release_target = {0.5, 1.0, 1.0, 0.0, 0.0, 0.0};
// Run 10 extra frames for the release
for (int step = 0; step < 10; step++) {
const char* sound_out = process_physics_frame(&vt, release_target);
if (sound_out[0] != '.') append_to_stream(&buf_ptr, sound_out);
}
}
printf("\n");
}
void simulate_motor_stream(f6_t* targets, int count) {
vocal_tract_t vt = {.vector = {0.5, 0.5, 0.5, 0.0, 0.0, 0.0}, .pressure = 0.0};
char* buf_ptr = RAW_OUTPUT_BUFFER;
RAW_OUTPUT_BUFFER[0] = '\0';
for (int i = 0; i < count; i++) {
f6_t target = targets[i];
// Frame count fixed at 30 for consistency
for (int step = 0; step < 30; step++) {
const char* sound_out = process_physics_frame(&vt, target);
print_face(vt.vector, vt.pressure);
usleep(5000);
if (sound_out[0] != '.')
append_to_stream(&buf_ptr, sound_out);
else
append_to_stream(&buf_ptr, ".");
}
append_to_stream(&buf_ptr, "|");
}
printf("\n");
}

52
src/articulator.h Normal file
View File

@@ -0,0 +1,52 @@
#ifndef ARTICULATOR_H
#define ARTICULATOR_H
#include <stddef.h>
// The 6-Dimensional Feature Vector representing the vocal tract state
typedef struct {
float tx; // Tongue Place (0.0=Lips ... 1.0=Glottis)
float ty; // Tongue Height (0.0=Roof ... 1.0=Open Low)
float la; // Lip Aperture (0.0=Closed ... 1.0=Wide)
float lr; // Lip Rounding (0.0=Spread ... 1.0=Protruded)
float gt; // Glottal Tension (0.0=Breath, 0.5=Voice, 1.0=Creak/Stop)
float nz; // Velum/Nasality (0.0=Oral, 1.0=Nasal)
} f6_t;
// The Physical State of the Mouth (Includes Air Pressure)
typedef struct {
f6_t vector;
float pressure; // Intra-oral air pressure (0.0 - 1.0)
} vocal_tract_t;
// Phoneme Definition (Target state + physical constraints)
typedef struct {
const char* ipa;
f6_t target;
f6_t weights;
} phoneme_t;
// --- Vector Math Helpers ---
static inline f6_t f6_sub(f6_t a, f6_t b) {
return (f6_t){a.tx - b.tx, a.ty - b.ty, a.la - b.la, a.lr - b.lr, a.gt - b.gt, a.nz - b.nz};
}
// Calculate weighted squared distance between current state and target
static inline float f6_dist_sq(f6_t state, f6_t target, f6_t weights) {
f6_t diff = f6_sub(state, target);
return (diff.tx * diff.tx * weights.tx) + (diff.ty * diff.ty * weights.ty) + (diff.la * diff.la * weights.la) +
(diff.lr * diff.lr * weights.lr) + (diff.gt * diff.gt * weights.gt) + (diff.nz * diff.nz * weights.nz);
}
// --- API ---
extern const phoneme_t PHONEME_DB[];
extern char RAW_OUTPUT_BUFFER[]; // The raw stream for the transcriber
// Run the physics simulation on a list of phoneme indices
void simulate_sequence(int* target_indices, int count);
void simulate_motor_stream(f6_t* target_vectors, int count);
void print_face(f6_t v, float pressure);
int is_vowel_index(int db_index);
#endif

200
src/articulator_db.c Normal file
View File

@@ -0,0 +1,200 @@
#include "articulator.h"
#include <stddef.h>
#include <string.h>
// --- WEIGHT PRESETS (Physics Constraints) ---
// These determine how strictly a muscle must match its target.
// Low weight = muscle can be lazy. High weight = precise positioning required.
const f6_t W_STOP = {2.0, 2.0, 2.0, 0.5, 1.0, 2.0}; // Seal is critical
const f6_t W_FRIC = {2.0, 2.0, 1.5, 0.5, 1.0, 1.0}; // Gap size is critical
const f6_t W_APPROX = {1.5, 1.5, 1.0, 1.0, 1.0, 1.0}; // Position is loose
const f6_t W_VOWEL = {2.0, 2.0, 0.5, 2.0, 1.0, 1.0}; // Shape is critical (LA is low weight)
const f6_t W_GLIDE = {2.0, 2.0, 1.5, 2.0, 1.0, 1.0};
const phoneme_t PHONEME_DB[] = {
// ==================================================================================
// 1. PULMONIC CONSONANTS
// ==================================================================================
// Mapped by Place (tx) and Manner (ty/la)
// Places: Bilabial=0.5, Dental=0.15, Alveolar=0.2, Velar=0.8, Uvular=0.9, Glottal=1.0
// --- NASALS (nz=1.0) ---
{"m", {0.5, 0.5, 0.0, 0.0, 0.7, 1.0}, W_STOP}, // Bilabial
{"ɱ", {0.5, 0.5, 0.15, 0.0, 0.7, 1.0}, W_STOP}, // Labiodental
{"n", {0.2, 0.0, 0.5, 0.0, 0.7, 1.0}, W_STOP}, // Alveolar
{"ɳ", {0.4, 0.0, 0.5, 0.0, 0.7, 1.0}, W_STOP}, // Retroflex
{"ɲ", {0.6, 0.0, 0.5, 0.0, 0.7, 1.0}, W_STOP}, // Palatal (Polish 'ń')
{"ŋ", {0.8, 0.0, 0.8, 0.0, 0.7, 1.0}, W_STOP}, // Velar (English 'ng')
{"ɴ", {0.9, 0.0, 0.8, 0.0, 0.7, 1.0}, W_STOP}, // Uvular
// --- PLOSIVES (la=0 or ty=0, nz=0) ---
{"p", {0.5, 0.5, 0.0, 0.0, 0.0, 0.0}, W_STOP}, // Bilabial Unvoiced
{"b", {0.5, 0.5, 0.0, 0.0, 0.7, 0.0}, W_STOP}, // Bilabial Voiced
{"t", {0.2, 0.0, 0.5, 0.0, 0.0, 0.0}, W_STOP}, // Alveolar
{"d", {0.2, 0.0, 0.5, 0.0, 0.7, 0.0}, W_STOP},
{"ʈ", {0.4, 0.0, 0.5, 0.0, 0.0, 0.0}, W_STOP}, // Retroflex
{"ɖ", {0.4, 0.0, 0.5, 0.0, 0.7, 0.0}, W_STOP},
{"c", {0.6, 0.0, 0.5, 0.0, 0.0, 0.0}, W_STOP}, // Palatal
{"ɟ", {0.6, 0.0, 0.5, 0.0, 0.7, 0.0}, W_STOP},
{"k", {0.8, 0.0, 0.8, 0.0, 0.0, 0.0}, W_STOP}, // Velar
{"g", {0.8, 0.0, 0.8, 0.0, 0.7, 0.0}, W_STOP},
{"q", {0.9, 0.0, 0.8, 0.0, 0.0, 0.0}, W_STOP}, // Uvular
{"ɢ", {0.9, 0.0, 0.8, 0.0, 0.7, 0.0}, W_STOP},
{"ʔ", {1.0, 1.0, 1.0, 0.0, 1.0, 0.0}, W_STOP}, // Glottal Stop (gt=1.0)
// --- FRICATIVES (ty=0.1 or la=0.15) ---
{"ɸ", {0.5, 0.5, 0.1, 0.0, 0.0, 0.0}, W_FRIC}, // Bilabial
{"β", {0.5, 0.5, 0.1, 0.0, 0.7, 0.0}, W_FRIC},
{"f", {0.5, 0.5, 0.15, 0.0, 0.0, 0.0}, W_FRIC}, // Labiodental
{"v", {0.5, 0.5, 0.15, 0.0, 0.7, 0.0}, W_FRIC},
{"θ", {0.15, 0.1, 0.5, 0.0, 0.0, 0.0}, W_FRIC}, // Dental (English 'th'in)
{"ð", {0.15, 0.1, 0.5, 0.0, 0.7, 0.0}, W_FRIC}, // Dental (English 'th'is)
{"s", {0.2, 0.1, 0.5, 0.0, 0.0, 0.0}, W_FRIC}, // Alveolar
{"z", {0.2, 0.1, 0.5, 0.0, 0.7, 0.0}, W_FRIC},
{"ʃ", {0.3, 0.1, 0.6, 0.1, 0.0, 0.0}, W_FRIC}, // Post-Alveolar (English 'sh')
{"ʒ", {0.3, 0.1, 0.6, 0.1, 0.7, 0.0}, W_FRIC},
{"ʂ", {0.4, 0.1, 0.6, 0.0, 0.0, 0.0}, W_FRIC}, // Retroflex
{"ʐ", {0.4, 0.1, 0.6, 0.0, 0.7, 0.0}, W_FRIC},
{"ç", {0.6, 0.1, 0.6, 0.0, 0.0, 0.0}, W_FRIC}, // Palatal
{"ʝ", {0.6, 0.1, 0.6, 0.0, 0.7, 0.0}, W_FRIC},
{"x", {0.8, 0.1, 0.8, 0.0, 0.0, 0.0}, W_FRIC}, // Velar (Loch)
{"ɣ", {0.8, 0.1, 0.8, 0.0, 0.7, 0.0}, W_FRIC},
{"χ", {0.9, 0.1, 0.8, 0.0, 0.0, 0.0}, W_FRIC}, // Uvular
{"ʁ", {0.9, 0.1, 0.8, 0.0, 0.7, 0.0}, W_FRIC}, // Uvular (French R approx)
{"ħ", {0.95, 0.5, 0.8, 0.0, 0.0, 0.0}, W_FRIC}, // Pharyngeal
{"ʕ", {0.95, 0.5, 0.8, 0.0, 0.7, 0.0}, W_FRIC},
{"h", {1.0, 1.0, 1.0, 0.0, 0.2, 0.0}, W_FRIC}, // Glottal
{"ɦ", {1.0, 1.0, 1.0, 0.0, 0.5, 0.0}, W_FRIC}, // Glottal Voiced
// --- LATERAL FRICATIVES ---
{"ɬ", {0.2, 0.1, 0.5, 0.0, 0.0, 0.0}, W_FRIC}, // Welsh ll
{"ɮ", {0.2, 0.1, 0.5, 0.0, 0.7, 0.0}, W_FRIC}, // Zulu
// --- APPROXIMANTS (ty=0.3 - 0.4) ---
{"ʋ", {0.5, 0.5, 0.3, 0.0, 0.7, 0.0}, W_APPROX}, // Labiodental
{"ɹ", {0.3, 0.3, 0.5, 0.0, 0.7, 0.0}, W_APPROX}, // Alveolar (English R)
{"ɻ", {0.4, 0.3, 0.5, 0.0, 0.7, 0.0}, W_APPROX}, // Retroflex
{"j", {0.6, 0.25, 0.5, 0.0, 0.7, 0.0}, W_GLIDE}, // Palatal (English Y)
{"ɰ", {0.8, 0.3, 0.8, 0.0, 0.7, 0.0}, W_APPROX}, // Velar
// --- LATERAL APPROXIMANTS ---
{"l", {0.2, 0.3, 0.5, 0.0, 0.7, 0.0}, W_APPROX}, // Alveolar
{"ɭ", {0.4, 0.3, 0.5, 0.0, 0.7, 0.0}, W_APPROX}, // Retroflex
{"ʎ", {0.6, 0.3, 0.5, 0.0, 0.7, 0.0}, W_APPROX}, // Palatal
{"ʟ", {0.8, 0.3, 0.8, 0.0, 0.7, 0.0}, W_APPROX}, // Velar
// --- TRILLS & TAPS ---
{"ʙ", {0.5, 0.5, 0.0, 0.0, 0.7, 0.0}, W_STOP}, // Bilabial Trill
{"r", {0.25, 0.2, 0.5, 0.0, 0.7, 0.0}, W_FRIC}, // Alveolar Trill (Rolled R)
{"ʀ", {0.9, 0.2, 0.8, 0.0, 0.7, 0.0}, W_FRIC}, // Uvular Trill
{"ɾ", {0.2, 0.05, 0.5, 0.0, 0.7, 0.0}, W_APPROX}, // Alveolar Tap
{"ɽ", {0.4, 0.05, 0.5, 0.0, 0.7, 0.0}, W_APPROX}, // Retroflex Tap
// ==================================================================================
// 2. NON-PULMONIC CONSONANTS
// ==================================================================================
// Modeled kinematically. Clicks=Strong Stops. Ejectives=Stop+Glottal Close
// --- CLICKS ---
{"ʘ", {0.5, 0.5, 0.0, 0.0, 0.0, 0.0}, W_STOP}, // Bilabial Click
{"ǀ", {0.15, 0.0, 0.5, 0.0, 0.0, 0.0}, W_STOP}, // Dental Click
{"!", {0.2, 0.0, 0.5, 0.0, 0.0, 0.0}, W_STOP}, // Alveolar Click
{"ǂ", {0.6, 0.0, 0.5, 0.0, 0.0, 0.0}, W_STOP}, // Palatoalveolar Click
{"ǁ", {0.2, 0.0, 0.5, 0.0, 0.0, 0.0}, W_STOP}, // Alveolar Lateral Click
// --- IMPLOSIVES ---
{"ɓ", {0.5, 0.5, 0.0, 0.0, 0.4, 0.0}, W_STOP}, // Bilabial
{"ɗ", {0.2, 0.0, 0.5, 0.0, 0.4, 0.0}, W_STOP}, // Alveolar
{"ʄ", {0.6, 0.0, 0.5, 0.0, 0.4, 0.0}, W_STOP}, // Palatal
{"ɠ", {0.8, 0.0, 0.8, 0.0, 0.4, 0.0}, W_STOP}, // Velar
{"ʛ", {0.9, 0.0, 0.8, 0.0, 0.4, 0.0}, W_STOP}, // Uvular
// --- EJECTIVES ---
{"p'", {0.5, 0.5, 0.0, 0.0, 1.0, 0.0}, W_STOP},
{"t'", {0.2, 0.0, 0.5, 0.0, 1.0, 0.0}, W_STOP},
{"k'", {0.8, 0.0, 0.8, 0.0, 1.0, 0.0}, W_STOP},
{"s'", {0.2, 0.1, 0.5, 0.0, 1.0, 0.0}, W_FRIC},
// ==================================================================================
// 3. CO-ARTICULATED CONSONANTS
// ==================================================================================
{"w", {0.9, 0.25, 0.4, 1.0, 0.7, 0.0}, W_GLIDE}, // Labial-Velar Approx
{"ʍ", {0.9, 0.25, 0.4, 1.0, 0.0, 0.0}, W_GLIDE}, // Voiceless w
{"ɥ", {0.6, 0.25, 0.4, 1.0, 0.7, 0.0}, W_GLIDE}, // Labial-Palatal Approx
{"ʜ", {0.95, 0.5, 0.8, 0.0, 0.0, 0.0}, W_FRIC}, // Voiceless Epiglottal Fricative
{"ʢ", {0.95, 0.5, 0.8, 0.0, 0.7, 0.0}, W_FRIC}, // Voiced Epiglottal Fricative
// Polish/Chinese Alveolo-palatal
{"ɕ", {0.35, 0.1, 0.5, 0.0, 0.0, 0.0}, W_FRIC}, // Polish 'ś'
{"ʑ", {0.35, 0.1, 0.5, 0.0, 0.7, 0.0}, W_FRIC}, // Polish 'ź'
// Swedish Sj-sound (Velar+Labial Fricative approximation)
{"ɧ", {0.8, 0.1, 0.4, 0.5, 0.0, 0.0}, W_FRIC},
// Double Stops (Approximated by closing lips + back tongue)
{"kp", {0.8, 0.0, 0.0, 0.0, 0.0, 0.0}, W_STOP},
{"gb", {0.8, 0.0, 0.0, 0.0, 0.7, 0.0}, W_STOP},
// ==================================================================================
// 4. VOWELS
// ==================================================================================
// ty: 0.1=Close, 0.4=Mid, 0.7=OpenMid, 1.0=Open
// tx: 0.1=Front, 0.5=Central, 0.9=Back
// lr: 0.0=Unround, 1.0=Round
// --- CLOSE ---
{"i", {0.1, 0.2, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Front Unround
{"y", {0.1, 0.2, 0.5, 1.0, 0.7, 0.0}, W_VOWEL}, // Front Round
{"ɨ", {0.5, 0.2, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Central Unround
{"ʉ", {0.5, 0.2, 0.5, 1.0, 0.7, 0.0}, W_VOWEL}, // Central Round
{"ɯ", {0.9, 0.2, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Back Unround
{"u", {0.9, 0.2, 0.5, 1.0, 0.7, 0.0}, W_VOWEL}, // Back Round
// --- NEAR-CLOSE ---
{"ɪ", {0.2, 0.25, 0.8, 0.0, 0.7, 0.0}, W_VOWEL},
{"ʏ", {0.2, 0.25, 0.5, 0.8, 0.7, 0.0}, W_VOWEL},
{"ʊ", {0.8, 0.25, 0.6, 0.8, 0.7, 0.0}, W_VOWEL},
// --- CLOSE-MID ---
{"e", {0.1, 0.4, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Front Unround
{"ø", {0.1, 0.4, 0.5, 1.0, 0.7, 0.0}, W_VOWEL}, // Front Round
{"ɘ", {0.5, 0.4, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Central
{"ɵ", {0.5, 0.4, 0.5, 1.0, 0.7, 0.0}, W_VOWEL},
{"ɤ", {0.9, 0.4, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Back Unround
{"o", {0.9, 0.4, 0.6, 1.0, 0.7, 0.0}, W_VOWEL}, // Back Round
// --- MID (Schwa) ---
{"ə", {0.5, 0.5, 0.8, 0.0, 0.7, 0.0}, W_VOWEL},
// --- OPEN-MID ---
{"ɛ", {0.1, 0.7, 0.8, 0.0, 0.7, 0.0}, W_VOWEL},
{"œ", {0.1, 0.7, 0.5, 1.0, 0.7, 0.0}, W_VOWEL},
{"ɜ", {0.5, 0.7, 0.8, 0.0, 0.7, 0.0}, W_VOWEL},
{"ɞ", {0.5, 0.7, 0.5, 1.0, 0.7, 0.0}, W_VOWEL},
{"ʌ", {0.9, 0.7, 0.8, 0.0, 0.7, 0.0}, W_VOWEL},
{"ɔ", {0.9, 0.7, 0.6, 1.0, 0.7, 0.0}, W_VOWEL},
// --- NEAR-OPEN ---
{"æ", {0.2, 0.85, 0.8, 0.0, 0.7, 0.0}, W_VOWEL},
{"ɐ", {0.5, 0.85, 0.8, 0.0, 0.7, 0.0}, W_VOWEL},
// --- OPEN ---
{"a", {0.1, 1.0, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Front Unround
{"ɶ", {0.1, 1.0, 0.5, 1.0, 0.7, 0.0}, W_VOWEL}, // Front Round
{"ɑ", {0.9, 1.0, 0.8, 0.0, 0.7, 0.0}, W_VOWEL}, // Back Unround
{"ɒ", {0.9, 1.0, 0.6, 1.0, 0.7, 0.0}, W_VOWEL}, // Back Round
{NULL}};
int is_vowel_index(int db_index) {
if (db_index < 0) return 0;
// Identify vowels by their Lip Aperture weight.
// Vowels allow variable lip aperture (weight < 1.0), Consonants are strict.
if (PHONEME_DB[db_index].weights.la < 1.0f) {
return 1;
}
return 0;
}

76
src/transcriber.c Normal file
View File

@@ -0,0 +1,76 @@
#include "transcriber.h"
#include <string.h>
#include <stdio.h>
#include "ortho_maps.h"
#define MIN_DURATION 4 // Sound must persist for this many frames to be transcribed
#define LONG_DURATION 45 // Sound must persist for this many frames to be doubled
const char* map_text(const char* ipa, const ortho_rule_t* rules) {
for (int i = 0; rules[i].ipa; i++) {
if (strcmp(rules[i].ipa, ipa) == 0) return rules[i].text;
}
return ipa;
}
void transcribe(const char* raw_stream, char* out_buf, const ortho_rule_t* rules) {
char work_buf[4096];
strncpy(work_buf, raw_stream, 4095);
char* token = strtok(work_buf, "\x1F|");
char last_ipa[16] = "";
int duration = 0;
out_buf[0] = '\0';
while (token != NULL) {
if (strcmp(token, ".") == 0) {
// Silence implies a break in the sound
if (duration >= MIN_DURATION && last_ipa[0] != '\0') {
const char* txt = map_text(last_ipa, rules);
strcat(out_buf, txt);
// Double the letter if held for a long time
if (duration >= LONG_DURATION) {
strcat(out_buf, txt);
}
}
duration = 0;
last_ipa[0] = '\0';
} else if (strcmp(token, last_ipa) == 0) {
duration++;
} else {
// New Sound Detected
if (duration >= MIN_DURATION && last_ipa[0] != '\0') {
const char* txt = map_text(last_ipa, rules);
strcat(out_buf, txt);
// Double the letter if held for a long time
if (duration >= LONG_DURATION) {
strcat(out_buf, txt);
}
}
// Exception: BURSTS (p,t,k) are physically short events.
// If we detect a stop char, we accept it even with short duration
int is_burst = (strchr("ptkdbg", token[0]) != NULL);
if (is_burst) {
strcat(out_buf, map_text(token, rules));
duration = 0; // Consumed
last_ipa[0] = '\0';
} else {
strcpy(last_ipa, token);
duration = 1;
}
}
token = strtok(NULL, "\x1F|");
}
// Handle tail
if (duration >= MIN_DURATION && last_ipa[0] != '\0') {
const char* txt = map_text(last_ipa, rules);
strcat(out_buf, txt);
if (duration >= LONG_DURATION) {
strcat(out_buf, txt);
}
}
}

11
src/transcriber.h Normal file
View File

@@ -0,0 +1,11 @@
#ifndef TRANSCRIBER_H
#define TRANSCRIBER_H
typedef struct {
const char* ipa;
const char* text;
} ortho_rule_t;
void transcribe(const char* raw_stream, char* out_buf, const ortho_rule_t* rules);
#endif

61
src/visualizer.c Normal file
View File

@@ -0,0 +1,61 @@
#include <stdio.h>
#include "articulator.h"
// VISUALIZATION OF THE VOCAL TRACT
//
// Nose: / \ Velum (nz)
// | | /
// Lips: |---| v <-- Roof
// (la) | T | <-- Tongue (tx, ty)
// \___/
//
// Glottis: [~] (Vibrating) or [ ] (Open)
void print_face(f6_t v, float pressure) {
// 1. LIPS REPRESENTATION
char lips[4] = " ";
if (v.la < 0.1)
sprintf(lips, "MMM"); // Closed
else if (v.lr > 0.6)
sprintf(lips, "(O)"); // Rounded
else
sprintf(lips, "---"); // Spread/Open
// 2. TONGUE REPRESENTATION
// Map TX (0.0-1.0) to a visual position 0-10
int t_pos = (int)(v.tx * 10);
if (t_pos < 0) t_pos = 0;
if (t_pos > 10) t_pos = 10;
// Map TY (Height) to char
char tongue_char = ' ';
if (v.ty < 0.1)
tongue_char = '^'; // Touching Roof (Stop)
else if (v.ty < 0.4)
tongue_char = '-'; // High (Fricative/Glide)
else
tongue_char = '_'; // Low (Vowel)
// Build the "Mouth Line"
char mouth_line[20];
for (int i = 0; i < 12; i++) mouth_line[i] = ' ';
mouth_line[12] = '\0';
mouth_line[t_pos] = tongue_char;
// 3. VELUM
char velum = (v.nz > 0.4) ? 'v' : '-'; // v=Open(Nasal), -=Closed
// 4. GLOTTIS
char glottis = (v.gt > 0.5) ? '~' : ' ';
// 5. PRESSURE GAUGE
int p_bars = (int)(pressure * 5);
char p_str[6] = " ";
for (int i = 0; i < p_bars; i++) p_str[i] = '#';
// DRAW
// Single line output for animation effect:
// Lips | Tongue Position | Velum | Glottis | Pressure
printf("\r[%s] Roof:[%s%c] G:[%c] P:[%s] ", lips, mouth_line, velum, glottis, p_str);
fflush(stdout);
}

120
tools/mapgen.c Normal file
View File

@@ -0,0 +1,120 @@
/* tools/mapgen.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
// Import physics directly to access PHONEME_DB and f6_t
// Assumes compilation from the main directory
#include "../src/articulator.h"
// External declaration of the database (located in articulator_db.c)
extern const phoneme_t PHONEME_DB[];
// --- ANCHOR DEFINITIONS ---
// Anchors serve as reference points for how a specific language writes specific sounds
typedef struct {
const char* spelling;
const char* ipa_ref;
} anchor_t;
const anchor_t ANCHORS_HUNGARIAN[] = {
{"a", "ɒ"}, {"a", "ɑ"}, {"á", "a"}, {"e", "ɛ"}, {"é", "e"}, {"i", "i"}, {"i", "ɨ"}, {"o", "o"},
{"ó", "o"}, {"u", "ʊ"}, {"ú", "u"}, {"ö", "ø"}, {"ő", "ø"}, {"ü", "y"}, {"ű", "y"}, {"p", "p"},
{"b", "b"}, {"t", "t"}, {"d", "d"}, {"k", "k"}, {"g", "g"}, {"m", "m"}, {"n", "n"}, {"ny", "ɲ"},
{"f", "f"}, {"v", "v"}, {"s", "ʃ"}, {"sz", "s"}, {"z", "z"}, {"zs", "ʒ"}, {"ty", "c"}, {"gy", "ɟ"},
{"l", "l"}, {"ly", "ʎ"}, {"r", "r"}, {"h", "h"}, {"c\u035Ch", "x"}, {"j", "j"}, {NULL, NULL}};
const anchor_t ANCHORS_POLISH[] = {
{"a", "a"}, {"a", "ɑ"}, {"e", "ɛ"}, {"i", "i"}, {"o", "o"}, {"u", "u"}, {"y", "ɨ"}, {"p", "p"},
{"b", "b"}, {"t", "t"}, {"d", "d"}, {"k", "k"}, {"g", "g"}, {"m", "m"}, {"n", "n"}, {"ń", "ɲ"},
{"f", "f"}, {"w", "v"}, {"s", "s"}, {"z", "z"}, {"sz", "ʂ"}, {"ż", "ʐ"}, {"ś", "ɕ"}, {"ź", "ʑ"},
{"ch", "x"}, {"h", "h"}, {"l", "l"}, {"ł", "w"}, {"r", "r"}, {"j", "j"}, {NULL, NULL}};
const anchor_t ANCHORS_GERMAN[] = {
{"a", "a"}, {"a", "ɑ"}, {"e", "e"}, {"e", "ɛ"}, {"e", "ə"}, {"i", "i"}, {"i", "ɪ"}, {"o", "o"}, {"o", "ɔ"},
{"u", "u"}, {"u", "ʊ"}, {"ä", "ɛ"}, {"ö", "ø"}, {"ü", "y"}, {"p", "p"}, {"b", "b"}, {"t", "t"}, {"d", "d"},
{"k", "k"}, {"g", "g"}, {"m", "m"}, {"n", "n"}, {"ng", "ŋ"}, {"f", "f"}, {"w", "v"}, {"s", "z"}, {"ss", "s"},
{"sch", "ʃ"}, {"j", "j"}, {"r", "r"}, {"ch", "x"}, {"ch", "ç"}, {"ts", "z"}, {NULL, NULL}};
const anchor_t ANCHORS_CYRILLIC[] = {
{"а", "a"}, {"а", "ɑ"}, {"б", "b"}, {"в", "v"}, {"г", "g"}, {"д", "d"}, {"е", "ɛ"}, {"ж", "ʒ"}, {"з", "z"},
{"и", "i"}, {"й", "j"}, {"к", "k"}, {"л", "l"}, {"м", "m"}, {"н", "n"}, {"о", "o"}, {"п", "p"}, {"р", "r"},
{"с", "s"}, {"т", "t"}, {"у", "u"}, {"ф", "f"}, {"х", "x"}, {"ш", "ʃ"}, {"ы", "ɨ"}, {NULL, NULL}};
const anchor_t ANCHORS_ORCISH[] = {
{"u", "y"}, {"u", "u"}, {"u", "ʊ"}, {"o", "ø"}, {"o", "o"}, {"o", "ɔ"}, {"a", "a"}, {"a", "ɑ"}, {"a", "ʌ"},
{"e", "e"}, {"i", "i"}, {"b", "b"}, {"d", "d"}, {"g", "g"}, {"p", "p"}, {"t", "t"}, {"k", "k"}, {"kh", "x"},
{"kh", "χ"}, {"gh", "ɣ"}, {"gh", "ʁ"}, {"q", "q"}, {"k", "ɢ"}, {"h", "h"}, {"hh", "ɦ"}, {"kh", "ħ"}, {"m", "m"},
{"n", "n"}, {"ng", "ŋ"}, {"ny", "ɲ"}, {"l", "l"}, {"r", "r"}, {"rr", "ʀ"}, {"v", "v"}, {"w", "w"}, {"z", "z"},
{"zg", "ʒ"}, {"sh", "ʃ"}, {"th", "θ"}, {"dh", "ð"}, {"y", "j"}, {NULL, NULL}};
// --- GENERATOR LOGIC ---
// Helper to retrieve vector from DB
f6_t get_vector(const char* ipa) {
for (int i = 0; PHONEME_DB[i].ipa; i++) {
if (strcmp(PHONEME_DB[i].ipa, ipa) == 0) return PHONEME_DB[i].target;
}
fprintf(stderr, "ERROR: Anchor refers to unknown IPA: %s\n", ipa);
exit(1);
}
void generate_ipa_identity_table(const char* array_name) {
printf("const ortho_rule_t %s[] = {\n", array_name);
for (int i = 0; PHONEME_DB[i].ipa != NULL; i++) {
printf(" {\"%s\", \"%s\"},\n", PHONEME_DB[i].ipa, PHONEME_DB[i].ipa);
}
printf(" {NULL, NULL}\n};\n\n");
}
void generate_table(const char* array_name, const anchor_t* anchors) {
int anchor_count = 0;
while (anchors[anchor_count].spelling) anchor_count++;
f6_t* anchor_vecs = malloc(sizeof(f6_t) * anchor_count);
for (int j = 0; j < anchor_count; j++) {
anchor_vecs[j] = get_vector(anchors[j].ipa_ref);
}
printf("const ortho_rule_t %s[] = {\n", array_name);
for (int i = 0; PHONEME_DB[i].ipa != NULL; i++) {
f6_t source = PHONEME_DB[i].target;
f6_t weights = PHONEME_DB[i].weights;
float min_dist = FLT_MAX;
int best = 0;
for (int j = 0; j < anchor_count; j++) {
float d = f6_dist_sq(source, anchor_vecs[j], weights);
if (d < min_dist) {
min_dist = d;
best = j;
}
}
printf(" {\"%s\", \"%s\"},", PHONEME_DB[i].ipa, anchors[best].spelling);
printf("\n");
}
printf(" {NULL, NULL}\n};\n\n");
free(anchor_vecs);
}
int main(void) {
printf("// Automatically generated by mapgen\n");
printf("#ifndef GENERATED_ORTHO_H\n");
printf("#define GENERATED_ORTHO_H\n\n");
printf("#include \"transcriber.h\"\n\n");
generate_table("ORTHO_POLISH", ANCHORS_POLISH);
generate_table("ORTHO_HUNGARIAN", ANCHORS_HUNGARIAN);
generate_table("ORTHO_GERMAN", ANCHORS_GERMAN);
generate_table("ORTHO_CYRILLIC", ANCHORS_CYRILLIC);
generate_table("ORTHO_ORCISH", ANCHORS_ORCISH);
generate_ipa_identity_table("ORTHO_IPA");
printf("#endif\n");
return 0;
}