framerudiments 0.4.4__tar.gz → 0.4.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {framerudiments-0.4.4/src/framerudiments.egg-info → framerudiments-0.4.5}/PKG-INFO +3 -1
- {framerudiments-0.4.4 → framerudiments-0.4.5}/README.md +2 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/RELEASE_NOTES.md +16 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/project-format.md +4 -1
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/rich-lyric-effects.md +9 -1
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/visualiser-routing.md +8 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/visualisers.md +27 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/pyproject.toml +1 -1
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/__init__.py +1 -1
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/compositor.py +5 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/lyric_indicator.py +46 -2
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/projectm_backend.py +19 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/timeline.py +15 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_routing.py +26 -4
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualisers.py +92 -12
- {framerudiments-0.4.4 → framerudiments-0.4.5/src/framerudiments.egg-info}/PKG-INFO +3 -1
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_rich_subtitles.py +24 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_visualiser_routing.py +56 -1
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_visualisers.py +53 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/AGENTS.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/LICENSE +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/MANIFEST.in +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/analysis-releases.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/ECOSYSTEM.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/FrameRudiments-PATCH_WORKFLOW.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/annular-modulation.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/authoring-workspace.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/beat-alignment.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/controlled-audio-response.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/expanded-effects.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/extraction.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/full-preset-analysis.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/hybrid-preset-analysis.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/keyframed-mattes.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/beat-alignment-evidence.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/beat-alignment.mp4 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/beat-alignment.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/demo-transcript.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/effects-pipeline.svg +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/full-frame-comparison.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/full-frame-demo.mp4 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/media-evidence.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/natural-materials.mp4 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/natural-materials.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/preset-test-cards.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/rangoli-transcript.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/rangoli.mp4 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/rangoli.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/test-card-inputs/colour-bars.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/test-card-inputs/grid-rings.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/media/test-card-inputs/text-card.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/multi-stem-modulation.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/music-reactive-3d.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/natural-materials.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/particle-smoke.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/phase3-compositing.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/preset-behaviour.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/preset-profiling.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/projectm-local-runtime.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/quality-effects.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/rangoli.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/rudiment-renderers.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/scoped-post-effects.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/solid-spatial-renderers.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/spatial-renderers.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/spatiotemporal-supersampling.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/spectrum.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/sprite-visualisations.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/stem-sources-and-masks.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/variable-bpm-word-subtitles.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/visualisation-atlas.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/visualisation-coverage-matrix.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/docs/workflow-media.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/integration/test_fire_volume_gpu.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/integration/test_music_spatial_gpu.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/integration/test_preset_analysis.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/integration/test_released_preset_analysis.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/integration/test_visualisers_gpu.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/Run-ControlledPresetAnalysis.ps1 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/Run-FullPresetAnalysis.ps1 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/Run-HybridPresetAnalysis.ps1 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/Run-PresetBehaviourAnalysis.ps1 +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/authoring_preview.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/bake_mantaflow_fire.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/build_sprite_assets.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/check_projectm_runtime.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/check_release_packages.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/controlled_preset_analysis.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/ensure_projectm_runtime.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/full_preset_analysis.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_particle_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_post_effect_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_quality_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_rich_lyric_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_sprite_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_supersampling_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_timing_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_visualisation_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/generate_visualiser_routing_gallery.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/install_analysis_releases.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/prepare_fire_atlas.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/prepare_pixelcue_taggers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/preset_behaviour_pipeline.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/profile_visualisation_coverage.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/profile_visualisers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/projectm_dll_diagnostics.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/render_natural_material_demo.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/repair_projectm_mingw.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/select_projectm_generator.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/scripts/validate_patch.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/setup.cfg +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/__main__.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/advanced_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/atlas.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/audio_probes.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/beat_alignment.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/beat_timeline.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/choreography.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/circular_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/cli.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/controlled_profiling.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/demo.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/effect_catalogue.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/effect_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/effects.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/encoding.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_assets/mantaflow/NOTICE.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_assets/mantaflow/atlas.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_assets/mantaflow/flames.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_assets/marburg/NOTICE.md +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_assets/marburg/atlas.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_assets/marburg/flames.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_assets.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_fluid.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_renderer.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/fire_volume.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/flight.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/gpu_spatial_backend.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/io.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/keyframes.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/layer_animation.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/layer_displacement.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/layer_masks.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/modulation.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/motion.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/music_spatial_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/natural_materials.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/output_quality.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/particle_engine.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/post_effects.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/preset_library.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/preset_profiling.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/profile_analysis.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/project.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/quality_effects.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/rangoli_renderer.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/render.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/rich_subtitles.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/rudiment_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/scoped_effects.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/solid_spatial_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/spatial_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/spectrum.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/arcade.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/crystal.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/fire.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/generated-neon.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/glass.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/hud.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/ice.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/lcd-amber.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/lcd-green.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/lcd.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/led.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/manifest.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/metal.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/mono.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/neon.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/pixel.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/plasma.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_assets/vfd.png +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_catalogue.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/sprite_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/stem_blend.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/stemlab.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/subtitles.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/super_sampling.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/terrain_field.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visual_style.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualisation_catalogue.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/butterchurn-page.js +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/butterchurn-worker.mjs +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/feedback.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/feedback.milk +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/julia.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/julia.milk +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/kaleidoscope.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/kaleidoscope.milk +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/package-lock.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/package.json +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_assets/rangoli-background.milk +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/visualiser_setup.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments/word_timing.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments.egg-info/SOURCES.txt +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments.egg-info/dependency_links.txt +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments.egg-info/entry_points.txt +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments.egg-info/requires.txt +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/src/framerudiments.egg-info/top_level.txt +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/conftest.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_adapters.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_advanced_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_authoring_preview.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_beat_alignment.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_behaviour_pipeline.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_checkpoint_batches.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_controlled_audio.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_coverage_matrix.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_default_antialias.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_effect_batch.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_extracted_helpers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_fire_methods.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_flight.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_full_preset_analysis.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_gpu_ci_isolation.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_hybrid_launcher.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_keyframes.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_layer_masks.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_lyrics.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_music_spatial_pipeline.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_music_spatial_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_natural_materials.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_particle_engine.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_patch_validation.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_phase3_animation.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_phase3_gpu_injection.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_pipeline.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_preset_profiling.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_projectm_dll_diagnostics.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_projectm_generator.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_projectm_runtime_preflight.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_pytest_temp_isolation.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_quality_effects.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_rangoli_renderer.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_release_metadata.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_repair_projectm_mingw.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_rudiment_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_scoped_effects.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_solid_spatial_integration.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_solid_spatial_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_spatial_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_spectrum.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_sprite_renderers.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_stem_blend.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_stem_source_integration.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_supersampling.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_timeline.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_variable_bpm_words.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_visualisation_modulation.py +0 -0
- {framerudiments-0.4.4 → framerudiments-0.4.5}/tests/test_windows_launcher_splitpath.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: framerudiments
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
|
|
5
5
|
Author: Kieran Simkin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -204,6 +204,8 @@ text. Existing plain text remains untouched unless configured. Set `word_indicat
|
|
|
204
204
|
native 1-dimensional DanceRudiment-driven bouncing sprite that tracks the active word of
|
|
205
205
|
the full line. Its particle field can be continuous, word/cue-triggered, or hybrid;
|
|
206
206
|
trigger windows independently change colour, density, size, glow and burst count.
|
|
207
|
+
An optional between-word travel arc rises before the next timestamp and lands
|
|
208
|
+
exactly on its encoded word frame.
|
|
207
209
|
See [rich lyric reference](docs/rich-lyric-effects.md) and the
|
|
208
210
|
[CI-generated demo](https://kieransimkin.github.io/FrameRudiments/rich-lyrics/).
|
|
209
211
|
|
|
@@ -173,6 +173,8 @@ text. Existing plain text remains untouched unless configured. Set `word_indicat
|
|
|
173
173
|
native 1-dimensional DanceRudiment-driven bouncing sprite that tracks the active word of
|
|
174
174
|
the full line. Its particle field can be continuous, word/cue-triggered, or hybrid;
|
|
175
175
|
trigger windows independently change colour, density, size, glow and burst count.
|
|
176
|
+
An optional between-word travel arc rises before the next timestamp and lands
|
|
177
|
+
exactly on its encoded word frame.
|
|
176
178
|
See [rich lyric reference](docs/rich-lyric-effects.md) and the
|
|
177
179
|
[CI-generated demo](https://kieransimkin.github.io/FrameRudiments/rich-lyrics/).
|
|
178
180
|
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
# FrameRudiments 0.4.5
|
|
2
|
+
|
|
3
|
+
- Word indicators can now travel between encoded word hits with a configurable
|
|
4
|
+
arc and land on the next word's authoritative frame, while retaining the
|
|
5
|
+
existing after-hit mode.
|
|
6
|
+
- projectM stages accept source-time preset timelines and perform bounded soft
|
|
7
|
+
transitions on the existing feedback instance, with every switch recorded in
|
|
8
|
+
render evidence.
|
|
9
|
+
- Lyric layers can feed the visualiser only during their final bounded pips when
|
|
10
|
+
a real blank gap follows, allowing the departing line to dissolve into the
|
|
11
|
+
background without contaminating immediate line-to-line changes.
|
|
12
|
+
- Project-format and visualiser documentation describe the new contracts;
|
|
13
|
+
focused regression coverage preserves clock, preset trust, routing and
|
|
14
|
+
evidence behaviour. Private campaign artwork and audio remain outside the
|
|
15
|
+
distribution.
|
|
16
|
+
|
|
1
17
|
# FrameRudiments 0.4.4
|
|
2
18
|
|
|
3
19
|
- Spectrum views accept a validated `style.grid` boolean. Set it to `false` to remove the three fixed dB guide lines while retaining calibrated bars and moving peak holds; label visibility remains independent.
|
|
@@ -23,6 +23,7 @@ the inputs are checked again before and after a render.
|
|
|
23
23
|
| `layers` | Ordered sprite, legacy bar and modular rudiment layers described below. |
|
|
24
24
|
| Native radial petals | Use `type: "rudiment", renderer: "rangoli"` with independent scale/spin/lift axes; see [rangoli configuration and actual demo](rangoli.md). |
|
|
25
25
|
| `visualiser_routing` | Optional per-element GPU input/overlay policies. Feed continuously, only for first N native pips after visibility onset, or never; see [routing](visualiser-routing.md). |
|
|
26
|
+
| `effects[].preset_timeline` | Optional ordered projectM preset changes at canonical source seconds, using the native soft transition and retaining one feedback instance. |
|
|
26
27
|
| `lyric_art` | `{items: [{text, asset}], placement: <sprite fields>, fallback: "text" or "error"}`. Exact text lookup preserves canonical lyric association. `error` requires every visible lyric to have an asset. |
|
|
27
28
|
| `stemlab_loop` | `{file: <pinned loops report>, id: <accepted loop id>}`; imports native bounds/rate, verifies source SHA, rejects conflicting explicit audio values. Does not approve a seam. |
|
|
28
29
|
|
|
@@ -148,7 +149,9 @@ Set `effects` and optional checksum-pinned `font` on any `captions`, `word_capti
|
|
|
148
149
|
emboss, inner/outer glow and drop shadow. `word_indicator` needs both line and word LRC,
|
|
149
150
|
a full-line captions block and a selected native one-axis Rudiment bounce driver. Its
|
|
150
151
|
particle options support constant emission, timed word/cue bursts and trigger-driven
|
|
151
|
-
appearance changes
|
|
152
|
+
appearance changes. `travel_mode: "between_words"` can use the pinned next-word
|
|
153
|
+
timestamp to follow a smooth pre-hit arc and land exactly on the word frame;
|
|
154
|
+
see [full schema and examples](rich-lyric-effects.md).
|
|
152
155
|
|
|
153
156
|
## Optional full-frame effects
|
|
154
157
|
|
|
@@ -92,6 +92,8 @@ DanceRudiment axis to the y-coordinate. The cursor may use a `spark`, `orb`,
|
|
|
92
92
|
"glow": 5,
|
|
93
93
|
"opacity": 1,
|
|
94
94
|
"travel_frames": 10,
|
|
95
|
+
"travel_mode": "between_words",
|
|
96
|
+
"travel_arc": 24,
|
|
95
97
|
"offset_x": 0,
|
|
96
98
|
"offset_y": 14,
|
|
97
99
|
"bounce": {
|
|
@@ -123,7 +125,13 @@ The `sprite` field is optional. For custom files, the supplied SHA-256 is
|
|
|
123
125
|
mandatory; changing images requires changing the project reference. The same
|
|
124
126
|
built-in transparent glyphs work without external assets. `position` accepts
|
|
125
127
|
`above`, `below` or `on`. `travel_frames` (0–120) determines the smoothstep
|
|
126
|
-
movement
|
|
128
|
+
movement window. The default `travel_mode: "after_hit"` preserves the original
|
|
129
|
+
behaviour, moving from the preceding word after the new timestamp. Use
|
|
130
|
+
`travel_mode: "between_words"` to move during the final `travel_frames` before
|
|
131
|
+
the next encoded word timestamp; `travel_arc` (0–250 pixels) adds a sine-shaped
|
|
132
|
+
lift that is zero at both ends, so the sprite lands exactly on the next word
|
|
133
|
+
frame. This uses only the pinned word ledger and does not infer timing from the
|
|
134
|
+
audio. Selecting `bounce.axis` projects one native
|
|
127
135
|
DanceRudiment dimension only; captured recordings retain their own source
|
|
128
136
|
clock, and rhythmic named/table motions use the corrected BPM map if enabled.
|
|
129
137
|
|
|
@@ -55,6 +55,7 @@ misspellings are errors rather than silent omissions.
|
|
|
55
55
|
| --- | --- |
|
|
56
56
|
| `continuous` | Inject this element on every output frame where it has visible alpha pixels |
|
|
57
57
|
| `first_pips` | Inject from the first visible frame until `duration_pips` source-clock pips have elapsed, using the half-open interval `[appearance_pip, appearance_pip + duration_pips)` |
|
|
58
|
+
| `last_pips` | With `retrigger: lyric`, inject the visible lyric element for the final `duration_pips` before the next effective lyric state becomes blank. A same-frame clear followed by a new nonblank line is an immediate handoff and does not trigger this window. |
|
|
58
59
|
| `never` | No pixels from this element enter the first GPU stage; it can still be overlaid |
|
|
59
60
|
|
|
60
61
|
`duration_pips` must be a finite number between 0 and 65,536. One beat is
|
|
@@ -64,6 +65,13 @@ beat count over repeated audio excerpts. Therefore a 64-pip window may be
|
|
|
64
65
|
shorter or longer than half a second as the music accelerates or slows.
|
|
65
66
|
A zero-pip window never injects the element.
|
|
66
67
|
|
|
68
|
+
`last_pips` is designed for lyric tails that should enter feedback only before
|
|
69
|
+
a genuine silent display gap. It requires `retrigger: lyric`, an active nonblank
|
|
70
|
+
lyric, and a known next effective lyric event. The route report records
|
|
71
|
+
`remaining_pips` and `lyric_gap_exit` so the decision is auditable. With
|
|
72
|
+
`overlay: true`, the readable original remains visible while its copy enters the
|
|
73
|
+
feedback pipeline; at the blank event both disappear from the clean layer.
|
|
74
|
+
|
|
67
75
|
`retrigger` controls how the window starts:
|
|
68
76
|
|
|
69
77
|
- `reappearance` (default): reset the window when an element becomes visible
|
|
@@ -25,6 +25,33 @@ that two independent MilkDrop implementations produce identical pixels.
|
|
|
25
25
|
## Per-element feed and clean overlay routing
|
|
26
26
|
|
|
27
27
|
FrameRudiments supports an optional project-level `visualiser_routing` object.
|
|
28
|
+
|
|
29
|
+
## Source-timed projectM preset changes
|
|
30
|
+
|
|
31
|
+
A projectM stage may retain one feedback instance while changing presets at
|
|
32
|
+
canonical source-audio times. The native projectM soft transition blends the
|
|
33
|
+
old and new preset; no frame is skipped and the existing PCM/output clock stays
|
|
34
|
+
authoritative.
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"id": "section-visualiser",
|
|
39
|
+
"engine": "projectm",
|
|
40
|
+
"preset": "feedback",
|
|
41
|
+
"preset_timeline": [
|
|
42
|
+
{"at_source_seconds": 62.46, "preset": "kaleidoscope", "transition_seconds": 0.42},
|
|
43
|
+
{"at_source_seconds": 128.60, "preset": "julia", "transition_seconds": 0.42}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Times are measured in the unchanged source recording. Entries must be strictly
|
|
49
|
+
increasing. Built-in names retain their package hashes; external `.milk` files
|
|
50
|
+
still require `trusted_preset: true` on the individual timeline entry. Timeline
|
|
51
|
+
switches currently use projectM because its pinned native API provides an
|
|
52
|
+
auditable soft-cut transition on the same renderer instance. The manifest
|
|
53
|
+
records the scheduled boundary, actual frame/source time, preset hash and blend
|
|
54
|
+
duration.
|
|
28
55
|
It controls the **input RGBA texture** entering the first Butterchurn or
|
|
29
56
|
projectM stage and independently decides which original layers are composited
|
|
30
57
|
cleanly above the **last** visualiser result. The subsequent stages remain
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "framerudiments"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.5"
|
|
8
8
|
description = "Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
readme = "README.md"
|
|
@@ -33,6 +33,8 @@ class FrameContext:
|
|
|
33
33
|
lyric: dict | None
|
|
34
34
|
cue: dict | None
|
|
35
35
|
word: dict | None = None
|
|
36
|
+
lyric_next: dict | None = None
|
|
37
|
+
lyric_next_beat: float | None = None
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
class TextBlock:
|
|
@@ -413,6 +415,7 @@ class Compositor:
|
|
|
413
415
|
def context(self, frame):
|
|
414
416
|
p = self.project
|
|
415
417
|
lyric = self.lyrics.at(frame)
|
|
418
|
+
lyric_next = self.lyrics.next_effective(frame)
|
|
416
419
|
word = self.words.at(frame, lyric if p.lyrics else None) if self.words else None
|
|
417
420
|
return FrameContext(
|
|
418
421
|
frame,
|
|
@@ -422,6 +425,8 @@ class Compositor:
|
|
|
422
425
|
lyric,
|
|
423
426
|
self.cues.at(frame),
|
|
424
427
|
word,
|
|
428
|
+
lyric_next,
|
|
429
|
+
p.continuous_beat(lyric_next["frame"]) if lyric_next is not None else None,
|
|
425
430
|
)
|
|
426
431
|
|
|
427
432
|
def _caption_draw(self, image, text, ctx):
|
|
@@ -107,6 +107,8 @@ class WordIndicator:
|
|
|
107
107
|
"offset_x",
|
|
108
108
|
"offset_y",
|
|
109
109
|
"travel_frames",
|
|
110
|
+
"travel_mode",
|
|
111
|
+
"travel_arc",
|
|
110
112
|
"bounce",
|
|
111
113
|
"rotation",
|
|
112
114
|
"particles",
|
|
@@ -142,6 +144,25 @@ class WordIndicator:
|
|
|
142
144
|
self.travel = integer(
|
|
143
145
|
config.get("travel_frames", 8), "indicator transition frames", maximum=120 * time_factor
|
|
144
146
|
)
|
|
147
|
+
self.travel_mode = config.get("travel_mode", "after_hit")
|
|
148
|
+
if self.travel_mode not in {"after_hit", "between_words"}:
|
|
149
|
+
raise ValueError("Indicator travel_mode must be after_hit or between_words")
|
|
150
|
+
self.travel_arc = number(
|
|
151
|
+
config.get("travel_arc", 0), "indicator travel arc", minimum=0, maximum=250
|
|
152
|
+
)
|
|
153
|
+
self._next_words = {}
|
|
154
|
+
timed_words = [
|
|
155
|
+
row for row in project.words if row.get("text") and row.get("word_index") is not None
|
|
156
|
+
]
|
|
157
|
+
for current, following in zip(timed_words, timed_words[1:]):
|
|
158
|
+
if (
|
|
159
|
+
current.get("cycle") == following.get("cycle")
|
|
160
|
+
and current.get("line_frame") == following.get("line_frame")
|
|
161
|
+
and current.get("line_order") == following.get("line_order")
|
|
162
|
+
and following["frame"] > current["frame"]
|
|
163
|
+
):
|
|
164
|
+
key = (current["frame"], current.get("source_order"), current.get("cycle"))
|
|
165
|
+
self._next_words[key] = following
|
|
145
166
|
self.style = dict(
|
|
146
167
|
indicator_size=number(config.get("size", 18), "indicator size", minimum=2, maximum=128),
|
|
147
168
|
indicator_glow=number(config.get("glow", 4), "indicator glow", minimum=0, maximum=24),
|
|
@@ -282,7 +303,13 @@ class WordIndicator:
|
|
|
282
303
|
font=self.font_ref,
|
|
283
304
|
glyph=self.glyph,
|
|
284
305
|
mode=self.particle_mode,
|
|
285
|
-
source_clock="encoded words/cues + native bounce axis",
|
|
306
|
+
source_clock="encoded words/cues + optional native bounce axis",
|
|
307
|
+
travel={
|
|
308
|
+
"mode": self.travel_mode,
|
|
309
|
+
"frames": self.travel,
|
|
310
|
+
"arc_pixels": self.travel_arc,
|
|
311
|
+
"landing_authority": "next encoded word frame" if self.travel_mode == "between_words" else None,
|
|
312
|
+
},
|
|
286
313
|
bounce=self.bounce.evidence if self.bounce else None,
|
|
287
314
|
modulations=self.modulations.evidence,
|
|
288
315
|
cue_file=self.cue_ref,
|
|
@@ -323,7 +350,21 @@ class WordIndicator:
|
|
|
323
350
|
if not 0 <= i < len(positions):
|
|
324
351
|
return None
|
|
325
352
|
x, y = positions[i]
|
|
326
|
-
|
|
353
|
+
arc = 0.0
|
|
354
|
+
if self.travel_mode == "between_words" and self.travel:
|
|
355
|
+
key = (word["frame"], word.get("source_order"), word.get("cycle"))
|
|
356
|
+
following = self._next_words.get(key)
|
|
357
|
+
if following is not None:
|
|
358
|
+
target_index = following["word_index"]
|
|
359
|
+
start = max(word["frame"], following["frame"] - self.travel)
|
|
360
|
+
if frame >= start and 0 <= target_index < len(positions):
|
|
361
|
+
u = _unit((frame - start) / max(1, following["frame"] - start))
|
|
362
|
+
smooth = u * u * (3 - 2 * u)
|
|
363
|
+
target = positions[target_index]
|
|
364
|
+
x = x + (target[0] - x) * smooth
|
|
365
|
+
y = y + (target[1] - y) * smooth
|
|
366
|
+
arc = math.sin(math.pi * u) * self.travel_arc
|
|
367
|
+
elif self.travel and i:
|
|
327
368
|
u = _unit((frame - word["frame"]) / self.travel)
|
|
328
369
|
smooth = u * u * (3 - 2 * u)
|
|
329
370
|
before = positions[i - 1]
|
|
@@ -337,6 +378,7 @@ class WordIndicator:
|
|
|
337
378
|
y -= h / 2 + style["offset_y"]
|
|
338
379
|
else:
|
|
339
380
|
y += self.block.maximum * 0.5
|
|
381
|
+
y -= arc
|
|
340
382
|
if self.bounce:
|
|
341
383
|
values = self.bounce.sample(ctx.source_seconds, continuous_beat=ctx.beat)
|
|
342
384
|
y -= values[self.bounce_axis] * style["bounce_amplitude"]
|
|
@@ -543,5 +585,7 @@ class WordIndicator:
|
|
|
543
585
|
particles_active=active,
|
|
544
586
|
cue_trigger=ctx.frame in self._events["cue"],
|
|
545
587
|
word_trigger=ctx.frame in self._events["word"],
|
|
588
|
+
travel_mode=self.travel_mode,
|
|
589
|
+
landed=ctx.frame == ctx.word["frame"],
|
|
546
590
|
)
|
|
547
591
|
return layer.getchannel("A").getbbox()
|
|
@@ -169,6 +169,7 @@ class ProjectMBackend:
|
|
|
169
169
|
"set_preset_locked": (None, [h, C.c_bool]),
|
|
170
170
|
"set_hard_cut_enabled": (None, [h, C.c_bool]),
|
|
171
171
|
"set_preset_start_clean": (None, [h, C.c_bool]),
|
|
172
|
+
"set_soft_cut_duration": (None, [h, C.c_double]),
|
|
172
173
|
"load_preset_file": (None, [h, C.c_char_p, C.c_bool]),
|
|
173
174
|
"set_preset_switch_failed_event_callback": (None, [h, C.c_void_p, h]),
|
|
174
175
|
"pcm_add_float": (None, [h, C.POINTER(C.c_float), C.c_uint, C.c_int]),
|
|
@@ -202,6 +203,24 @@ class ProjectMBackend:
|
|
|
202
203
|
def advance(self, image, context, pcm, *, capture=False):
|
|
203
204
|
return self.process(image, context, pcm, capture=capture)
|
|
204
205
|
|
|
206
|
+
def switch_preset(self, preset, transition_seconds):
|
|
207
|
+
"""Change preset on the existing feedback instance with projectM's soft transition."""
|
|
208
|
+
self.glfw.make_context_current(self.window)
|
|
209
|
+
self.lib.projectm_set_soft_cut_duration(self.handle, float(transition_seconds))
|
|
210
|
+
self.lib.projectm_load_preset_file(
|
|
211
|
+
self.handle,
|
|
212
|
+
os.fsencode(preset),
|
|
213
|
+
bool(transition_seconds),
|
|
214
|
+
)
|
|
215
|
+
self._check()
|
|
216
|
+
self.evidence.setdefault("preset_switches", []).append(
|
|
217
|
+
dict(
|
|
218
|
+
preset_path=str(Path(preset).resolve()),
|
|
219
|
+
preset_sha256=sha256(preset),
|
|
220
|
+
transition_seconds=float(transition_seconds),
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
205
224
|
def process(self, image, context, pcm, *, capture=True):
|
|
206
225
|
gl = self.gl
|
|
207
226
|
self.glfw.make_context_current(self.window)
|
|
@@ -111,6 +111,21 @@ class EventTrack:
|
|
|
111
111
|
index = bisect_right(self.frames, frame) - 1
|
|
112
112
|
return self.events[index] if index >= 0 else None
|
|
113
113
|
|
|
114
|
+
def next_effective(self, frame):
|
|
115
|
+
"""Return the rendered state at the next distinct event frame.
|
|
116
|
+
|
|
117
|
+
Several lyric ledgers deliberately write a blank clear followed by the
|
|
118
|
+
next line at the same frame. The last event at that frame is the state
|
|
119
|
+
a viewer actually sees, so a same-frame clear must not be mistaken for
|
|
120
|
+
a real lyric gap.
|
|
121
|
+
"""
|
|
122
|
+
index = bisect_right(self.frames, frame)
|
|
123
|
+
if index >= len(self.events):
|
|
124
|
+
return None
|
|
125
|
+
next_frame = self.events[index]["frame"]
|
|
126
|
+
end = bisect_right(self.frames, next_frame)
|
|
127
|
+
return self.events[end - 1]
|
|
128
|
+
|
|
114
129
|
def neighbours(self, frame):
|
|
115
130
|
index = bisect_right(self.frames, frame) - 1
|
|
116
131
|
current = self.events[index] if index >= 0 else None
|
|
@@ -13,7 +13,7 @@ from PIL import Image
|
|
|
13
13
|
from .io import fields, number
|
|
14
14
|
|
|
15
15
|
MAX_TARGETS = 256
|
|
16
|
-
MODES = frozenset({"continuous", "first_pips", "never"})
|
|
16
|
+
MODES = frozenset({"continuous", "first_pips", "last_pips", "never"})
|
|
17
17
|
RETRIGGERS = frozenset({"reappearance", "once", "lyric", "cue"})
|
|
18
18
|
|
|
19
19
|
|
|
@@ -27,12 +27,12 @@ def _policy(config, label, inherited=None):
|
|
|
27
27
|
fields(feed, {"mode", "duration_pips"}, label + ".feed")
|
|
28
28
|
mode = feed.get("mode")
|
|
29
29
|
if mode not in MODES:
|
|
30
|
-
raise ValueError(f"{label}.feed.mode must be continuous, first_pips or never")
|
|
31
|
-
if mode
|
|
30
|
+
raise ValueError(f"{label}.feed.mode must be continuous, first_pips, last_pips or never")
|
|
31
|
+
if mode in {"first_pips", "last_pips"}:
|
|
32
32
|
duration = number(feed.get("duration_pips"), label + ".feed.duration_pips", minimum=0, maximum=65536)
|
|
33
33
|
feed = dict(mode=mode, duration_pips=duration)
|
|
34
34
|
elif "duration_pips" in feed:
|
|
35
|
-
raise ValueError(f"{label}.feed.duration_pips requires first_pips")
|
|
35
|
+
raise ValueError(f"{label}.feed.duration_pips requires first_pips or last_pips")
|
|
36
36
|
else:
|
|
37
37
|
feed = dict(mode=mode)
|
|
38
38
|
overlay = config.get("overlay", parent["overlay"])
|
|
@@ -41,6 +41,8 @@ def _policy(config, label, inherited=None):
|
|
|
41
41
|
retrigger = config.get("retrigger", parent["retrigger"])
|
|
42
42
|
if retrigger not in RETRIGGERS:
|
|
43
43
|
raise ValueError(f"{label}.retrigger must be reappearance, once, lyric or cue")
|
|
44
|
+
if feed["mode"] == "last_pips" and retrigger != "lyric":
|
|
45
|
+
raise ValueError(f"{label}.feed.mode last_pips requires retrigger=lyric")
|
|
44
46
|
return dict(feed=feed, overlay=overlay, retrigger=retrigger)
|
|
45
47
|
|
|
46
48
|
|
|
@@ -128,6 +130,20 @@ class VisualiserLayerRouting:
|
|
|
128
130
|
state["lyric"], state["cue"] = lyric, cue
|
|
129
131
|
mode = policy["feed"]["mode"]
|
|
130
132
|
elapsed = max(0.0, pip - state["onset"]) if state["onset"] is not None else None
|
|
133
|
+
lyric_next = getattr(context, "lyric_next", None)
|
|
134
|
+
lyric_next_beat = getattr(context, "lyric_next_beat", None)
|
|
135
|
+
remaining = (
|
|
136
|
+
max(0.0, float(lyric_next_beat) * 64.0 - pip)
|
|
137
|
+
if lyric_next_beat is not None and math.isfinite(float(lyric_next_beat))
|
|
138
|
+
else None
|
|
139
|
+
)
|
|
140
|
+
lyric_gap_exit = bool(
|
|
141
|
+
trigger == "lyric"
|
|
142
|
+
and getattr(context, "lyric", None)
|
|
143
|
+
and getattr(context, "lyric", {}).get("text")
|
|
144
|
+
and lyric_next is not None
|
|
145
|
+
and not lyric_next.get("text")
|
|
146
|
+
)
|
|
131
147
|
feeding = bool(
|
|
132
148
|
visible
|
|
133
149
|
and (
|
|
@@ -135,6 +151,10 @@ class VisualiserLayerRouting:
|
|
|
135
151
|
or mode == "first_pips"
|
|
136
152
|
and elapsed is not None
|
|
137
153
|
and elapsed < policy["feed"]["duration_pips"]
|
|
154
|
+
or mode == "last_pips"
|
|
155
|
+
and lyric_gap_exit
|
|
156
|
+
and remaining is not None
|
|
157
|
+
and 0.0 < remaining <= policy["feed"]["duration_pips"]
|
|
138
158
|
)
|
|
139
159
|
)
|
|
140
160
|
on_top = bool(visible and policy["overlay"])
|
|
@@ -149,6 +169,8 @@ class VisualiserLayerRouting:
|
|
|
149
169
|
overlay=on_top,
|
|
150
170
|
first_pip=state["onset"],
|
|
151
171
|
elapsed_pips=elapsed,
|
|
172
|
+
remaining_pips=remaining,
|
|
173
|
+
lyric_gap_exit=lyric_gap_exit,
|
|
152
174
|
policy=policy["feed"]["mode"],
|
|
153
175
|
)
|
|
154
176
|
self.next_frame += 1
|
|
@@ -18,31 +18,88 @@ ASSETS = Path(__file__).with_name("visualiser_assets")
|
|
|
18
18
|
PRESETS = ("feedback", "kaleidoscope", "julia")
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _preset_ref(project, engine, preset, *, trusted=False):
|
|
22
|
+
if isinstance(preset, str):
|
|
23
|
+
if preset not in PRESETS:
|
|
24
|
+
raise ValueError(f"Unknown built-in visualiser preset: {preset}")
|
|
25
|
+
path = ASSETS / f"{preset}.{'json' if engine == 'butterchurn' else 'milk'}"
|
|
26
|
+
return project.ref(dict(path=str(path), sha256=sha256(path)))
|
|
27
|
+
if trusted is not True:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"External presets contain executable equations/shaders: set trusted_preset true after review"
|
|
30
|
+
)
|
|
31
|
+
return project.ref(preset)
|
|
32
|
+
|
|
33
|
+
|
|
21
34
|
def effect_specs(project):
|
|
22
35
|
configs = project.data.get("effects", [])
|
|
23
36
|
if not isinstance(configs, list) or len(configs) > 8:
|
|
24
37
|
raise ValueError("effects must be an ordered list with at most eight stages")
|
|
25
38
|
specs = []
|
|
26
39
|
for cfg in configs:
|
|
27
|
-
fields(
|
|
40
|
+
fields(
|
|
41
|
+
cfg,
|
|
42
|
+
{
|
|
43
|
+
"id",
|
|
44
|
+
"engine",
|
|
45
|
+
"preset",
|
|
46
|
+
"input_mix",
|
|
47
|
+
"seed",
|
|
48
|
+
"trusted_preset",
|
|
49
|
+
"preset_timeline",
|
|
50
|
+
},
|
|
51
|
+
"full-frame effect",
|
|
52
|
+
)
|
|
28
53
|
name = cfg.get("id")
|
|
29
54
|
if not isinstance(name, str) or not name or name in [item["id"] for item in specs]:
|
|
30
55
|
raise ValueError("Full-frame effect ids must be nonempty and unique")
|
|
31
56
|
engine = cfg.get("engine")
|
|
32
57
|
if engine not in {"butterchurn", "projectm"}:
|
|
33
58
|
raise ValueError("Full-frame engine must be butterchurn or projectm")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
ref = _preset_ref(
|
|
60
|
+
project,
|
|
61
|
+
engine,
|
|
62
|
+
cfg.get("preset", "feedback"),
|
|
63
|
+
trusted=cfg.get("trusted_preset", False),
|
|
64
|
+
)
|
|
65
|
+
timeline = cfg.get("preset_timeline", [])
|
|
66
|
+
if timeline and engine != "projectm":
|
|
67
|
+
raise ValueError("preset_timeline currently requires projectM")
|
|
68
|
+
if not isinstance(timeline, list) or len(timeline) > 64:
|
|
69
|
+
raise ValueError("preset_timeline must be a list with at most 64 entries")
|
|
70
|
+
resolved_timeline = []
|
|
71
|
+
previous = -1.0
|
|
72
|
+
for index, entry in enumerate(timeline):
|
|
73
|
+
fields(
|
|
74
|
+
entry,
|
|
75
|
+
{"at_source_seconds", "preset", "transition_seconds", "trusted_preset"},
|
|
76
|
+
f"preset_timeline[{index}]",
|
|
77
|
+
)
|
|
78
|
+
at = number(
|
|
79
|
+
entry.get("at_source_seconds"),
|
|
80
|
+
f"preset_timeline[{index}].at_source_seconds",
|
|
81
|
+
minimum=0,
|
|
82
|
+
)
|
|
83
|
+
if at <= previous:
|
|
84
|
+
raise ValueError("preset_timeline source times must be strictly increasing")
|
|
85
|
+
previous = at
|
|
86
|
+
resolved_timeline.append(
|
|
87
|
+
dict(
|
|
88
|
+
at_source_seconds=at,
|
|
89
|
+
preset=_preset_ref(
|
|
90
|
+
project,
|
|
91
|
+
engine,
|
|
92
|
+
entry.get("preset"),
|
|
93
|
+
trusted=entry.get("trusted_preset", False),
|
|
94
|
+
),
|
|
95
|
+
transition_seconds=number(
|
|
96
|
+
entry.get("transition_seconds", 0.5),
|
|
97
|
+
f"preset_timeline[{index}].transition_seconds",
|
|
98
|
+
minimum=0,
|
|
99
|
+
maximum=30,
|
|
100
|
+
),
|
|
44
101
|
)
|
|
45
|
-
|
|
102
|
+
)
|
|
46
103
|
specs.append(
|
|
47
104
|
dict(
|
|
48
105
|
id=name,
|
|
@@ -50,6 +107,7 @@ def effect_specs(project):
|
|
|
50
107
|
preset=ref,
|
|
51
108
|
input_mix=number(cfg.get("input_mix", 0.65), "input mix", minimum=0.001, maximum=1),
|
|
52
109
|
seed=integer(cfg.get("seed", 0), "visualiser seed", maximum=2**32 - 1),
|
|
110
|
+
preset_timeline=resolved_timeline,
|
|
53
111
|
)
|
|
54
112
|
)
|
|
55
113
|
return specs
|
|
@@ -243,6 +301,8 @@ class EffectChain:
|
|
|
243
301
|
self.closed = False
|
|
244
302
|
self.specs = specs
|
|
245
303
|
self.routing = routing
|
|
304
|
+
self.preset_timeline_positions = [0 for _ in specs]
|
|
305
|
+
self.preset_switches = []
|
|
246
306
|
self.stage_injections = {}
|
|
247
307
|
requested = getattr(project, "data", {}).get("visualiser_stage_injections", [])
|
|
248
308
|
if not isinstance(requested, list) or len(requested) > 32 or (requested and not specs):
|
|
@@ -298,6 +358,25 @@ class EffectChain:
|
|
|
298
358
|
if self.stage_injections and isolated_layers is None:
|
|
299
359
|
raise ValueError("Intermediate stage injection needs isolated layer surfaces")
|
|
300
360
|
for index, backend in enumerate(self.backends):
|
|
361
|
+
timeline = self.specs[index].get("preset_timeline", [])
|
|
362
|
+
position = self.preset_timeline_positions[index]
|
|
363
|
+
while position < len(timeline) and context.source_seconds >= timeline[position]["at_source_seconds"]:
|
|
364
|
+
event = timeline[position]
|
|
365
|
+
if not hasattr(backend, "switch_preset"):
|
|
366
|
+
raise ValueError("Scheduled preset changes require a backend with switch_preset support")
|
|
367
|
+
backend.switch_preset(event["preset"]["path"], event["transition_seconds"])
|
|
368
|
+
self.preset_switches.append(
|
|
369
|
+
dict(
|
|
370
|
+
frame=context.frame,
|
|
371
|
+
source_seconds=context.source_seconds,
|
|
372
|
+
stage=index,
|
|
373
|
+
scheduled_at_source_seconds=event["at_source_seconds"],
|
|
374
|
+
transition_seconds=event["transition_seconds"],
|
|
375
|
+
preset=event["preset"],
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
position += 1
|
|
379
|
+
self.preset_timeline_positions[index] = position
|
|
301
380
|
if index in self.stage_injections:
|
|
302
381
|
image = image.convert("RGBA")
|
|
303
382
|
for names, opacity in self.stage_injections[index]:
|
|
@@ -330,6 +409,7 @@ class EffectChain:
|
|
|
330
409
|
pcm=self.audio.evidence,
|
|
331
410
|
routing=self.routing.evidence if self.routing is not None else None,
|
|
332
411
|
stage_injections={index: entries for index, entries in self.stage_injections.items()},
|
|
412
|
+
preset_switches=self.preset_switches,
|
|
333
413
|
stages=[dict(spec, runtime=backend.evidence) for spec, backend in zip(self.specs, self.backends)],
|
|
334
414
|
limits="GPU/driver and engine noise can vary; not a cross-platform bit-exact contract. Text bounds refer to pre-effect composition.",
|
|
335
415
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: framerudiments
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
|
|
5
5
|
Author: Kieran Simkin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -204,6 +204,8 @@ text. Existing plain text remains untouched unless configured. Set `word_indicat
|
|
|
204
204
|
native 1-dimensional DanceRudiment-driven bouncing sprite that tracks the active word of
|
|
205
205
|
the full line. Its particle field can be continuous, word/cue-triggered, or hybrid;
|
|
206
206
|
trigger windows independently change colour, density, size, glow and burst count.
|
|
207
|
+
An optional between-word travel arc rises before the next timestamp and lands
|
|
208
|
+
exactly on its encoded word frame.
|
|
207
209
|
See [rich lyric reference](docs/rich-lyric-effects.md) and the
|
|
208
210
|
[CI-generated demo](https://kieransimkin.github.io/FrameRudiments/rich-lyrics/).
|
|
209
211
|
|
|
@@ -226,6 +226,30 @@ def test_bounce_uses_single_selected_native_rudiment_axis_and_replays(project):
|
|
|
226
226
|
assert obj.bounce_axis == 0
|
|
227
227
|
|
|
228
228
|
|
|
229
|
+
def test_between_word_arc_lands_on_the_next_word_frame(project):
|
|
230
|
+
settings = {
|
|
231
|
+
"glyph": "star",
|
|
232
|
+
"size": 17,
|
|
233
|
+
"travel_frames": 20,
|
|
234
|
+
"travel_mode": "between_words",
|
|
235
|
+
"travel_arc": 24,
|
|
236
|
+
}
|
|
237
|
+
obj = WordIndicator(project, settings, project.context, block(project, {}))
|
|
238
|
+
first = obj._centre(5)
|
|
239
|
+
midpoint = obj._centre(25)
|
|
240
|
+
before_landing = obj._centre(34)
|
|
241
|
+
landed = obj._centre(35)
|
|
242
|
+
assert first and midpoint and before_landing and landed
|
|
243
|
+
assert first[0] < midpoint[0] < landed[0]
|
|
244
|
+
assert midpoint[1] < first[1] and midpoint[1] < landed[1]
|
|
245
|
+
assert before_landing[0] < landed[0]
|
|
246
|
+
assert landed[1] == pytest.approx(first[1])
|
|
247
|
+
frame = image()
|
|
248
|
+
obj.draw(frame, project.context(35))
|
|
249
|
+
assert obj.frame_report["landed"] is True
|
|
250
|
+
assert obj.frame_report["travel_mode"] == "between_words"
|
|
251
|
+
|
|
252
|
+
|
|
229
253
|
def test_constant_particles_stay_on_and_word_events_trigger_bursts(project):
|
|
230
254
|
obj = indicator(
|
|
231
255
|
project,
|