Skip to content

feat(export): export grani in JSON via --grain-json (#73)#93

Open
DMGiulioRomano wants to merge 2 commits into
mainfrom
claude/magical-sagan-NaMGq
Open

feat(export): export grani in JSON via --grain-json (#73)#93
DMGiulioRomano wants to merge 2 commits into
mainfrom
claude/magical-sagan-NaMGq

Conversation

@DMGiulioRomano

@DMGiulioRomano DMGiulioRomano commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Contesto

Chiude #73. PGE-ui (consumer tracciato in PGE-ui#13) deve disegnare i rettangoli dei singoli grani nella clip timeline di ogni stream. L'IR Grain era già calcolato in stream.voices ma mai persistito in forma strutturata (solo .sco Csound o buffer audio). Questa PR aggiunge un flag CLI --grain-json che esporta i grani in JSON.

Design

Nessuna interfaccia/ABC condivisa tra exporter (scelta deliberata). Reaper export e JSON export divergono troppo — input (aif_paths vs no), cardinalità output (1 file vs N), profondità dati (stream-level vs grain-level) — quindi un'astrazione comune ora sarebbe leaky/prematura (Rule of Three: solo 2 exporter). GrainJsonWriter nasce standalone e riusa l'unica convenzione utile del Reaper writer: split generate() (puro) / write() (I/O).

Modifiche

  • src/export/grain_json_writer.py (nuovo) — GrainJsonWriter con build()/generate()/write(). Itera stream.voices per preservare l'indice voce, ordina per t, JSON compatto.
  • tests/export/test_grain_json_writer.py (nuovo) — 19 test (TDD rosso→verde).
  • src/main.py — flag --grain-json, attivo solo con --per-stream. I file JSON sono scritti come sidecar accanto agli stem .aif (stessa directory dell'output STEMS), così PGE-ui trova grain JSON e audio nello stesso posto. Usage aggiornato.

Schema JSON

Un file per stream, accanto allo stem audio: {output_dir}/{basename}__{stream_id}__grains.json

{"stream_id":"s1","duration":8.0,"num_voices":2,"grains":[{"t":-0.5,"dur":0.05,"vol":-9.0,"ptr":0.1,"v":1}, ...]}
  • t = grain.onset - stream.onset (relativo allo stream; può essere < 0 con onset offset per-voce)
  • v = indice voce; dur/vol/ptr = duration/volume/pointer_pos
  • ptr ha unità variabile per stream (secondi o frazione)
  • grani ordinati per t, JSON compatto (no whitespace)
  • num_voices = len(stream.voices) (voci effettivamente generate)

Verifica

  • make tests: 4273 passed, 2 skipped — verde.
  • Output del writer verificato con il vero Grain (ordinamento, t negativo, mapping, write() == generate()).
  • Nota: e2e via CLI con render reale non eseguibile nell'ambiente (refs/ privo di sample audio; il blocco grain-json gira dopo il render). Logica coperta da test unitari e verifica diretta.

Impatto cross-repo

  • PGE-ls: nessuno (nessuna modifica a YAML/schema).
  • PGE-ui: già tracciato come PGE-ui#13 (consumer del formato JSON). Path output cambiato in sidecar accanto agli .aif — verificare che il consumer cerchi i JSON nella directory degli stem. Nessuna nuova issue (aggiornare PGE-ui#13 se necessario).

Changelog branch

  • 3b2c49f feat: implementazione iniziale --grain-json (output in cache_dir).
  • 254352c fix: grain JSON scritto accanto agli stem .aif invece che in cache_dir (sidecar coerente con l'audio a cui si riferiscono; cache_dir non aveva relazione semantica con l'export e poteva non esistere senza --cache).

https://claude.ai/code/session_019N8ZDHmri8tCNCu2WgGRzc


Generated by Claude Code

claude added 2 commits June 7, 2026 15:43
Aggiunge GrainJsonWriter che serializza i grani di ogni stream in JSON
per i client di visualizzazione (PGE-ui#13). Un file per stream:
{cache}/{basename}__{stream_id}__grains.json

- Schema: stream_id, duration, num_voices, grains[{t,dur,vol,ptr,v}]
- t relativo a stream.onset (puo' essere < 0 con onset offset per-voce)
- itera stream.voices per preservare l'indice voce
- JSON compatto, grani ordinati per t
- flag CLI --grain-json, attivo solo con --per-stream

https://claude.ai/code/session_015LPLSKXNZW6Qg67gyzVZcy
…ache_dir

I file grain JSON sono sidecar degli stem audio a cui si riferiscono:
PGE-ui li trova nella stessa directory degli .aif (output STEMS) invece
che in cache_dir, che non ha relazione semantica con l'export e poteva
non esistere senza --cache. Riusa il pattern os.path.dirname(abspath)
gia' usato per aif_dir nel garbage collect.

https://claude.ai/code/session_019N8ZDHmri8tCNCu2WgGRzc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants