framerudiments 0.4.7__tar.gz → 0.4.8__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.
Files changed (269) hide show
  1. {framerudiments-0.4.7/src/framerudiments.egg-info → framerudiments-0.4.8}/PKG-INFO +17 -1
  2. {framerudiments-0.4.7 → framerudiments-0.4.8}/README.md +16 -0
  3. {framerudiments-0.4.7 → framerudiments-0.4.8}/RELEASE_NOTES.md +19 -0
  4. framerudiments-0.4.8/docs/media/visualiser-feedback-writeback.json +45 -0
  5. framerudiments-0.4.8/docs/media/visualiser-feedback-writeback.png +0 -0
  6. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/project-format.md +3 -2
  7. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/visualisers.md +105 -0
  8. framerudiments-0.4.8/integration/build_feedback_contact_sheet.py +83 -0
  9. framerudiments-0.4.8/integration/verify_feedback_writeback.py +117 -0
  10. {framerudiments-0.4.7 → framerudiments-0.4.8}/pyproject.toml +1 -1
  11. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/__init__.py +1 -1
  12. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/compositor.py +13 -2
  13. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/projectm_backend.py +33 -16
  14. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/render.py +16 -2
  15. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/butterchurn-page.js +13 -7
  16. framerudiments-0.4.8/src/framerudiments/visualiser_compositing.py +146 -0
  17. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualisers.py +108 -3
  18. {framerudiments-0.4.7 → framerudiments-0.4.8/src/framerudiments.egg-info}/PKG-INFO +17 -1
  19. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments.egg-info/SOURCES.txt +6 -0
  20. framerudiments-0.4.8/tests/test_visualiser_compositing.py +215 -0
  21. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_visualisers.py +29 -0
  22. {framerudiments-0.4.7 → framerudiments-0.4.8}/AGENTS.md +0 -0
  23. {framerudiments-0.4.7 → framerudiments-0.4.8}/LICENSE +0 -0
  24. {framerudiments-0.4.7 → framerudiments-0.4.8}/MANIFEST.in +0 -0
  25. {framerudiments-0.4.7 → framerudiments-0.4.8}/analysis-releases.json +0 -0
  26. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/ECOSYSTEM.md +0 -0
  27. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/FrameRudiments-PATCH_WORKFLOW.md +0 -0
  28. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/annular-modulation.md +0 -0
  29. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/authoring-workspace.md +0 -0
  30. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/beat-alignment.md +0 -0
  31. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/bounded-preflight.md +0 -0
  32. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/controlled-audio-response.md +0 -0
  33. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/expanded-effects.md +0 -0
  34. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/extraction.md +0 -0
  35. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/full-preset-analysis.md +0 -0
  36. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/hybrid-preset-analysis.md +0 -0
  37. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/keyframed-mattes.md +0 -0
  38. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/beat-alignment-evidence.json +0 -0
  39. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/beat-alignment.mp4 +0 -0
  40. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/beat-alignment.png +0 -0
  41. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/bounded-preflight.svg +0 -0
  42. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/demo-transcript.md +0 -0
  43. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/effects-pipeline.svg +0 -0
  44. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/full-frame-comparison.png +0 -0
  45. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/full-frame-demo.mp4 +0 -0
  46. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/media-evidence.json +0 -0
  47. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/natural-materials.mp4 +0 -0
  48. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/natural-materials.png +0 -0
  49. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/palette-grade-comparison.png +0 -0
  50. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/preset-test-cards.png +0 -0
  51. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/rangoli-transcript.md +0 -0
  52. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/rangoli.mp4 +0 -0
  53. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/rangoli.png +0 -0
  54. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/test-card-inputs/colour-bars.png +0 -0
  55. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/test-card-inputs/grid-rings.png +0 -0
  56. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/media/test-card-inputs/text-card.png +0 -0
  57. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/multi-stem-modulation.md +0 -0
  58. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/music-reactive-3d.md +0 -0
  59. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/natural-materials.md +0 -0
  60. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/particle-smoke.md +0 -0
  61. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/phase3-compositing.md +0 -0
  62. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/preset-behaviour.md +0 -0
  63. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/preset-profiling.md +0 -0
  64. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/projectm-local-runtime.md +0 -0
  65. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/quality-effects.md +0 -0
  66. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/rangoli.md +0 -0
  67. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/rich-lyric-effects.md +0 -0
  68. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/rudiment-renderers.md +0 -0
  69. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/scoped-post-effects.md +0 -0
  70. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/solid-spatial-renderers.md +0 -0
  71. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/spatial-renderers.md +0 -0
  72. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/spatiotemporal-supersampling.md +0 -0
  73. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/spectrum.md +0 -0
  74. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/sprite-visualisations.md +0 -0
  75. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/stem-sources-and-masks.md +0 -0
  76. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/variable-bpm-word-subtitles.md +0 -0
  77. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/visualisation-atlas.md +0 -0
  78. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/visualisation-coverage-matrix.md +0 -0
  79. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/visualiser-routing.md +0 -0
  80. {framerudiments-0.4.7 → framerudiments-0.4.8}/docs/workflow-media.md +0 -0
  81. {framerudiments-0.4.7 → framerudiments-0.4.8}/integration/test_fire_volume_gpu.py +0 -0
  82. {framerudiments-0.4.7 → framerudiments-0.4.8}/integration/test_music_spatial_gpu.py +0 -0
  83. {framerudiments-0.4.7 → framerudiments-0.4.8}/integration/test_preset_analysis.py +0 -0
  84. {framerudiments-0.4.7 → framerudiments-0.4.8}/integration/test_released_preset_analysis.py +0 -0
  85. {framerudiments-0.4.7 → framerudiments-0.4.8}/integration/test_visualisers_gpu.py +0 -0
  86. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/Run-ControlledPresetAnalysis.ps1 +0 -0
  87. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/Run-FullPresetAnalysis.ps1 +0 -0
  88. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/Run-HybridPresetAnalysis.ps1 +0 -0
  89. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/Run-PresetBehaviourAnalysis.ps1 +0 -0
  90. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/authoring_preview.py +0 -0
  91. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/bake_mantaflow_fire.py +0 -0
  92. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/build_sprite_assets.py +0 -0
  93. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/check_projectm_runtime.py +0 -0
  94. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/check_release_packages.py +0 -0
  95. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/controlled_preset_analysis.py +0 -0
  96. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/ensure_projectm_runtime.py +0 -0
  97. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/full_preset_analysis.py +0 -0
  98. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_particle_gallery.py +0 -0
  99. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_post_effect_gallery.py +0 -0
  100. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_quality_gallery.py +0 -0
  101. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_rich_lyric_gallery.py +0 -0
  102. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_sprite_gallery.py +0 -0
  103. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_supersampling_gallery.py +0 -0
  104. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_timing_gallery.py +0 -0
  105. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_visualisation_gallery.py +0 -0
  106. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/generate_visualiser_routing_gallery.py +0 -0
  107. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/install_analysis_releases.py +0 -0
  108. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/prepare_fire_atlas.py +0 -0
  109. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/prepare_pixelcue_taggers.py +0 -0
  110. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/preset_behaviour_pipeline.py +0 -0
  111. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/profile_visualisation_coverage.py +0 -0
  112. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/profile_visualisers.py +0 -0
  113. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/projectm_dll_diagnostics.py +0 -0
  114. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/render_natural_material_demo.py +0 -0
  115. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/repair_projectm_mingw.py +0 -0
  116. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/select_projectm_generator.py +0 -0
  117. {framerudiments-0.4.7 → framerudiments-0.4.8}/scripts/validate_patch.py +0 -0
  118. {framerudiments-0.4.7 → framerudiments-0.4.8}/setup.cfg +0 -0
  119. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/__main__.py +0 -0
  120. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/advanced_renderers.py +0 -0
  121. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/atlas.py +0 -0
  122. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/audio_probes.py +0 -0
  123. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/beat_alignment.py +0 -0
  124. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/beat_timeline.py +0 -0
  125. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/choreography.py +0 -0
  126. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/circular_renderers.py +0 -0
  127. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/cli.py +0 -0
  128. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/controlled_profiling.py +0 -0
  129. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/demo.py +0 -0
  130. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/effect_catalogue.py +0 -0
  131. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/effect_renderers.py +0 -0
  132. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/effects.py +0 -0
  133. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/encoding.py +0 -0
  134. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_assets/mantaflow/NOTICE.md +0 -0
  135. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_assets/mantaflow/atlas.json +0 -0
  136. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_assets/mantaflow/flames.png +0 -0
  137. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_assets/marburg/NOTICE.md +0 -0
  138. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_assets/marburg/atlas.json +0 -0
  139. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_assets/marburg/flames.png +0 -0
  140. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_assets.py +0 -0
  141. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_fluid.py +0 -0
  142. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_renderer.py +0 -0
  143. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/fire_volume.py +0 -0
  144. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/flight.py +0 -0
  145. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/gpu_spatial_backend.py +0 -0
  146. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/io.py +0 -0
  147. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/keyframes.py +0 -0
  148. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/layer_animation.py +0 -0
  149. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/layer_displacement.py +0 -0
  150. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/layer_masks.py +0 -0
  151. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/lyric_indicator.py +0 -0
  152. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/modulation.py +0 -0
  153. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/motion.py +0 -0
  154. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/music_spatial_renderers.py +0 -0
  155. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/natural_materials.py +0 -0
  156. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/output_quality.py +0 -0
  157. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/particle_engine.py +0 -0
  158. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/post_effects.py +0 -0
  159. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/preset_library.py +0 -0
  160. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/preset_profiling.py +0 -0
  161. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/profile_analysis.py +0 -0
  162. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/project.py +0 -0
  163. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/quality_effects.py +0 -0
  164. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/rangoli_renderer.py +0 -0
  165. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/rich_subtitles.py +0 -0
  166. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/rudiment_renderers.py +0 -0
  167. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/scoped_effects.py +0 -0
  168. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/solid_spatial_renderers.py +0 -0
  169. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/spatial_renderers.py +0 -0
  170. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/spectrum.py +0 -0
  171. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/arcade.png +0 -0
  172. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/crystal.png +0 -0
  173. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/fire.png +0 -0
  174. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/generated-neon.png +0 -0
  175. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/glass.png +0 -0
  176. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/hud.png +0 -0
  177. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/ice.png +0 -0
  178. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/lcd-amber.png +0 -0
  179. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/lcd-green.png +0 -0
  180. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/lcd.png +0 -0
  181. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/led.png +0 -0
  182. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/manifest.json +0 -0
  183. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/metal.png +0 -0
  184. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/mono.png +0 -0
  185. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/neon.png +0 -0
  186. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/pixel.png +0 -0
  187. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/plasma.png +0 -0
  188. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_assets/vfd.png +0 -0
  189. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_catalogue.py +0 -0
  190. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/sprite_renderers.py +0 -0
  191. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/stem_blend.py +0 -0
  192. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/stemlab.py +0 -0
  193. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/subtitles.py +0 -0
  194. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/super_sampling.py +0 -0
  195. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/terrain_field.py +0 -0
  196. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/timeline.py +0 -0
  197. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visual_style.py +0 -0
  198. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualisation_catalogue.py +0 -0
  199. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/butterchurn-worker.mjs +0 -0
  200. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/feedback.json +0 -0
  201. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/feedback.milk +0 -0
  202. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/julia.json +0 -0
  203. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/julia.milk +0 -0
  204. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/kaleidoscope.json +0 -0
  205. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/kaleidoscope.milk +0 -0
  206. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/package-lock.json +0 -0
  207. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/package.json +0 -0
  208. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_assets/rangoli-background.milk +0 -0
  209. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_routing.py +0 -0
  210. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/visualiser_setup.py +0 -0
  211. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments/word_timing.py +0 -0
  212. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments.egg-info/dependency_links.txt +0 -0
  213. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments.egg-info/entry_points.txt +0 -0
  214. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments.egg-info/requires.txt +0 -0
  215. {framerudiments-0.4.7 → framerudiments-0.4.8}/src/framerudiments.egg-info/top_level.txt +0 -0
  216. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/conftest.py +0 -0
  217. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_adapters.py +0 -0
  218. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_advanced_renderers.py +0 -0
  219. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_authoring_preview.py +0 -0
  220. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_beat_alignment.py +0 -0
  221. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_behaviour_pipeline.py +0 -0
  222. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_checkpoint_batches.py +0 -0
  223. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_controlled_audio.py +0 -0
  224. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_coverage_matrix.py +0 -0
  225. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_default_antialias.py +0 -0
  226. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_effect_batch.py +0 -0
  227. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_encoding.py +0 -0
  228. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_extracted_helpers.py +0 -0
  229. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_fire_methods.py +0 -0
  230. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_flight.py +0 -0
  231. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_full_preset_analysis.py +0 -0
  232. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_gpu_ci_isolation.py +0 -0
  233. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_hybrid_launcher.py +0 -0
  234. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_keyframes.py +0 -0
  235. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_layer_masks.py +0 -0
  236. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_lyrics.py +0 -0
  237. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_music_spatial_pipeline.py +0 -0
  238. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_music_spatial_renderers.py +0 -0
  239. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_natural_materials.py +0 -0
  240. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_particle_engine.py +0 -0
  241. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_patch_validation.py +0 -0
  242. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_phase3_animation.py +0 -0
  243. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_phase3_gpu_injection.py +0 -0
  244. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_pipeline.py +0 -0
  245. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_preset_profiling.py +0 -0
  246. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_projectm_dll_diagnostics.py +0 -0
  247. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_projectm_generator.py +0 -0
  248. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_projectm_runtime_preflight.py +0 -0
  249. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_pytest_temp_isolation.py +0 -0
  250. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_quality_effects.py +0 -0
  251. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_rangoli_renderer.py +0 -0
  252. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_release_metadata.py +0 -0
  253. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_repair_projectm_mingw.py +0 -0
  254. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_rich_subtitles.py +0 -0
  255. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_rudiment_renderers.py +0 -0
  256. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_scoped_effects.py +0 -0
  257. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_solid_spatial_integration.py +0 -0
  258. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_solid_spatial_renderers.py +0 -0
  259. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_spatial_renderers.py +0 -0
  260. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_spectrum.py +0 -0
  261. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_sprite_renderers.py +0 -0
  262. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_stem_blend.py +0 -0
  263. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_stem_source_integration.py +0 -0
  264. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_supersampling.py +0 -0
  265. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_timeline.py +0 -0
  266. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_variable_bpm_words.py +0 -0
  267. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_visualisation_modulation.py +0 -0
  268. {framerudiments-0.4.7 → framerudiments-0.4.8}/tests/test_visualiser_routing.py +0 -0
  269. {framerudiments-0.4.7 → framerudiments-0.4.8}/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.7
3
+ Version: 0.4.8
4
4
  Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
5
5
  Author: Kieran Simkin
6
6
  License-Expression: MIT
@@ -358,6 +358,22 @@ a browser or projectM. Feedback is sequential, and GPU output can vary by driver
358
358
  Inspect processed/encoded glyphs and safe zones; pre-effect text bounds cannot
359
359
  prove final legibility.
360
360
 
361
+ Each visualiser stage can also be chroma-keyed, constrained by a named animated
362
+ alpha/luminance track matte, faded, or use all three operations together. The
363
+ processed stage is composited over its own input before the next engine, so
364
+ projectM and Butterchurn can occupy different regions while retaining their
365
+ continuous source-time feedback. See [stage-level keying and matting](docs/visualisers.md#stage-level-chroma-key-and-track-mattes).
366
+
367
+ The same stage can apply the complete FrameRudiments visual-effect pipeline at
368
+ its source input, directly to the retained Butterchurn/projectM feedback state,
369
+ or to display output. These hooks are separate and evidence-recorded; a
370
+ feedback hook is written back for consumption by the next frame.
371
+
372
+ The [real-engine feedback smoke evidence](docs/media/visualiser-feedback-writeback.png)
373
+ shows the control frame, injected retained state and measured pixel difference
374
+ for both pinned engines. It uses synthetic imagery and is evidence of the hook,
375
+ not a claim about a campaign render.
376
+
361
377
  Version 0.1.4 preserves portrait geometry in pinned projectM legacy presets through a recorded square overscan/crop mapping; the upstream engine remains unmodified. Neutral portrait/landscape/square mapping is tested for both backends.
362
378
 
363
379
  ![Actual synthetic frame before and after both visualisers](https://raw.githubusercontent.com/kieransimkin/FrameRudiments/v0.1.4/docs/media/full-frame-comparison.png)
@@ -327,6 +327,22 @@ a browser or projectM. Feedback is sequential, and GPU output can vary by driver
327
327
  Inspect processed/encoded glyphs and safe zones; pre-effect text bounds cannot
328
328
  prove final legibility.
329
329
 
330
+ Each visualiser stage can also be chroma-keyed, constrained by a named animated
331
+ alpha/luminance track matte, faded, or use all three operations together. The
332
+ processed stage is composited over its own input before the next engine, so
333
+ projectM and Butterchurn can occupy different regions while retaining their
334
+ continuous source-time feedback. See [stage-level keying and matting](docs/visualisers.md#stage-level-chroma-key-and-track-mattes).
335
+
336
+ The same stage can apply the complete FrameRudiments visual-effect pipeline at
337
+ its source input, directly to the retained Butterchurn/projectM feedback state,
338
+ or to display output. These hooks are separate and evidence-recorded; a
339
+ feedback hook is written back for consumption by the next frame.
340
+
341
+ The [real-engine feedback smoke evidence](docs/media/visualiser-feedback-writeback.png)
342
+ shows the control frame, injected retained state and measured pixel difference
343
+ for both pinned engines. It uses synthetic imagery and is evidence of the hook,
344
+ not a claim about a campaign render.
345
+
330
346
  Version 0.1.4 preserves portrait geometry in pinned projectM legacy presets through a recorded square overscan/crop mapping; the upstream engine remains unmodified. Neutral portrait/landscape/square mapping is tested for both backends.
331
347
 
332
348
  ![Actual synthetic frame before and after both visualisers](https://raw.githubusercontent.com/kieransimkin/FrameRudiments/v0.1.4/docs/media/full-frame-comparison.png)
@@ -1,3 +1,22 @@
1
+ # FrameRudiments 0.4.8
2
+
3
+ - Butterchurn and projectM stages now support deterministic post-stage chroma
4
+ keying, edge smoothing and bounded despill.
5
+ - The complete VisualStyle effect set can run on a stage's source input,
6
+ retained feedback state, or display output. Retained-feedback treatments are
7
+ written back through Butterchurn's pinned target framebuffer and projectM's
8
+ pinned 4.2 texture-burn API before the next frame.
9
+ - A stage can use any named isolated compositor element as an alpha or
10
+ luminance track matte, with inversion and independent matte/stage opacity.
11
+ - Keying and matting compose in a documented order over the image that entered
12
+ each stage; later engines receive the already contained result while clean
13
+ routed text remains above the final chain.
14
+ - Focused pixel tests cover hard/soft keys, combined key-plus-matte output,
15
+ luminance/alpha multiplication and two-engine stage order.
16
+ - Pinned real-engine smoke evidence is included at
17
+ `docs/media/visualiser-feedback-writeback.png`, with source hashes in the
18
+ adjacent JSON receipt.
19
+
1
20
  # FrameRudiments 0.4.7
2
21
 
3
22
  - `preflight --max-frames N` composes a declared sequential prefix for expensive
@@ -0,0 +1,45 @@
1
+ {
2
+ "schema": "framerudiments.feedback-contact-sheet.v1",
3
+ "synthetic_input": true,
4
+ "purpose": "Visual evidence of retained-feedback writeback in pinned real engines.",
5
+ "output": "docs\\media\\visualiser-feedback-writeback.png",
6
+ "output_sha256": "5d35e7555196c539bf669616405f759754472661c7838e2cfe92d74c74b323d5",
7
+ "sources": [
8
+ {
9
+ "engine": "Butterchurn",
10
+ "state": "control",
11
+ "file": "butterchurn-control.png",
12
+ "sha256": "c0210c81e18d3626fddcbae69151c11246879123fef5c27a7b8647c82ffb3225"
13
+ },
14
+ {
15
+ "engine": "Butterchurn",
16
+ "state": "feedback writeback",
17
+ "file": "butterchurn-feedback-hook.png",
18
+ "sha256": "406a7a828b49775a506507b59097588e0bc05d2f63ca28de571df211732f8a2f"
19
+ },
20
+ {
21
+ "engine": "Butterchurn",
22
+ "state": "pixel difference",
23
+ "file": "butterchurn-difference.png",
24
+ "sha256": "9ac9efc47ca3eb0dff21af46aaa5e8f7f7879bb4c5f004c64ca1fe2111a6d08d"
25
+ },
26
+ {
27
+ "engine": "projectM",
28
+ "state": "control",
29
+ "file": "projectm-control.png",
30
+ "sha256": "8ff0c598bde30f1c53f7037a46142c47e9e87a7d68cc66e87871d973d19cd74f"
31
+ },
32
+ {
33
+ "engine": "projectM",
34
+ "state": "feedback writeback",
35
+ "file": "projectm-feedback-hook.png",
36
+ "sha256": "3f8c01f0e568dfe61e691ebddd7a9c714bfc515d59613a6f14bae126950c4b08"
37
+ },
38
+ {
39
+ "engine": "projectM",
40
+ "state": "pixel difference",
41
+ "file": "projectm-difference.png",
42
+ "sha256": "2eac385f46c092d80e71224567f012b108505cc79c92c5a436a033af7e0dd9fc"
43
+ }
44
+ ]
45
+ }
@@ -23,7 +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` | Ordered Butterchurn/projectM stages. Butterchurn may set a denser `mesh_width`/`mesh_height`; any stage may set a multi-stop `output_palette_grade` applied before the next stage or clean routing overlay. projectM stages may use `preset_timeline` for ordered changes at canonical source seconds through the native soft transition on one retained feedback instance. See [visualisers](visualisers.md). |
26
+ | `effects` | Ordered Butterchurn/projectM stages. Butterchurn may set a denser `mesh_width`/`mesh_height`; any stage may set `input_effects`, retained-loop `feedback_effects`, `output_effects`, a multi-stop `output_palette_grade`, and a stage-level `composite` containing chroma key, named alpha/luminance matte and opacity controls. projectM stages may use `preset_timeline` for ordered changes at canonical source seconds through the native soft transition on one retained feedback instance. See [visualisers](visualisers.md). |
27
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. |
28
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. |
29
29
 
@@ -156,7 +156,8 @@ see [full schema and examples](rich-lyric-effects.md).
156
156
  ## Optional full-frame effects
157
157
 
158
158
  The top-level `effects` list is additive to v1. See [visualisers.md](visualisers.md)
159
- for the `id`, `engine`, `preset`, `input_mix`, `seed` and `trusted_preset` fields,
159
+ for the `id`, `engine`, `preset`, `input_mix`, `seed`, `trusted_preset` and
160
+ stage-level `composite` fields,
160
161
  runtime setup and sequential feedback contract. Both engines process all existing
161
162
  layers, including native moving sprites/atlases, captions and lyric art, before
162
163
  encoding. Existing projects without `effects` keep the original composition path.
@@ -57,6 +57,111 @@ Generated test illustration; it demonstrates the colour transform rather than
57
57
  evidence from a private campaign. A video is omitted because the grade is a
58
58
  stateless per-frame colour operation with no temporal behaviour.
59
59
 
60
+ ## Stage-level chroma key and track mattes
61
+
62
+ ### Input, retained-feedback and output effect hooks
63
+
64
+ Every stage accepts three ordered arrays using the same `EffectStage` schema as
65
+ `post_effects.frame`: `input_effects`, `feedback_effects` and
66
+ `output_effects`. They support the complete validated `VisualStyle` set and
67
+ native DanceRudiment modulations.
68
+
69
+ ```json
70
+ {
71
+ "id": "cloud-feedback",
72
+ "engine": "butterchurn",
73
+ "preset": "feedback",
74
+ "input_effects": [
75
+ {"id": "source-grade", "visual": {"saturation": 0.7, "contrast": 1.1}}
76
+ ],
77
+ "feedback_effects": [
78
+ {"id": "memory-drift", "visual": {"hue_shift": 4, "blur": 0.6}}
79
+ ],
80
+ "output_effects": [
81
+ {"id": "display-finish", "visual": {"grain": 0.03, "vignette": 0.2}}
82
+ ]
83
+ }
84
+ ```
85
+
86
+ - `input_effects` changes the source texture before it is injected into that
87
+ engine's warp/composite pass. It therefore participates in feedback.
88
+ - `feedback_effects` processes the engine frame and writes those pixels back
89
+ into the engine's retained feedback target before the next frame consumes
90
+ it. Butterchurn writes to its pinned `targetFrameBuffer`; projectM uses the
91
+ pinned 4.2 `projectm_opengl_burn_texture` API. This is an actual loop-state
92
+ mutation and requires one GPU readback/upload per affected stage and frame.
93
+ - `output_effects` changes only the displayed stage result. It does not alter
94
+ retained feedback unless the same treatment is also declared under
95
+ `feedback_effects`.
96
+
97
+ The exact order is stage input injection → `input_effects` → engine render →
98
+ `feedback_effects` branch/writeback → legacy `output_palette_grade` →
99
+ `output_effects` → chroma key → track matte → stage opacity/alpha-over. The
100
+ next engine receives that composited display result. Clean routing overlays
101
+ remain after the final engine.
102
+
103
+ ![Synthetic control frames beside real Butterchurn and projectM retained-feedback writeback results](media/visualiser-feedback-writeback.png)
104
+
105
+ *Real-engine GPU smoke evidence from the pinned Butterchurn and projectM
106
+ runtimes. The magenta card is deliberately written into each engine's retained
107
+ feedback target; the following frame changes before ordinary output processing.
108
+ Inputs are synthetic and the enlarged nearest-neighbour pixels make the
109
+ difference inspectable. Source hashes are retained in the adjacent
110
+ `visualiser-feedback-writeback.json` receipt.*
111
+
112
+ Each Butterchurn or projectM stage may declare `composite`. FrameRudiments
113
+ renders the visualiser normally, optionally removes a keyed colour, multiplies
114
+ the remaining alpha by a named isolated compositor layer, then composites that
115
+ result over the image that entered the stage. Both operations can be combined:
116
+
117
+ ```json
118
+ {
119
+ "id": "coastal-light",
120
+ "engine": "projectm",
121
+ "preset": "julia",
122
+ "composite": {
123
+ "opacity": 0.82,
124
+ "chroma_key": {
125
+ "colour": "#00ff00",
126
+ "similarity": 0.10,
127
+ "smoothness": 0.08,
128
+ "despill": 0.35
129
+ },
130
+ "matte": {
131
+ "source": "layer:moving-clouds",
132
+ "mode": "luminance",
133
+ "invert": false,
134
+ "opacity": 0.9
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ `similarity` is the fully removed maximum per-channel RGB distance from the
141
+ key colour, normalised to 0..1. `smoothness` is the following soft transition
142
+ to fully opaque pixels. A zero smoothness gives a hard key. `despill` blends
143
+ near-key edge pixels toward their luminance without altering distant colours.
144
+
145
+ The matte `source` names an actual isolated compositor element such as
146
+ `layer:moving-clouds`, `background`, `scene`, `caption` or `lyric-art:0`.
147
+ Unknown names fail during composition setup. `alpha` uses its rendered alpha;
148
+ `luminance` multiplies that alpha by greyscale RGB. `invert`, matte `opacity`
149
+ and stage `opacity` are applied after keying. An invisible or gated matte
150
+ source produces an empty contribution for that frame.
151
+
152
+ Compositing happens independently after every configured stage. The next
153
+ visualiser receives the already keyed/matted result, so two engines can occupy
154
+ different spatial regions without a release-owned frame loop. The visualiser
155
+ still advances across the full sequential programme even when its visible
156
+ matte is empty; its feedback state and PCM clock therefore remain continuous.
157
+ Clean `visualiser_routing` overlays are drawn after the last stage as before.
158
+
159
+ Keying is colour removal, not evidence that a preset was authored for a green
160
+ screen. Choose a key colour demonstrated to be isolated in inspected output,
161
+ and inspect edge transparency at delivery size. Track mattes preserve the
162
+ provider's rendered motion and post effects; they do not infer a mask from a
163
+ filename or visual role.
164
+
60
165
  ## Per-element feed and clean overlay routing
61
166
 
62
167
  FrameRudiments supports an optional project-level `visualiser_routing` object.
@@ -0,0 +1,83 @@
1
+ """Build a labelled documentation image from real-engine feedback smoke outputs."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from PIL import Image, ImageDraw, ImageFont
9
+
10
+
11
+ def file_hash(path):
12
+ return hashlib.sha256(path.read_bytes()).hexdigest()
13
+
14
+
15
+ def main():
16
+ parser = argparse.ArgumentParser()
17
+ parser.add_argument("--butterchurn-dir", type=Path, required=True)
18
+ parser.add_argument("--projectm-dir", type=Path, required=True)
19
+ parser.add_argument("--output", type=Path, required=True)
20
+ args = parser.parse_args()
21
+
22
+ entries = []
23
+ for engine, folder in (
24
+ ("Butterchurn", args.butterchurn_dir),
25
+ ("projectM", args.projectm_dir),
26
+ ):
27
+ for state, filename in (
28
+ ("control", f"{engine.lower()}-control.png"),
29
+ ("feedback writeback", f"{engine.lower()}-feedback-hook.png"),
30
+ ("pixel difference", f"{engine.lower()}-difference.png"),
31
+ ):
32
+ path = folder / filename
33
+ entries.append((engine, state, path, Image.open(path).convert("RGB")))
34
+
35
+ scale = 3
36
+ tile = (96 * scale, 64 * scale)
37
+ margin, label_height, footer_height = 20, 42, 22
38
+ sheet = Image.new(
39
+ "RGB",
40
+ (
41
+ margin * 4 + tile[0] * 3,
42
+ margin * 3 + (label_height + tile[1] + footer_height) * 2,
43
+ ),
44
+ "#0b1020",
45
+ )
46
+ draw = ImageDraw.Draw(sheet)
47
+ font = ImageFont.load_default(size=16)
48
+ small = ImageFont.load_default(size=13)
49
+ for index, (engine, state, path, image) in enumerate(entries):
50
+ row, column = divmod(index, 3)
51
+ x = margin + column * (tile[0] + margin)
52
+ y = margin + row * (tile[1] + label_height + footer_height + margin)
53
+ draw.text((x, y), f"{engine} - {state}", font=font, fill="#f4f0e8")
54
+ resized = image.resize(tile, Image.Resampling.NEAREST)
55
+ sheet.paste(resized, (x, y + label_height))
56
+ draw.rectangle((x, y + label_height, x + tile[0] - 1, y + label_height + tile[1] - 1), outline="#5ce1e6", width=2)
57
+ draw.text((x, y + label_height + tile[1] + 3), path.name, font=small, fill="#9aa8c7")
58
+
59
+ args.output.parent.mkdir(parents=True, exist_ok=True)
60
+ sheet.save(args.output, optimize=True)
61
+ receipt = {
62
+ "schema": "framerudiments.feedback-contact-sheet.v1",
63
+ "synthetic_input": True,
64
+ "purpose": "Visual evidence of retained-feedback writeback in pinned real engines.",
65
+ "output": str(args.output),
66
+ "output_sha256": file_hash(args.output),
67
+ "sources": [
68
+ {
69
+ "engine": engine,
70
+ "state": state,
71
+ "file": path.name,
72
+ "sha256": file_hash(path),
73
+ }
74
+ for engine, state, path, _ in entries
75
+ ],
76
+ }
77
+ args.output.with_suffix(".json").write_text(
78
+ json.dumps(receipt, indent=2) + "\n", encoding="utf-8"
79
+ )
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
@@ -0,0 +1,117 @@
1
+ """Run opt-in real-engine feedback-writeback smoke checks.
2
+
3
+ This script needs the documented Butterchurn and projectM environment variables.
4
+ It writes tiny synthetic PNG evidence and a JSON receipt; no campaign media is used.
5
+ """
6
+
7
+ import argparse
8
+ import hashlib
9
+ import json
10
+ from array import array
11
+ from pathlib import Path
12
+ from types import SimpleNamespace
13
+
14
+ from PIL import Image, ImageChops, ImageDraw
15
+
16
+ from framerudiments.projectm_backend import ProjectMBackend
17
+ from framerudiments.visualisers import ASSETS, ButterchurnBackend
18
+
19
+
20
+ def digest(image):
21
+ return hashlib.sha256(image.tobytes()).hexdigest()
22
+
23
+
24
+ def input_card(size):
25
+ image = Image.new("RGBA", size, "#10182f")
26
+ draw = ImageDraw.Draw(image)
27
+ draw.ellipse((8, 8, size[0] - 8, size[1] - 8), fill="#ff5b87")
28
+ draw.line((0, size[1] - 1, size[0] - 1, 0), fill="#4ee9ff", width=5)
29
+ return image
30
+
31
+
32
+ def run_engine(name, factory, preset, output):
33
+ size = (96, 64)
34
+ pcm = array("f", [0.0] * 2048)
35
+ card = input_card(size)
36
+ transparent = Image.new("RGBA", size)
37
+ override = Image.new("RGBA", size, "#ff00ff")
38
+
39
+ def trial(writeback):
40
+ engine = factory(size, 60, preset, 0.72, 41, output)
41
+ try:
42
+ first = engine.process(card, SimpleNamespace(frame=0, output_seconds=0), pcm)
43
+ if writeback:
44
+ engine.set_feedback(override)
45
+ second = engine.process(
46
+ transparent, SimpleNamespace(frame=1, output_seconds=1 / 60), pcm
47
+ )
48
+ return first, second
49
+ finally:
50
+ engine.close()
51
+
52
+ first_control, control = trial(False)
53
+ first_treated, treated = trial(True)
54
+ try:
55
+ difference = ImageChops.difference(control.convert("RGB"), treated.convert("RGB"))
56
+ bbox = difference.getbbox()
57
+ if bbox is None:
58
+ raise AssertionError(f"{name} feedback writeback produced no next-frame pixel change")
59
+ control.save(output / f"{name}-control.png")
60
+ treated.save(output / f"{name}-feedback-hook.png")
61
+ difference.save(output / f"{name}-difference.png")
62
+ return {
63
+ "engine": name,
64
+ "frame0_hashes": [digest(first_control), digest(first_treated)],
65
+ "control_hash": digest(control),
66
+ "feedback_hook_hash": digest(treated),
67
+ "difference_bbox": list(bbox),
68
+ "difference_extrema": [list(value) for value in difference.getextrema()],
69
+ }
70
+ finally:
71
+ del first_control, first_treated, control, treated
72
+
73
+
74
+ def write_receipt(path, receipt):
75
+ path.write_text(json.dumps(receipt, indent=2) + "\n", encoding="utf-8")
76
+
77
+
78
+ def main():
79
+ parser = argparse.ArgumentParser()
80
+ parser.add_argument("--output", type=Path, required=True)
81
+ parser.add_argument(
82
+ "--engine",
83
+ choices=("all", "butterchurn", "projectm"),
84
+ default="all",
85
+ help="Run both engines or a single bounded recovery trial.",
86
+ )
87
+ args = parser.parse_args()
88
+ args.output.mkdir(parents=True, exist_ok=False)
89
+ receipt = {
90
+ "schema": "framerudiments.feedback-writeback-smoke.v1",
91
+ "synthetic": True,
92
+ "engines": [],
93
+ }
94
+ if args.engine in {"all", "butterchurn"}:
95
+ receipt["engines"].append(
96
+ run_engine(
97
+ "butterchurn",
98
+ ButterchurnBackend,
99
+ ASSETS / "feedback.json",
100
+ args.output,
101
+ )
102
+ )
103
+ write_receipt(args.output / "receipt.json", receipt)
104
+ if args.engine in {"all", "projectm"}:
105
+ receipt["engines"].append(
106
+ run_engine(
107
+ "projectm",
108
+ ProjectMBackend,
109
+ ASSETS / "feedback.milk",
110
+ args.output,
111
+ )
112
+ )
113
+ write_receipt(args.output / "receipt.json", receipt)
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "framerudiments"
7
- version = "0.4.7"
7
+ version = "0.4.8"
8
8
  description = "Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence."
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  """Offline frame composition on a traceable musical clock."""
2
2
 
3
- __version__ = "0.4.7"
3
+ __version__ = "0.4.8"
4
4
 
5
5
  from .project import Project
6
6
  from .render import render, preflight
@@ -16,7 +16,7 @@ from .timeline import EventTrack
16
16
  from .word_timing import WordTrack
17
17
  from .rich_subtitles import RichTextAppearance
18
18
  from .lyric_indicator import WordIndicator
19
- from .visualisers import effect_specs
19
+ from .visualisers import effect_specs, VisualiserEffectHooks
20
20
  from .scoped_effects import CompositingEffects
21
21
  from .visualiser_routing import VisualiserLayerRouting
22
22
  from .layer_masks import LayerMasks
@@ -378,6 +378,15 @@ class Compositor:
378
378
  targets.add("word-caption")
379
379
  if self.word_indicator:
380
380
  targets.add("word-indicator")
381
+ matte_sources = {
382
+ spec["composite"]["matte"]["source"]
383
+ for spec in self.effect_specs
384
+ if spec.get("composite") and spec["composite"].get("matte")
385
+ }
386
+ unknown_mattes = matte_sources - targets
387
+ if unknown_mattes:
388
+ raise ValueError(f"Unknown visualiser matte sources: {sorted(unknown_mattes)}")
389
+ self.visualiser_hooks = VisualiserEffectHooks(project, self.effect_specs, self.context)
381
390
  self.post_pipeline = CompositingEffects(
382
391
  project, project.data.get("post_effects", {}), self.context, valid_elements=targets
383
392
  )
@@ -386,7 +395,9 @@ class Compositor:
386
395
  self.layer_masks = LayerMasks(project.data.get("layer_masks", {}), mask_order)
387
396
  self.layer_animations = LayerAnimations(project.data.get("layer_animations", {}), mask_order)
388
397
  self.layer_displacement = LayerDisplacement(project.data.get("layer_displacement", {}), mask_order)
389
- self._isolate_stage_injections = bool(project.data.get("visualiser_stage_injections"))
398
+ self._isolate_stage_injections = bool(
399
+ project.data.get("visualiser_stage_injections") or matte_sources
400
+ )
390
401
  self.visualiser_routing = None
391
402
  if "visualiser_routing" in project.data:
392
403
  if not self.effect_specs:
@@ -27,6 +27,7 @@ class ProjectMBackend:
27
27
  self.glfw, self.gl = glfw, GL
28
28
  self.window = self.handle = None
29
29
  self.texture = self.fbo = self.output_texture = None
30
+ self.feedback_override = None
30
31
  self.dll_dirs = []
31
32
  self.registered = False
32
33
  path = Path(os.environ.get("FRAMERUDIMENTS_PROJECTM_LIBRARY", ""))
@@ -225,7 +226,33 @@ class ProjectMBackend:
225
226
  gl = self.gl
226
227
  self.glfw.make_context_current(self.window)
227
228
  self.lib.projectm_set_log_callback(self.log_callback, True, None)
229
+ if self.feedback_override is not None:
230
+ self._burn(self.feedback_override, 1.0)
231
+ self.feedback_override = None
228
232
  frame = image.convert("RGBA")
233
+ self._burn(frame, self.mix)
234
+ self.lib.projectm_set_frame_time(self.handle, context.output_seconds)
235
+ audio = (C.c_float * len(pcm)).from_buffer_copy(pcm)
236
+ self.lib.projectm_pcm_add_float(self.handle, audio, len(pcm) // 2, 2)
237
+ self.lib.projectm_opengl_render_frame_fbo(self.handle, int(self.fbo))
238
+ self._check()
239
+ actual = self.lib.projectm_get_last_frame_time(self.handle)
240
+ if abs(actual - context.output_seconds) > 1e-9:
241
+ raise RuntimeError("projectM did not honour the offline frame clock")
242
+ if not capture:
243
+ return None
244
+ gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.fbo)
245
+ gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
246
+ raw = gl.glReadPixels(0, 0, *self.size, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)
247
+ frame = Image.frombytes("RGBA", self.size, raw).transpose(Image.Transpose.FLIP_TOP_BOTTOM)
248
+ return frame.crop((self.crop_left, 0, self.crop_left + self.frame_size[0], self.frame_size[1]))
249
+
250
+ def _burn(self, image, opacity):
251
+ """Burn an RGBA source into projectM's retained target through its public 4.2 API."""
252
+ gl = self.gl
253
+ frame = image.convert("RGBA")
254
+ if frame.size != self.frame_size:
255
+ raise ValueError("projectM feedback hook needs frame-sized RGBA")
229
256
  if self.size != self.frame_size:
230
257
  blank = (0, 0, 0, 0) if frame.getchannel("A").getextrema()[0] < 255 else "black"
231
258
  canvas = Image.new("RGBA", self.size, blank)
@@ -233,7 +260,7 @@ class ProjectMBackend:
233
260
  frame = canvas
234
261
  # Preserve source alpha so an unfed layer cannot paint black into feedback.
235
262
  # Fully opaque legacy inputs remain numerically unchanged.
236
- frame.putalpha(frame.getchannel("A").point(lambda a: round(a * self.mix)))
263
+ frame.putalpha(frame.getchannel("A").point(lambda a: round(a * opacity)))
237
264
  pixels = frame.transpose(Image.Transpose.FLIP_TOP_BOTTOM).tobytes()
238
265
  gl.glActiveTexture(gl.GL_TEXTURE0)
239
266
  gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture)
@@ -243,22 +270,12 @@ class ProjectMBackend:
243
270
  gl.glEnable(gl.GL_BLEND)
244
271
  gl.glBlendEquation(gl.GL_FUNC_ADD)
245
272
  gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
246
- self.lib.projectm_set_frame_time(self.handle, context.output_seconds)
247
273
  self.lib.projectm_opengl_burn_texture(self.handle, int(self.texture), 0, 0, *self.size)
248
- audio = (C.c_float * len(pcm)).from_buffer_copy(pcm)
249
- self.lib.projectm_pcm_add_float(self.handle, audio, len(pcm) // 2, 2)
250
- self.lib.projectm_opengl_render_frame_fbo(self.handle, int(self.fbo))
251
- self._check()
252
- actual = self.lib.projectm_get_last_frame_time(self.handle)
253
- if abs(actual - context.output_seconds) > 1e-9:
254
- raise RuntimeError("projectM did not honour the offline frame clock")
255
- if not capture:
256
- return None
257
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.fbo)
258
- gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
259
- raw = gl.glReadPixels(0, 0, *self.size, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)
260
- frame = Image.frombytes("RGBA", self.size, raw).transpose(Image.Transpose.FLIP_TOP_BOTTOM)
261
- return frame.crop((self.crop_left, 0, self.crop_left + self.frame_size[0], self.frame_size[1]))
274
+
275
+ def set_feedback(self, image):
276
+ if image.mode != "RGBA" or image.size != self.frame_size:
277
+ raise ValueError("projectM feedback hook needs frame-sized RGBA")
278
+ self.feedback_override = image.copy()
262
279
 
263
280
  def close(self):
264
281
  global _users
@@ -142,7 +142,14 @@ def preflight(
142
142
  effects = None
143
143
  try:
144
144
  effects = (
145
- EffectChain(p, compositor.effect_specs, ffmpeg, out, routing=compositor.visualiser_routing)
145
+ EffectChain(
146
+ p,
147
+ compositor.effect_specs,
148
+ ffmpeg,
149
+ out,
150
+ routing=compositor.visualiser_routing,
151
+ hooks=compositor.visualiser_hooks,
152
+ )
146
153
  if compositor.effect_specs
147
154
  else None
148
155
  )
@@ -201,7 +208,14 @@ def render(project, output, *, ffmpeg=None, ffprobe=None, scene=None, renderers=
201
208
  effects = None
202
209
  try:
203
210
  effects = (
204
- EffectChain(p, compositor.effect_specs, ffmpeg, out, routing=compositor.visualiser_routing)
211
+ EffectChain(
212
+ p,
213
+ compositor.effect_specs,
214
+ ffmpeg,
215
+ out,
216
+ routing=compositor.visualiser_routing,
217
+ hooks=compositor.visualiser_hooks,
218
+ )
205
219
  if compositor.effect_specs
206
220
  else None
207
221
  )
@@ -84,13 +84,19 @@ async function initialise(config) {
84
84
  gl.activeTexture(gl.TEXTURE0);
85
85
  const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D), oldSampler = gl.getParameter(gl.SAMPLER_BINDING);
86
86
  gl.bindSampler(0, null); gl.bindTexture(gl.TEXTURE_2D, texture);
87
- if (message.rgba) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, config.width, config.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, decode(message.rgba));
88
- // target becomes prev on render(): source pixels enter the real warp pass.
89
- gl.bindFramebuffer(gl.FRAMEBUFFER, r.targetFrameBuffer);
90
- gl.viewport(0, 0, r.texsizeX, r.texsizeY);
91
- gl.useProgram(program); gl.bindVertexArray(vao); gl.uniform1i(source, 0); gl.uniform1f(amount, config.mix);
92
- gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
93
- gl.drawArrays(gl.TRIANGLES, 0, 3);
87
+ const inject = (rgba, opacity) => {
88
+ if (rgba) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, config.width, config.height, 0,
89
+ gl.RGBA, gl.UNSIGNED_BYTE, decode(rgba));
90
+ gl.bindFramebuffer(gl.FRAMEBUFFER, r.targetFrameBuffer);
91
+ gl.viewport(0, 0, r.texsizeX, r.texsizeY);
92
+ gl.useProgram(program); gl.bindVertexArray(vao); gl.uniform1i(source, 0); gl.uniform1f(amount, opacity);
93
+ gl.enable(gl.BLEND); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
94
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
95
+ };
96
+ // A feedback hook replaces the retained target before this frame consumes
97
+ // it. The ordinary source is then injected with its independent input mix.
98
+ if (message.feedback_rgba) inject(message.feedback_rgba, 1);
99
+ inject(message.rgba, config.mix);
94
100
  gl.bindVertexArray(state.vao); gl.useProgram(state.program);
95
101
  gl.bindTexture(gl.TEXTURE_2D, oldTexture); gl.bindSampler(0, oldSampler); gl.activeTexture(state.active);
96
102
  gl.bindFramebuffer(gl.FRAMEBUFFER, state.fbo); gl.viewport(...state.viewport);