Offline - Alle Schreibzugriffe von API-Nutzern laufen über eine Warteschlange in IndexedDB (services/syncQueue.js): FIFO, zusammengefasste Updates, Wiederholung bei Netz/Sichtbarkeit/Backoff, 4xx wird als Fehler gemeldet. - Medien-Uploads liegen mit Bytes in der Queue, erscheinen sofort mit Marker und werden nach dem Upload durch das Server-Objekt ersetzt. - PWA-Modus mit Service Worker für die App-Hülle; API nie gecacht. npm run build erzeugt dist/pwa. - Speicherzustand im Panel: ausstehend / Fehler mit Wiederholen-Knopf, Verbindungsanzeige im Header. Event - Event entsteht erst mit dem ersten Eintrag; jede Löschung fragt nach. - Key Image als 4:3-Rahmen, Galerie mit Video, mehrere Sprachnotizen mit Notiz, Live-Pegel beim Aufnehmen, WAV-Umwandlung für lokale Aufnahmen. - Neue Felder: Zeitraum, Kategorie, Personen, Meilenstein; Stimmung als kompakte Zeile mit ausklappbarer Punktfarbe. Oberfläche - Untere Leiste: Darstellung und Zoom links, Jahres-Pille mittig, Plus rechts. - Logos und Goldpalette, Login neu, zehn Wellen-Presets mit Vorschau, Hintergrundbilder als Kacheln, Kontomenü und Einstellungen bereinigt, Escape schließt Ebenen, größere Touch-Ziele. - Alten Sync-Service und ungenutzte Legacy-Seiten entfernt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
43 lines
1.9 KiB
JavaScript
43 lines
1.9 KiB
JavaScript
import { describe, expect, it } from 'vitest'
|
|
import { WAV_SAMPLE_RATE, encodeWav, needsWavForPlayback } from 'src/utils/audioWav'
|
|
|
|
function ascii(view, offset, length) {
|
|
return String.fromCharCode(...Array.from({ length }, (_, i) => view.getUint8(offset + i)))
|
|
}
|
|
|
|
describe('audioWav — WAV for recordings kept in the browser', () => {
|
|
it('writes a valid 16-bit mono RIFF header', () => {
|
|
const samples = new Float32Array([0, 0.5, -0.5, 1, -1])
|
|
const view = new DataView(encodeWav(samples, WAV_SAMPLE_RATE))
|
|
|
|
expect(ascii(view, 0, 4)).toBe('RIFF')
|
|
expect(ascii(view, 8, 4)).toBe('WAVE')
|
|
expect(ascii(view, 12, 4)).toBe('fmt ')
|
|
expect(view.getUint16(20, true)).toBe(1) // PCM
|
|
expect(view.getUint16(22, true)).toBe(1) // mono
|
|
expect(view.getUint32(24, true)).toBe(WAV_SAMPLE_RATE)
|
|
expect(view.getUint32(28, true)).toBe(WAV_SAMPLE_RATE * 2)
|
|
expect(view.getUint16(34, true)).toBe(16)
|
|
expect(ascii(view, 36, 4)).toBe('data')
|
|
expect(view.getUint32(40, true)).toBe(samples.length * 2)
|
|
expect(view.byteLength).toBe(44 + samples.length * 2)
|
|
expect(view.getUint32(4, true)).toBe(view.byteLength - 8)
|
|
})
|
|
|
|
it('scales and clamps samples to the 16-bit range', () => {
|
|
const view = new DataView(encodeWav(new Float32Array([0, 1, -1, 2, -2]), 8000))
|
|
expect(view.getInt16(44, true)).toBe(0)
|
|
expect(view.getInt16(46, true)).toBe(32767)
|
|
expect(view.getInt16(48, true)).toBe(-32768)
|
|
expect(view.getInt16(50, true)).toBe(32767)
|
|
expect(view.getInt16(52, true)).toBe(-32768)
|
|
})
|
|
|
|
it('converts only the containers Safari refuses', () => {
|
|
expect(needsWavForPlayback('audio/webm')).toBe(true)
|
|
expect(needsWavForPlayback('audio/ogg;codecs=opus')).toBe(true)
|
|
expect(needsWavForPlayback('audio/mp4')).toBe(false)
|
|
expect(needsWavForPlayback('audio/wav')).toBe(false)
|
|
expect(needsWavForPlayback('')).toBe(false)
|
|
})
|
|
})
|