diff --git a/apps/rebreak-native/app/lyra.tsx b/apps/rebreak-native/app/lyra.tsx
index 56d3e57..7d99c8f 100644
--- a/apps/rebreak-native/app/lyra.tsx
+++ b/apps/rebreak-native/app/lyra.tsx
@@ -94,28 +94,42 @@ function ThinkingDots() {
// ── Voice bars ────────────────────────────────────────────────────────────────
-function VoiceBars({ count, baseColor }: { count: number; baseColor: string }) {
- const anims = useRef(Array.from({ length: count }, () => new Animated.Value(4))).current;
+function VoiceBars({ count, baseColor, active }: { count: number; baseColor: string; active: boolean }) {
+ const anims = useRef(Array.from({ length: count }, () => new Animated.Value(3))).current;
+ const runningRef = useRef(false);
useEffect(() => {
- const animations = anims.map((a, i) =>
- Animated.loop(
- Animated.sequence([
- Animated.timing(a, { toValue: 4 + Math.random() * 14, duration: 450 + (i % 5) * 80, useNativeDriver: false }),
- Animated.timing(a, { toValue: 4, duration: 450 + (i % 5) * 80, useNativeDriver: false }),
- ])
- )
- );
- animations.forEach((a) => a.start());
- return () => animations.forEach((a) => a.stop());
- }, []);
+ if (active && !runningRef.current) {
+ runningRef.current = true;
+ const animations = anims.map((a, i) =>
+ Animated.loop(
+ Animated.sequence([
+ Animated.timing(a, { toValue: 3 + Math.random() * 14, duration: 400 + (i % 5) * 90, useNativeDriver: false }),
+ Animated.timing(a, { toValue: 3, duration: 400 + (i % 5) * 90, useNativeDriver: false }),
+ ])
+ )
+ );
+ animations.forEach((a) => a.start());
+ return () => { animations.forEach((a) => a.stop()); runningRef.current = false; };
+ } else if (!active) {
+ // Stille: alle Bars auf minimale Höhe zurück
+ anims.forEach((a) => Animated.timing(a, { toValue: 3, duration: 150, useNativeDriver: false }).start());
+ runningRef.current = false;
+ }
+ }, [active]);
return (
-
+
{anims.map((a, i) => (
))}
@@ -195,6 +209,9 @@ export default function CoachScreen() {
const soundRef = useRef(null);
const micHeld = useRef(false);
const recordingTimer = useRef | null>(null);
+ const recordingStartTime = useRef(0);
+ const [audioLevel, setAudioLevel] = useState(0); // 0–1, live metering
+ const [trashFlash, setTrashFlash] = useState(false); // kurze rote Animation beim Cancel
const typingTimer = useRef | null>(null);
const emotionTimer = useRef | null>(null);
const isNearBottomRef = useRef(true);
@@ -426,12 +443,26 @@ export default function CoachScreen() {
function startRecordingTimer() {
setRecordingDuration(0);
- recordingTimer.current = setInterval(() => setRecordingDuration((d) => d + 1), 1000);
+ setAudioLevel(0);
+ recordingStartTime.current = Date.now();
+ recordingTimer.current = setInterval(async () => {
+ setRecordingDuration(Math.floor((Date.now() - recordingStartTime.current) / 1000));
+ // Metering: Audio-Pegel für Stille/Sprechen-Unterscheidung
+ try {
+ const status = await recordingRef.current?.getStatusAsync();
+ if (status?.isRecording && status.metering !== undefined) {
+ // metering ist in dBFS (-160 bis 0). Normalisieren auf 0–1.
+ const normalized = Math.max(0, Math.min(1, (status.metering + 60) / 60));
+ setAudioLevel(normalized);
+ }
+ } catch { /* kein Metering auf diesem Gerät */ }
+ }, 200);
}
function stopRecordingTimer() {
if (recordingTimer.current) clearInterval(recordingTimer.current);
recordingTimer.current = null;
setRecordingDuration(0);
+ setAudioLevel(0);
}
async function onMicDown() {
@@ -449,7 +480,10 @@ export default function CoachScreen() {
try {
await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true });
const rec = new Audio.Recording();
- await rec.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
+ await rec.prepareToRecordAsync({
+ ...Audio.RecordingOptionsPresets.HIGH_QUALITY,
+ isMeteringEnabled: true,
+ });
await rec.startAsync();
recordingRef.current = rec;
setIsRecording(true);
@@ -463,6 +497,9 @@ export default function CoachScreen() {
async function cancelRecording() {
if (!isRecording) return;
+ // Kurze rote Flash-Animation bevor der State verschwindet
+ setTrashFlash(true);
+ setTimeout(() => setTrashFlash(false), 400);
micHeld.current = false;
stopRecordingTimer();
setIsRecording(false);
@@ -590,7 +627,7 @@ export default function CoachScreen() {
{isSpeaking && (
-
+
{t('coach.speaking')}
@@ -654,21 +691,26 @@ export default function CoachScreen() {
{isRecording ? (
/* ── Instagram-style Recording Bar ─────────────────────── */
- {/* Trash - links */}
+ {/* Trash - links, kurz rot bei Klick */}
-
+
{/* Waveform + Timer - mitte */}
-
-
-
+ 0.1} />
{formatDuration(recordingDuration)}