Skip to content

Releases: DMGiulioRomano/PythonGranularEngine

v4.0.0 — "Unit-Driven Pitch"

06 Jun 17:39

Choose a tag to compare

Aggiunto

  • Sistema pitch unit-driven (PitchUnit): il blocco pitch (base e
    voices.pitch) accetta sei unità di misura — semitones, cents,
    quarter_tone, eighth_tone, edo (EDO arbitrario: edo: N + value: X
    su base, unit: {edo: N} nelle voci) e ratio — con un'unica interfaccia di
    conversione a ratio. Famiglia EDO:
    2^(valore / N); ratio è moltiplicatore diretto. Default invariato
    (semitones, valore neutro → ratio 1.0). EdoUnit/RatioUnit e factory
    make_pitch_unit in src/parameters/pitch_unit.py; strategy unica
    UnitPitchStrategy. PR #84.

  • Validazione strict del blocco pitch: una chiave sconosciuta — incluso un
    refuso sull'unità (es. semitone: invece di semitones:) — solleva
    InvalidFieldValueError che elenca le chiavi valide, invece di essere
    ignorata silenziosamente con default a semitoni neutri (No Silent Failures).
    Chiavi valide: le 6 unità più range e value (value solo con edo: N).
    Inoltre un blocco pitch presente ma vuoto (pitch:None) o
    non-mapping (lista/scalare, es. pitch: [[0, -1200], [1, 1200]]) solleva
    InvalidFieldValueError con hint, invece del precedente TypeError grezzo da
    PitchController._select_unit: per nessuna trasposizione si omette del tutto
    il blocco. pitch: {} e blocco assente restano default semitoni neutro
    (indistinguibili a valle: Stream passa {} in entrambi i casi). Migrati i
    config in-repo PGE_pino2.yml (rimosso il pitch: vuoto) e
    PGE_envelope_syntax_test.yml (envelope di pitch esplicitato come cents,
    che è l'unità reale dei valori ±1200). PR #84.

  • Flag normalized nel blocco voices.pointer (YAML): opt-in per interpretare
    l'offset di pointer di voce come frazione di sample_dur_sec anziché in
    secondi. Default invariato (normalized: false → secondi), nessun breaking
    change sugli YAML esistenti. Vale per le strategie linear e stochastic;
    lo scaling avviene in Stream._create_grain, le strategy restano pure.
    Il flag accetta solo true/false: un valore non-bool solleva
    InvalidFieldValueError (nessuna coercion silenziosa, coerente con
    grain.reverse). Risolve l'ambiguità di unità documentata in issue #80.

  • Flag --format aiff|wav|flac in src/main.py e variabile FORMAT nel
    Makefile: seleziona il formato audio di output (default aiff). Il formato
    viene propagato a NamingStrategy (estensione file), NumpyAudioRenderer
    (parametri sf.write), StreamCacheManager (fingerprint cache e
    garbage collection). Csound non richiede modifiche: rileva il formato
    dall'estensione del flag -o. Aggiunto AudioFormat dataclass in
    src/rendering/audio_format.py. Risolve issue #75.

  • Target make clean-rpp (make/clean.mk): rimuove i file .rpp e .rpp-bak
    in $(SFDIR) (default output/) e nella root del repo. Risolve la
    pulizia esplicita dei progetti Reaper, prima orfana di target dedicato.
    Issue #65.

  • Flag CLEAN_RPP nel Makefile (default false): controlla se make clean
    rimuove anche i .rpp in output/. Default false per preservare
    eventuale lavoro REAPER manuale (FX chain, automation, mixer routing) che
    non è rigenerabile da YAML. CLEAN_RPP=true ripristina il comportamento
    pre-issue#65 (wipe totale $(SFDIR)/*). Issue #65.

  • Flag REAPER_REUSE_TAB nel Makefile (default false): se true con
    REAPER=true, prima di aprire il .rpp aggiornato lo script Lua
    generated/open_reaper_tab.lua scorre le tab REAPER aperte (EnumProjects)
    e chiude solo quella con path assoluto matching (action 40860 "Close
    current project tab"), poi apre nuova tab (action 40859). Le altre tab
    restano intatte. Alternativa meno distruttiva ad AUTOKILL_REAPER per
    rebuild ripetuti dello stesso YAML. Risolve issue #59.

  • Refactor make/build.mk: estratta macro emit_open_reaper_lua condivisa
    da autopen_stems e autopen_single per centralizzare la generazione
    dello ReaScript Lua (branch condizionale su REAPER_REUSE_TAB).

  • Supporto Fedora / RHEL / Rocky / AlmaLinux nel branch dnf di
    make install-system-deps (issue #58). Installa python3 + sox;
    stampa istruzioni per Csound (non disponibile nei repo Fedora / RPM
    Fusion — usare RENDERER=numpy o compilare dai sorgenti).

  • README: sezione dedicata "Fedora / RHEL / Rocky / AlmaLinux" con
    istruzioni install e nota Csound; righe Fedora/RHEL nella tabella
    compatibilità Python; voce Fedora/RHEL nella tabella "Platform Support".

  • Flag AUTOKILL_REAPER nel Makefile (default false): se true con
    REAPER=true, chiude REAPER prima del build via SIGKILL
    (pkill -9 -x REAPER macOS / pkill -9 -x reaper Linux), poi il .rpp
    viene riscritto e REAPER riaperto. Kill immediato senza dialog di
    salvataggio (modifiche manuali non salvate vengono perse — scelta
    intenzionale per garantire automazione non bloccante). Risolve issue #17
    REAPER non ricarica da disco le modifiche a onset / duration se il
    progetto e' gia' aperto.

  • Target make reaper-stop: chiude REAPER se attivo (specchio di rx-stop).

  • Multi-tab REAPER per YAML: se REAPER e' gia' in esecuzione, l'apertura del
    .rpp post-build avviene via ReaScript Lua generato al volo in
    generated/open_reaper_tab.lua (action 40859 "New project tab" +
    Main_openProject), invocato con REAPER -nonewinst <script.lua>.
    Build dello stesso YAML produce nuova tab con dati aggiornati; build di
    YAML diverso produce tab indipendente. Comportamento deterministico, non
    dipende da preferenze utente REAPER. Richiede REAPER >= 6.80.

  • docs/reaper-workflow.md: workflow REAPER, requisiti, troubleshooting.

  • tests/e2e/test_reaper_makefile_e2e.py: 6 scenari su target reaper-stop,
    wiring AUTOKILL_REAPER, default REAPER_PATH.

Modificato

  • Pitch delle voci unit-agnostico: la geometria della distribuzione vive
    ora nella PitchUnit via il nuovo metodo materialize(position, amount)
    (EDO additiva 2^(position·amount/N), ratio geometrica amount^position).
    Le voice pitch strategy emettono un fattore di ratio (get_pitch_factor,
    prima get_pitch_offset in semitoni); VoiceConfig.pitch_offset
    pitch_factor (default 1.0 = identità) e Stream._create_grain moltiplica
    direttamente, senza il guard != 0.0. Conseguenze su voices.pitch con
    unit: ratio: range e stochastic diventano validi (distribuzione
    geometrica, nessun ratio negativo o sub-zero); step passa da i·step
    (lineare) a step^i (geometrico) — breaking sui valori delle voci ≥2 con
    unit: ratio. I path EDO (semitones/cents/quarter_tone/eighth_tone/edo)
    restano numericamente identici. chord/spectral restano semitone-locked.
  • Default REAPER_PATH: da $(FILE).rpp (root del repo) a $(SFDIR)/$(FILE).rpp
    (default output/$(FILE).rpp). I progetti Reaper vivono ora accanto agli
    .aif generati, co-location semantica tra progetto Reaper e audio referenziati.
    Breaking change minore: script che cercano foo.rpp nella root vanno
    aggiornati a output/foo.rpp. REAPER_PATH=custom/path.rpp resta supportato
    per override esplicito. Issue #65.
  • make clean non rimuove più $(SFDIR)/* con rm -rf per default. Usa find
    con esclusione di *.rpp per preservare progetti Reaper. Override via
    CLEAN_RPP=true. Issue #65.

Modificato (breaking)

  • Chiave YAML voices.pitch.semitone_range rinominata in pitch_range (strategie
    range e stochastic). Il valore è interpretato nell'unità attiva
    (semitones/cents/edo/ratio), non in semitoni: il vecchio nome mentiva.
    pitch_range è domain-based, coerente con i sibling pointer_range/max_offset.
    Hard break: la vecchia chiave semitone_range solleva
    InvalidStrategyConfigError con hint di migrazione (guard in
    Stream._init_voice_manager). Migrati i config in-repo.
  • Default REAPER_PATH: era Project.rpp fisso, ora $(FILE).rpp. Ogni YAML
    produce un .rpp con lo stesso basename, abilitando il multi-tab. Override
    esplicito via REAPER_PATH=... sempre supportato. Aggiornato help
    make help di conseguenza.

Corretto

  • Stream._create_grain (src/core/stream.py): l'offset di voce sul pointer
    veniva sommato dopo il wrap base, lasciando grain.pointer_pos oltre
    sample_dur per le voci con offset positivo. Ora la somma è re-wrappata
    in [0, sample_dur) con % self.sample_dur_sec. L'audio era già corretto
    (GrainRenderer e Csound ri-wrappano la traiettoria di lettura), ma la
    partitura (ScoreVisualizer) clippava le voci sopra il bordo del buffer,
    facendole "ricomparire" tutte insieme al wrap della voce 0 invece che
    sfasate. Ora grain.pointer_pos è la posizione reale di lettura, condivisa
    da audio e partitura. Risolve issue #79.

  • Docstring delle voice strategy (voice_pointer_strategy.py,
    voice_onset_strategy.py, voice_pitch_strategy.py, voice_pan_strategy.py):
    rimosso il claim falso «seed deterministico / riproducibile tra sessioni».
    hash() su stringa è randomizzato per-processo (PYTHONHASHSEED non fissato),
    quindi l'offset per voce è stabile solo entro un run, non fra processi. Le
    docstring ora descrivono accuratamente il comportamento. Corretta anche la
    frase del README sui due renderer: stesso comportamento musicale, non output
    bit-identico (sequenze random indipendenti per i grani stocastici). Solo
    documentazione, nessuna modifica al comportamento. Risolve issue #76.

  • Macro autopen_stems in make/build.mk: il glob *.aif hardcoded è stato
    sostituito con *$(FORMAT_EXT), così con FORMAT=wav o FORMAT=flac il
    comando AUTOPEN=true apre i file con l'estensione corretta invece di non
    trovare nulla. Nessuna regressione: FORMAT_EXT defaults a .aif. Risolve
    issue #77.

  • Naming dei file stem .aif in STEMS mode: separatore tra basename del
    progetto e stream_id cambiato da _ a __ (issue #56), per
    allinearsi al protocollo del se...

Read more

v3.9.0 — "Reaper Polish"

22 May 09:14
ab3e551

Choose a tag to compare

Highlights

Focus su workflow REAPER e installazione multi-distro.

Feat

  • .rpp in output/ (#65, #66): default REAPER_PATH = $(SFDIR)/$(FILE).rpp, co-location con .aif. Nuovo target make clean-rpp + flag CLEAN_RPP (default false) per preservare lavoro REAPER manuale da make clean.
  • REAPER_REUSE_TAB (#59, #62): reload single-tab via ReaScript Lua + action 40860/40859. Alternativa non distruttiva ad AUTOKILL_REAPER.
  • Supporto Fedora/RHEL (#58, #60): branch dnf in make install-system-deps.

Fix

  • REAPER autokill multi-tab (#17, #61): SIGKILL + ReaScript per ricaricare .rpp quando REAPER aperto.
  • Stem naming (#56, #57): separator ___ per evitare collisioni.

Docs

  • INDEX Obsidian + wikilinks tra documenti (#63).

Breaking change minore

REAPER_PATH default: $(FILE).rpp (root) → $(SFDIR)/$(FILE).rpp. Script che cercano foo.rpp in root vanno aggiornati a output/foo.rpp. Override esplicito REAPER_PATH=... invariato.

Full changelog

v3.8.0...v3.9.0

v3.8.0 — Arch/Manjaro compat + Cartridge removal

12 May 18:07
459a878

Choose a tag to compare

Highlights

Arch / Manjaro compat (issue #51)

make setup ora funziona su Arch/Manjaro senza pyenv. Il Makefile rileva automaticamente python3.12..python3.16 versionati oppure python3 generico (se >= 3.12).

sudo pacman -Sy python sox csound
make setup   # ora completa anche con Python 3.14 di sistema

Cartridge removal (#40, breaking ma silent)

Rimossa la classe Cartridge (tape recorder head) e tutte le dipendenze. Feature non utilizzata da nessun YAML in configs/. La chiave cartridges: in YAML viene ignorata silenziosamente — zero impatto sui brani esistenti.

Changes

  • fix(make): detection Python multi-versione (numpy/scipy/soundfile cp314 verificate)
  • refactor: rimozione Cartridge + test correlati
  • docs: tabella compatibilità OS, brief UI editor
  • tests: nuovo test_makefile_python_detection.py (5 scenari)

Test

  • 4081 unit pass
  • 51 e2e pass (csound)

Full changelog: CHANGELOG.md

v3.7.0 - EngineError extension: controllers + envelopes

09 May 23:07
b714782

Choose a tag to compare

Issue #46 chiusa (follow-up #38)

Convertiti gli ultimi 11 raise user-facing residui nei moduli controllers/ e envelopes/ alle sotto-classi EngineError esistenti, completando l'unificazione della Categoria A (config errors).

PR incluse

  • #47 - controllers (window strategies, registry, pitch/density)
  • #48 - envelopes (segment, time_distribution)

Modificato

Controllers:

  • _validate_curve_range -> InvalidStrategyConfigError(strategy_kind="window")
  • MultiStateWindowStrategy.__init__ -> InvalidStrategyConfigError(strategy_kind="window_multistate")
  • WindowStrategyFactory.create -> StrategyNotFoundError (era KeyError)
  • WindowRegistry.generate_ftable_statement -> InvalidWindowError
  • pitch_controller / density_controller violazione gruppo esclusivo -> InvalidFieldValueError

Envelopes:

  • Segment.__init__ empty breakpoints -> InvalidFieldValueError
  • _validate_inputs (n_reps, total_time) + ExponentialDistribution (rate) -> ParameterBoundError

Compatibilita'

Tutte le nuove sotto-classi ereditano ValueError -> pytest.raises(ValueError) e except ValueError pre-esistenti continuano a funzionare. Unica eccezione: WindowStrategyFactory.create cambia base da KeyError a StrategyNotFoundError(ValueError) (verificato no caller).

Test

  • 4172 unit tests passing
  • 51 e2e tests passing

Per dettagli vedi CHANGELOG.

v3.6.0 — EngineError hierarchy & user-facing errors

09 May 21:43
e8611ec

Choose a tag to compare

Issue #38 chiusa: estensione completa della gerarchia EngineError con messaggi user-facing strutturati per tutti gli errori di configurazione e rendering.

Highlights

  • Gerarchia EngineError completa (ConfigError + EngineRuntimeError sub-tree)
  • user_message() contract: head [ERRORE] ... + context indentato + Stream/Config + path engine log
  • Pattern context enrichment layered: stream_id arricchito al chiamante (parser/strategy/controller), config_file in Generator.create_elements, handler unico polimorfico in main.py
  • Backward-compat preservata: ConfigError → ValueError, CsoundRenderError → RuntimeError
  • Documentazione: nuovo docs/error-handling.md

Sotto-classi aggiunte (issue #38)

PR Classi
PR1 (#39) ConfigError, MissingFieldError, InvalidFieldValueError
PR2 (#41) InvalidParameterError, ParameterBoundError
PR3 (#42) StrategyNotFoundError, InvalidStrategyConfigError
PR4 (#43) EngineRuntimeError, InvalidRendererError, InvalidWindowError, FtableError, CsoundRenderError
PR5 (#44) docs/error-handling.md

Test

  • 4161 unit tests passing
  • 49 e2e tests passing (tutti gli errori coperti via subprocess su YAML inline)

Riferimenti

v3.5.0 — Strategy passThrough

09 May 12:56
91bf5ed

Choose a tag to compare

Strategy passThrough

GrainClipStrategy come unica fonte di verità su quali grain esistono; numpy_audio_renderer passthrough puro senza opinioni proprie sui bounds.

Highlights

  • Nuovi campi YAML: clip_strategy (overflow_margin default | passthrough), clip_margin (float, default 0.0)
  • Strategy registrate: OverflowMarginClipStrategy, PassthroughClipStrategy, registry + factory
  • Filtro grain in post-process dentro Stream.generate_grains. Csound e NumPy ricevono ora la stessa stream.voices → parità garantita strutturalmente
  • numpy_audio_renderer: buffer dimensionato sull'extent reale dei grain; rimossi clamp end_sample > n_total e onset_sample >= n_total; preservato clamp onset < 0
  • Doc YAML aggiornata in docs/yaml-reference.md

Bugfix

  • Risolve #27 (divergenza renderer su grain con onset > stream.duration)
  • make: rileva package manager Linux a runtime (apt vs pacman) (#32)

Nota di compatibilità

Comportamento default più restrittivo per la coda: grain con grain.onset + grain.duration > stream_end vengono esclusi. Per ripristinare l'inclusione integrale dei grain (vecchio comportamento Csound), aggiungi al blocco stream:

clip_strategy: passthrough

In modalità passthrough il file .aif può superare stream.duration se i grain sforano.

Test

  • 4076 unit test passing
  • 39 e2e test passing

PR

  • #34 — Plan 001: modello (GrainClipStrategy)
  • #35 — Plan 002: renderer (passthrough puro)

v3.4.0 — "Temporal Voice"

28 Apr 09:53
2be75c9

Choose a tag to compare

Nuove funzionalità

Dynamic strategy parameters — ogni parametro delle voice strategy accetta ora float o Envelope. Il valore viene valutato al tempo reale di ogni grain, consentendo evoluzione temporale su tutte le dimensioni del sistema multi-voice.

Cosa cambia

  • resolve_param(param, time) — primitiva condivisa in parameters/parameter.py; risolve Union[float, Envelope] a float
  • Tutte le strategy ABC ricevono time: float; implementazioni stochastiche separano direzione (cache fissa, seeded) da magnitudine (time-varying)
  • VoiceManager stateless: get_voice_config(voice_index, time) calcola on-the-fly per ogni grain
  • Parsing YAML: _parse_strategy_kwarg rileva list/dict → costruisce Envelope; supporta time_mode: normalized
  • generate_grains passa voice_cursors[voice_index] — ogni voce valuta l'envelope al proprio tempo musicale reale

Parametri time-varying

Strategy Parametri
step pitch step
range pitch semitone_range
stochastic pitch semitone_range
linear onset step
geometric onset step, base
stochastic onset max_offset
linear pointer step
stochastic pointer pointer_range
tutte le pan spread (via VoiceManager)

Backward compatibility

Tutti i config YAML scalari esistenti rimangono validi senza modifiche.

File allegati

PGE_dynamic_strategy_params_test.yml — config di test empirico con 19 stream da 10s (~3.75 min). Copre ogni dimensione time-varying in isolamento e in combinazione. Da usare per verifica ascolto della feature.

v3.3.0 — Jazz Chords & Chord Inversions

13 Apr 22:55
12bdd59

Choose a tag to compare

Novità

Jazz Chords (5–7 voci)

11 nuovi accordi aggiunti a CHORD_INTERVALS:

  • 5 voci: dom9, maj9, min9, 9sus4
  • 6 voci: dom9s11, maj9s11, min11
  • 7 voci: dom13, min13, maj13s11, altered
voices:
  num_voices: 7
  pitch:
    strategy: chord
    chord: dom13

Inversioni accordo

ChordPitchStrategy ora accetta inversion: int = 0. Ruota gli intervalli in modo che il grado k diventi la voce più bassa, normalizzata a 0.

voices:
  num_voices: 4
  pitch:
    strategy: chord
    chord: dom7
    inversion: 1   # [0,3,6,8] invece di [0,4,7,10]

WindowStrategyFactory (refactoring interno)

select_window() è ora pura delegazione. Nuove strategy si aggiungono via registry senza toccare WindowController.

Test

3974 test, tutti verdi.

v3.2.0 — Window Transitions

13 Apr 21:03
271de68

Choose a tag to compare

Novità

Transizioni probabilistiche tra finestre di grano

Due nuove modalità YAML per l'envelope del grano:

Transition — morphing da una finestra a un'altra guidato da una curva temporale:

grain:
  envelope:
    from: hanning
    to: expodec
    curve: [[0, 0], [30, 1]]

Multi-state — transizione attraverso N finestre con separazione tra spazio del valore e spazio del tempo:

grain:
  envelope:
    states:
      - [0.0, hanning]
      - [0.3, bartlett]
      - [0.7, expodec]
      - [1.0, gaussian]
    curve: [[0, 0], [60, 1]]

La selezione per ogni grano è stocastica — il timbro dell'involucro evolve in modo probabilistico, non a step.

WindowStrategyFactory

Architettura OCP allineata al pattern voice_pitch_strategy: registry + **kwargs, estendibile senza toccare WindowController.

Finestra gaussian nel renderer NumPy

gaussian era già disponibile nel path Csound, ora è supportata anche dal renderer NumPy.

Breaking changes

  • envelope_range rimosso dal YAML (era ridondante — la variazione è implicita dalla struttura lista/stringa)

Fix

  • Errore leggibile quando sample è mancante o null in uno stream

v3.1.0

08 Apr 12:20
98bed64

Choose a tag to compare

What's Changed

Features

  • PointerController: quando loop_start è definito ma start non è esplicito nello YAML, il pointer parte direttamente da loop_start(t=0) invece che da 0. Il valore start esplicito non viene mai sovrascritto.

Fixes

  • Loop bounds relativi al file audio: loop_dur, loop_start, loop_end non hanno più un upper bound statico arbitrario nel registry. max_val=None indica assenza di limite statico — il bound reale è sempre sample_dur_sec, passato dinamicamente. Eliminati i fallback 1000.0 / 100.0 che non rispecchiavano la realtà.

Test suite

3802 test, 0 falliti.