From c913cacf8d5f45d01a3081fb09cf0132d0adc7c3 Mon Sep 17 00:00:00 2001 From: David Ali Date: Wed, 28 Jan 2026 22:20:52 +0100 Subject: [PATCH] first commit --- .gitignore | 19 ++++ LICENSE | 21 ++++ Makefile | 53 ++++++++++ README.md | 88 +++++++++++++++++ examples/example_babble.c | 107 ++++++++++++++++++++ examples/example_names.c | 178 +++++++++++++++++++++++++++++++++ src/articulator.c | 186 +++++++++++++++++++++++++++++++++++ src/articulator.h | 52 ++++++++++ src/articulator_db.c | 200 ++++++++++++++++++++++++++++++++++++++ src/transcriber.c | 76 +++++++++++++++ src/transcriber.h | 11 +++ src/visualizer.c | 61 ++++++++++++ tools/mapgen.c | 120 +++++++++++++++++++++++ 13 files changed, 1172 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 examples/example_babble.c create mode 100644 examples/example_names.c create mode 100644 src/articulator.c create mode 100644 src/articulator.h create mode 100644 src/articulator_db.c create mode 100644 src/transcriber.c create mode 100644 src/transcriber.h create mode 100644 src/visualizer.c create mode 100644 tools/mapgen.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00fd67b --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e2989f --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d7f1046 --- /dev/null +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1eff59 --- /dev/null +++ b/README.md @@ -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 diff --git a/examples/example_babble.c b/examples/example_babble.c new file mode 100644 index 0000000..b1c8a14 --- /dev/null +++ b/examples/example_babble.c @@ -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 +#include +#include +#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; +} \ No newline at end of file diff --git a/examples/example_names.c b/examples/example_names.c new file mode 100644 index 0000000..13c7af9 --- /dev/null +++ b/examples/example_names.c @@ -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 +#include +#include +#include +#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 + "ʃ", "ʒ", "tʃ", "dʒ", // sz, ż, cz, dż + "ɕ", "ʑ", "tɕ", "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; +} \ No newline at end of file diff --git a/src/articulator.c b/src/articulator.c new file mode 100644 index 0000000..3eab6bd --- /dev/null +++ b/src/articulator.c @@ -0,0 +1,186 @@ +#include "articulator.h" +#include +#include +#include +#include + +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"); +} \ No newline at end of file diff --git a/src/articulator.h b/src/articulator.h new file mode 100644 index 0000000..6b9db71 --- /dev/null +++ b/src/articulator.h @@ -0,0 +1,52 @@ +#ifndef ARTICULATOR_H +#define ARTICULATOR_H + +#include + +// 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 \ No newline at end of file diff --git a/src/articulator_db.c b/src/articulator_db.c new file mode 100644 index 0000000..67bfc6f --- /dev/null +++ b/src/articulator_db.c @@ -0,0 +1,200 @@ +#include "articulator.h" +#include +#include + +// --- 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; +} \ No newline at end of file diff --git a/src/transcriber.c b/src/transcriber.c new file mode 100644 index 0000000..3c5728b --- /dev/null +++ b/src/transcriber.c @@ -0,0 +1,76 @@ +#include "transcriber.h" +#include +#include + +#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); + } + } +} \ No newline at end of file diff --git a/src/transcriber.h b/src/transcriber.h new file mode 100644 index 0000000..81e35ee --- /dev/null +++ b/src/transcriber.h @@ -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 \ No newline at end of file diff --git a/src/visualizer.c b/src/visualizer.c new file mode 100644 index 0000000..f6aaa57 --- /dev/null +++ b/src/visualizer.c @@ -0,0 +1,61 @@ +#include +#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); +} \ No newline at end of file diff --git a/tools/mapgen.c b/tools/mapgen.c new file mode 100644 index 0000000..e03b363 --- /dev/null +++ b/tools/mapgen.c @@ -0,0 +1,120 @@ +/* tools/mapgen.c */ +#include +#include +#include +#include + +// 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; +} \ No newline at end of file