Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbbe69d928 | |||
| 23c39d5bba | |||
| 5328dc8595 | |||
| 0c03b4f161 |
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 405
|
||||
versionName "0.0.4.5"
|
||||
versionCode 407
|
||||
versionName "0.0.4.7"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
||||
// Sekunden Audio die VOR play()-Start gepuffert sein muessen.
|
||||
// 2.5s Vorrat = genug um XTTS-Render-Pausen zwischen Chunks zu puffern.
|
||||
private const val PREROLL_SECONDS = 2.5
|
||||
// Stille am Stream-Anfang, damit AudioTrack sauber anfaehrt und die
|
||||
// ersten Samples nicht abgeschnitten werden (XTTS-Warmup + play()-Latenz).
|
||||
private const val LEADING_SILENCE_SECONDS = 0.2
|
||||
}
|
||||
|
||||
override fun getName() = "PcmStreamPlayer"
|
||||
@@ -45,6 +48,7 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
||||
@Volatile private var prerollBytes: Int = 0
|
||||
@Volatile private var playbackStarted = false
|
||||
@Volatile private var bytesBuffered: Long = 0
|
||||
@Volatile private var streamBytesPerFrame: Int = 2 // mono s16le default
|
||||
|
||||
// ── Lifecycle ──
|
||||
|
||||
@@ -64,6 +68,7 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
||||
prerollBytes = prerollTarget
|
||||
bytesBuffered = 0
|
||||
playbackStarted = false
|
||||
streamBytesPerFrame = channels * 2 // s16 = 2 bytes per sample
|
||||
|
||||
val newTrack = AudioTrack.Builder()
|
||||
.setAudioAttributes(
|
||||
@@ -92,6 +97,18 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
||||
writerThread = Thread({
|
||||
val t = track ?: return@Thread
|
||||
try {
|
||||
// Leading-Silence in den Buffer — gibt AudioTrack Zeit anzufahren.
|
||||
val silenceBytes = ((sampleRate * channels * 2) * LEADING_SILENCE_SECONDS).toInt() and 0x7FFFFFFE
|
||||
if (silenceBytes > 0) {
|
||||
val silence = ByteArray(silenceBytes)
|
||||
var silOff = 0
|
||||
while (silOff < silence.size && !writerShouldStop) {
|
||||
val w = t.write(silence, silOff, silence.size - silOff)
|
||||
if (w <= 0) break
|
||||
silOff += w
|
||||
}
|
||||
bytesBuffered += silence.size
|
||||
}
|
||||
while (!writerShouldStop) {
|
||||
val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS) ?: run {
|
||||
if (endRequested) {
|
||||
@@ -127,6 +144,30 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Writer-Thread Fehler: ${e.message}")
|
||||
} finally {
|
||||
// Warten bis alle geschriebenen Samples tatsaechlich abgespielt sind,
|
||||
// sonst cuttet t.release() die letzten Sekunden ab.
|
||||
try {
|
||||
val totalFrames = (bytesBuffered / streamBytesPerFrame).toInt()
|
||||
var lastPos = -1
|
||||
var stalledCount = 0
|
||||
while (!writerShouldStop) {
|
||||
val pos = t.playbackHeadPosition
|
||||
if (pos >= totalFrames) break
|
||||
// Safety: wenn Position 2s nicht mehr vorwaerts → AudioTrack hing
|
||||
if (pos == lastPos) {
|
||||
stalledCount++
|
||||
if (stalledCount > 40) {
|
||||
Log.w(TAG, "playback stalled at $pos/$totalFrames — give up")
|
||||
break
|
||||
}
|
||||
} else {
|
||||
stalledCount = 0
|
||||
lastPos = pos
|
||||
}
|
||||
Thread.sleep(50)
|
||||
}
|
||||
Log.i(TAG, "Playback fertig: frames=$totalFrames pos=${t.playbackHeadPosition}")
|
||||
} catch (_: Exception) {}
|
||||
try { t.stop() } catch (_: Exception) {}
|
||||
try { t.release() } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.0.4.5",
|
||||
"version": "0.0.4.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -150,6 +150,15 @@ def _small_range_to_words(m):
|
||||
return f"{_num_to_words_de(a)} bis {_num_to_words_de(b)}"
|
||||
|
||||
|
||||
def _decimal_to_words(m):
|
||||
"""'0.1' / '0,1' → 'null komma eins', '1,25' → 'eins komma zwei fuenf'."""
|
||||
int_part = int(m.group(1))
|
||||
dec_part = m.group(2)
|
||||
int_word = _num_to_words_de(int_part) if 0 <= int_part <= 59 else str(int_part)
|
||||
dec_words = " ".join(_num_to_words_de(int(d)) for d in dec_part)
|
||||
return f"{int_word} komma {dec_words}"
|
||||
|
||||
|
||||
_UNIT_WORDS = [
|
||||
(r'\bTB\b', 'Terabyte'),
|
||||
(r'\bGB\b', 'Gigabyte'),
|
||||
@@ -236,6 +245,11 @@ def clean_text_for_tts(text: str) -> str:
|
||||
# Kleine Zahlen-Bereiche ohne "Uhr": "5-6" → "fuenf bis sechs"
|
||||
t = _re_tts.sub(r'\b(\d{1,2})\s*[-–]\s*(\d{1,2})\b', _small_range_to_words, t)
|
||||
|
||||
# Dezimalzahlen: "0.1" / "0,5" / "1,25" → "null komma eins" / "null komma fuenf" / ...
|
||||
# Muss vor "Zahl+Einheit" laufen, sonst frisst die Unit-Regel den Nachkommaanteil.
|
||||
# Lookahead verhindert Match auf IP-artigen Strings wie 192.168.1.1.
|
||||
t = _re_tts.sub(r'\b(\d+)[.,](\d+)(?![.,\d])', _decimal_to_words, t)
|
||||
|
||||
# Zahlen + Einheit: "22GB" → "22 Gigabyte" (Leerzeichen einfuegen)
|
||||
t = _re_tts.sub(r'(\d+)([A-Za-z]{1,4})\b', r'\1 \2', t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user