first commit
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
53
Makefile
Normal 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
88
README.md
Normal 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
107
examples/example_babble.c
Normal 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
178
examples/example_names.c
Normal 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
|
||||
"ʃ", "ʒ", "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;
|
||||
}
|
||||
186
src/articulator.c
Normal file
186
src/articulator.c
Normal 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
52
src/articulator.h
Normal 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
200
src/articulator_db.c
Normal 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
76
src/transcriber.c
Normal 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
11
src/transcriber.h
Normal 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
61
src/visualizer.c
Normal 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
120
tools/mapgen.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user