framerudiments 0.4.4__tar.gz → 0.4.6__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 (261) hide show
  1. {framerudiments-0.4.4/src/framerudiments.egg-info → framerudiments-0.4.6}/PKG-INFO +28 -1
  2. {framerudiments-0.4.4 → framerudiments-0.4.6}/README.md +27 -0
  3. {framerudiments-0.4.4 → framerudiments-0.4.6}/RELEASE_NOTES.md +50 -0
  4. framerudiments-0.4.6/docs/media/palette-grade-comparison.png +0 -0
  5. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/project-format.md +4 -1
  6. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/rich-lyric-effects.md +9 -1
  7. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/visualiser-routing.md +8 -0
  8. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/visualisers.md +62 -0
  9. {framerudiments-0.4.4 → framerudiments-0.4.6}/pyproject.toml +1 -1
  10. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/__init__.py +1 -1
  11. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/compositor.py +5 -0
  12. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/encoding.py +15 -1
  13. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/lyric_indicator.py +46 -2
  14. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/projectm_backend.py +19 -0
  15. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/timeline.py +15 -0
  16. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/butterchurn-page.js +3 -2
  17. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_routing.py +26 -4
  18. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualisers.py +174 -15
  19. {framerudiments-0.4.4 → framerudiments-0.4.6/src/framerudiments.egg-info}/PKG-INFO +28 -1
  20. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments.egg-info/SOURCES.txt +2 -0
  21. framerudiments-0.4.6/tests/test_encoding.py +33 -0
  22. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_rich_subtitles.py +24 -0
  23. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_visualiser_routing.py +109 -1
  24. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_visualisers.py +90 -2
  25. {framerudiments-0.4.4 → framerudiments-0.4.6}/AGENTS.md +0 -0
  26. {framerudiments-0.4.4 → framerudiments-0.4.6}/LICENSE +0 -0
  27. {framerudiments-0.4.4 → framerudiments-0.4.6}/MANIFEST.in +0 -0
  28. {framerudiments-0.4.4 → framerudiments-0.4.6}/analysis-releases.json +0 -0
  29. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/ECOSYSTEM.md +0 -0
  30. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/FrameRudiments-PATCH_WORKFLOW.md +0 -0
  31. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/annular-modulation.md +0 -0
  32. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/authoring-workspace.md +0 -0
  33. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/beat-alignment.md +0 -0
  34. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/controlled-audio-response.md +0 -0
  35. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/expanded-effects.md +0 -0
  36. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/extraction.md +0 -0
  37. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/full-preset-analysis.md +0 -0
  38. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/hybrid-preset-analysis.md +0 -0
  39. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/keyframed-mattes.md +0 -0
  40. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/beat-alignment-evidence.json +0 -0
  41. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/beat-alignment.mp4 +0 -0
  42. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/beat-alignment.png +0 -0
  43. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/demo-transcript.md +0 -0
  44. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/effects-pipeline.svg +0 -0
  45. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/full-frame-comparison.png +0 -0
  46. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/full-frame-demo.mp4 +0 -0
  47. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/media-evidence.json +0 -0
  48. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/natural-materials.mp4 +0 -0
  49. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/natural-materials.png +0 -0
  50. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/preset-test-cards.png +0 -0
  51. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/rangoli-transcript.md +0 -0
  52. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/rangoli.mp4 +0 -0
  53. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/rangoli.png +0 -0
  54. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/test-card-inputs/colour-bars.png +0 -0
  55. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/test-card-inputs/grid-rings.png +0 -0
  56. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/media/test-card-inputs/text-card.png +0 -0
  57. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/multi-stem-modulation.md +0 -0
  58. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/music-reactive-3d.md +0 -0
  59. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/natural-materials.md +0 -0
  60. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/particle-smoke.md +0 -0
  61. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/phase3-compositing.md +0 -0
  62. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/preset-behaviour.md +0 -0
  63. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/preset-profiling.md +0 -0
  64. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/projectm-local-runtime.md +0 -0
  65. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/quality-effects.md +0 -0
  66. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/rangoli.md +0 -0
  67. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/rudiment-renderers.md +0 -0
  68. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/scoped-post-effects.md +0 -0
  69. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/solid-spatial-renderers.md +0 -0
  70. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/spatial-renderers.md +0 -0
  71. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/spatiotemporal-supersampling.md +0 -0
  72. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/spectrum.md +0 -0
  73. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/sprite-visualisations.md +0 -0
  74. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/stem-sources-and-masks.md +0 -0
  75. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/variable-bpm-word-subtitles.md +0 -0
  76. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/visualisation-atlas.md +0 -0
  77. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/visualisation-coverage-matrix.md +0 -0
  78. {framerudiments-0.4.4 → framerudiments-0.4.6}/docs/workflow-media.md +0 -0
  79. {framerudiments-0.4.4 → framerudiments-0.4.6}/integration/test_fire_volume_gpu.py +0 -0
  80. {framerudiments-0.4.4 → framerudiments-0.4.6}/integration/test_music_spatial_gpu.py +0 -0
  81. {framerudiments-0.4.4 → framerudiments-0.4.6}/integration/test_preset_analysis.py +0 -0
  82. {framerudiments-0.4.4 → framerudiments-0.4.6}/integration/test_released_preset_analysis.py +0 -0
  83. {framerudiments-0.4.4 → framerudiments-0.4.6}/integration/test_visualisers_gpu.py +0 -0
  84. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/Run-ControlledPresetAnalysis.ps1 +0 -0
  85. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/Run-FullPresetAnalysis.ps1 +0 -0
  86. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/Run-HybridPresetAnalysis.ps1 +0 -0
  87. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/Run-PresetBehaviourAnalysis.ps1 +0 -0
  88. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/authoring_preview.py +0 -0
  89. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/bake_mantaflow_fire.py +0 -0
  90. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/build_sprite_assets.py +0 -0
  91. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/check_projectm_runtime.py +0 -0
  92. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/check_release_packages.py +0 -0
  93. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/controlled_preset_analysis.py +0 -0
  94. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/ensure_projectm_runtime.py +0 -0
  95. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/full_preset_analysis.py +0 -0
  96. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_particle_gallery.py +0 -0
  97. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_post_effect_gallery.py +0 -0
  98. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_quality_gallery.py +0 -0
  99. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_rich_lyric_gallery.py +0 -0
  100. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_sprite_gallery.py +0 -0
  101. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_supersampling_gallery.py +0 -0
  102. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_timing_gallery.py +0 -0
  103. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_visualisation_gallery.py +0 -0
  104. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/generate_visualiser_routing_gallery.py +0 -0
  105. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/install_analysis_releases.py +0 -0
  106. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/prepare_fire_atlas.py +0 -0
  107. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/prepare_pixelcue_taggers.py +0 -0
  108. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/preset_behaviour_pipeline.py +0 -0
  109. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/profile_visualisation_coverage.py +0 -0
  110. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/profile_visualisers.py +0 -0
  111. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/projectm_dll_diagnostics.py +0 -0
  112. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/render_natural_material_demo.py +0 -0
  113. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/repair_projectm_mingw.py +0 -0
  114. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/select_projectm_generator.py +0 -0
  115. {framerudiments-0.4.4 → framerudiments-0.4.6}/scripts/validate_patch.py +0 -0
  116. {framerudiments-0.4.4 → framerudiments-0.4.6}/setup.cfg +0 -0
  117. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/__main__.py +0 -0
  118. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/advanced_renderers.py +0 -0
  119. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/atlas.py +0 -0
  120. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/audio_probes.py +0 -0
  121. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/beat_alignment.py +0 -0
  122. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/beat_timeline.py +0 -0
  123. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/choreography.py +0 -0
  124. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/circular_renderers.py +0 -0
  125. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/cli.py +0 -0
  126. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/controlled_profiling.py +0 -0
  127. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/demo.py +0 -0
  128. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/effect_catalogue.py +0 -0
  129. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/effect_renderers.py +0 -0
  130. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/effects.py +0 -0
  131. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_assets/mantaflow/NOTICE.md +0 -0
  132. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_assets/mantaflow/atlas.json +0 -0
  133. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_assets/mantaflow/flames.png +0 -0
  134. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_assets/marburg/NOTICE.md +0 -0
  135. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_assets/marburg/atlas.json +0 -0
  136. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_assets/marburg/flames.png +0 -0
  137. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_assets.py +0 -0
  138. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_fluid.py +0 -0
  139. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_renderer.py +0 -0
  140. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/fire_volume.py +0 -0
  141. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/flight.py +0 -0
  142. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/gpu_spatial_backend.py +0 -0
  143. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/io.py +0 -0
  144. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/keyframes.py +0 -0
  145. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/layer_animation.py +0 -0
  146. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/layer_displacement.py +0 -0
  147. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/layer_masks.py +0 -0
  148. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/modulation.py +0 -0
  149. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/motion.py +0 -0
  150. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/music_spatial_renderers.py +0 -0
  151. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/natural_materials.py +0 -0
  152. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/output_quality.py +0 -0
  153. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/particle_engine.py +0 -0
  154. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/post_effects.py +0 -0
  155. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/preset_library.py +0 -0
  156. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/preset_profiling.py +0 -0
  157. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/profile_analysis.py +0 -0
  158. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/project.py +0 -0
  159. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/quality_effects.py +0 -0
  160. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/rangoli_renderer.py +0 -0
  161. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/render.py +0 -0
  162. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/rich_subtitles.py +0 -0
  163. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/rudiment_renderers.py +0 -0
  164. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/scoped_effects.py +0 -0
  165. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/solid_spatial_renderers.py +0 -0
  166. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/spatial_renderers.py +0 -0
  167. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/spectrum.py +0 -0
  168. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/arcade.png +0 -0
  169. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/crystal.png +0 -0
  170. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/fire.png +0 -0
  171. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/generated-neon.png +0 -0
  172. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/glass.png +0 -0
  173. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/hud.png +0 -0
  174. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/ice.png +0 -0
  175. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/lcd-amber.png +0 -0
  176. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/lcd-green.png +0 -0
  177. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/lcd.png +0 -0
  178. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/led.png +0 -0
  179. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/manifest.json +0 -0
  180. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/metal.png +0 -0
  181. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/mono.png +0 -0
  182. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/neon.png +0 -0
  183. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/pixel.png +0 -0
  184. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/plasma.png +0 -0
  185. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_assets/vfd.png +0 -0
  186. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_catalogue.py +0 -0
  187. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/sprite_renderers.py +0 -0
  188. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/stem_blend.py +0 -0
  189. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/stemlab.py +0 -0
  190. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/subtitles.py +0 -0
  191. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/super_sampling.py +0 -0
  192. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/terrain_field.py +0 -0
  193. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visual_style.py +0 -0
  194. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualisation_catalogue.py +0 -0
  195. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/butterchurn-worker.mjs +0 -0
  196. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/feedback.json +0 -0
  197. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/feedback.milk +0 -0
  198. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/julia.json +0 -0
  199. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/julia.milk +0 -0
  200. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/kaleidoscope.json +0 -0
  201. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/kaleidoscope.milk +0 -0
  202. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/package-lock.json +0 -0
  203. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/package.json +0 -0
  204. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_assets/rangoli-background.milk +0 -0
  205. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/visualiser_setup.py +0 -0
  206. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments/word_timing.py +0 -0
  207. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments.egg-info/dependency_links.txt +0 -0
  208. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments.egg-info/entry_points.txt +0 -0
  209. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments.egg-info/requires.txt +0 -0
  210. {framerudiments-0.4.4 → framerudiments-0.4.6}/src/framerudiments.egg-info/top_level.txt +0 -0
  211. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/conftest.py +0 -0
  212. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_adapters.py +0 -0
  213. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_advanced_renderers.py +0 -0
  214. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_authoring_preview.py +0 -0
  215. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_beat_alignment.py +0 -0
  216. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_behaviour_pipeline.py +0 -0
  217. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_checkpoint_batches.py +0 -0
  218. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_controlled_audio.py +0 -0
  219. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_coverage_matrix.py +0 -0
  220. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_default_antialias.py +0 -0
  221. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_effect_batch.py +0 -0
  222. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_extracted_helpers.py +0 -0
  223. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_fire_methods.py +0 -0
  224. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_flight.py +0 -0
  225. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_full_preset_analysis.py +0 -0
  226. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_gpu_ci_isolation.py +0 -0
  227. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_hybrid_launcher.py +0 -0
  228. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_keyframes.py +0 -0
  229. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_layer_masks.py +0 -0
  230. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_lyrics.py +0 -0
  231. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_music_spatial_pipeline.py +0 -0
  232. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_music_spatial_renderers.py +0 -0
  233. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_natural_materials.py +0 -0
  234. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_particle_engine.py +0 -0
  235. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_patch_validation.py +0 -0
  236. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_phase3_animation.py +0 -0
  237. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_phase3_gpu_injection.py +0 -0
  238. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_pipeline.py +0 -0
  239. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_preset_profiling.py +0 -0
  240. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_projectm_dll_diagnostics.py +0 -0
  241. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_projectm_generator.py +0 -0
  242. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_projectm_runtime_preflight.py +0 -0
  243. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_pytest_temp_isolation.py +0 -0
  244. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_quality_effects.py +0 -0
  245. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_rangoli_renderer.py +0 -0
  246. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_release_metadata.py +0 -0
  247. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_repair_projectm_mingw.py +0 -0
  248. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_rudiment_renderers.py +0 -0
  249. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_scoped_effects.py +0 -0
  250. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_solid_spatial_integration.py +0 -0
  251. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_solid_spatial_renderers.py +0 -0
  252. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_spatial_renderers.py +0 -0
  253. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_spectrum.py +0 -0
  254. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_sprite_renderers.py +0 -0
  255. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_stem_blend.py +0 -0
  256. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_stem_source_integration.py +0 -0
  257. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_supersampling.py +0 -0
  258. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_timeline.py +0 -0
  259. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_variable_bpm_words.py +0 -0
  260. {framerudiments-0.4.4 → framerudiments-0.4.6}/tests/test_visualisation_modulation.py +0 -0
  261. {framerudiments-0.4.4 → framerudiments-0.4.6}/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.4
3
+ Version: 0.4.6
4
4
  Summary: Deterministic Shorts and lyric visualisers using DanceRudiments and StemLab evidence.
5
5
  Author: Kieran Simkin
6
6
  License-Expression: MIT
@@ -31,6 +31,31 @@ Dynamic: license-file
31
31
 
32
32
  # FrameRudiments
33
33
 
34
+ ## Complete x264 preset range
35
+
36
+ Version 0.4.6 accepts every named libx264 preset from `ultrafast` through
37
+ `placebo`, including the previously rejected `slower` and `veryslow` delivery
38
+ profiles. The selected preset and CRF remain explicit render arguments and are
39
+ retained in the encode command and manifest evidence. A stronger preset can
40
+ improve compression efficiency; it does not raise project resolution, mesh
41
+ density, frame rate or source sampling on its own.
42
+
43
+ The existing encoded demonstrations below remain representative because this
44
+ change alters compression controls rather than composition or motion. No new
45
+ image or video was generated solely to illustrate byte-level encoder effort.
46
+
47
+ ## Palette-controlled high-detail feedback
48
+
49
+ Version 0.4.4 adds optional Butterchurn mesh dimensions and a deterministic
50
+ multi-stop palette grade after each full-frame visualiser stage. This lets a
51
+ 1080p visualiser retain more spatial detail and conform feedback colours to a
52
+ campaign palette while clean routed overlays keep their authored appearance.
53
+ See [full-frame visualisers](docs/visualisers.md).
54
+
55
+ ![Generated test illustration comparing ungraded and palette-graded output](docs/media/palette-grade-comparison.png)
56
+
57
+ Generated test illustration; no private campaign media is included.
58
+
34
59
  ## Five fire methods and ice
35
60
 
36
61
  Version 0.4.3 adds evidence-recorded three-stop fire palettes, an optional
@@ -204,6 +229,8 @@ text. Existing plain text remains untouched unless configured. Set `word_indicat
204
229
  native 1-dimensional DanceRudiment-driven bouncing sprite that tracks the active word of
205
230
  the full line. Its particle field can be continuous, word/cue-triggered, or hybrid;
206
231
  trigger windows independently change colour, density, size, glow and burst count.
232
+ An optional between-word travel arc rises before the next timestamp and lands
233
+ exactly on its encoded word frame.
207
234
  See [rich lyric reference](docs/rich-lyric-effects.md) and the
208
235
  [CI-generated demo](https://kieransimkin.github.io/FrameRudiments/rich-lyrics/).
209
236
 
@@ -1,5 +1,30 @@
1
1
  # FrameRudiments
2
2
 
3
+ ## Complete x264 preset range
4
+
5
+ Version 0.4.6 accepts every named libx264 preset from `ultrafast` through
6
+ `placebo`, including the previously rejected `slower` and `veryslow` delivery
7
+ profiles. The selected preset and CRF remain explicit render arguments and are
8
+ retained in the encode command and manifest evidence. A stronger preset can
9
+ improve compression efficiency; it does not raise project resolution, mesh
10
+ density, frame rate or source sampling on its own.
11
+
12
+ The existing encoded demonstrations below remain representative because this
13
+ change alters compression controls rather than composition or motion. No new
14
+ image or video was generated solely to illustrate byte-level encoder effort.
15
+
16
+ ## Palette-controlled high-detail feedback
17
+
18
+ Version 0.4.4 adds optional Butterchurn mesh dimensions and a deterministic
19
+ multi-stop palette grade after each full-frame visualiser stage. This lets a
20
+ 1080p visualiser retain more spatial detail and conform feedback colours to a
21
+ campaign palette while clean routed overlays keep their authored appearance.
22
+ See [full-frame visualisers](docs/visualisers.md).
23
+
24
+ ![Generated test illustration comparing ungraded and palette-graded output](docs/media/palette-grade-comparison.png)
25
+
26
+ Generated test illustration; no private campaign media is included.
27
+
3
28
  ## Five fire methods and ice
4
29
 
5
30
  Version 0.4.3 adds evidence-recorded three-stop fire palettes, an optional
@@ -173,6 +198,8 @@ text. Existing plain text remains untouched unless configured. Set `word_indicat
173
198
  native 1-dimensional DanceRudiment-driven bouncing sprite that tracks the active word of
174
199
  the full line. Its particle field can be continuous, word/cue-triggered, or hybrid;
175
200
  trigger windows independently change colour, density, size, glow and burst count.
201
+ An optional between-word travel arc rises before the next timestamp and lands
202
+ exactly on its encoded word frame.
176
203
  See [rich lyric reference](docs/rich-lyric-effects.md) and the
177
204
  [CI-generated demo](https://kieransimkin.github.io/FrameRudiments/rich-lyrics/).
178
205
 
@@ -1,3 +1,53 @@
1
+ # FrameRudiments 0.4.6
2
+
3
+ - Encoding now accepts the complete named libx264 preset range: `ultrafast`,
4
+ `superfast`, `veryfast`, `faster`, `fast`, `medium`, `slow`, `slower`,
5
+ `veryslow` and `placebo`.
6
+ - Focused regression coverage verifies every accepted preset reaches the exact
7
+ FFmpeg command and that unknown values still fail closed.
8
+ - Linux CI, gallery and release jobs now pin Ubuntu 24.04 so runner-image
9
+ migrations cannot silently change the release environment mid-validation.
10
+ - Linux render dependencies use one shared installer with bounded timeouts and
11
+ retries, preventing a stalled package mirror from hanging a release forever.
12
+ - Preset strength remains separate from composition resolution, visualiser
13
+ mesh density, frame rate and source sampling. Existing public demonstration
14
+ media therefore remains applicable; no new visual was generated for this
15
+ command-validation-only change.
16
+
17
+ # FrameRudiments 0.4.5
18
+
19
+ - Word indicators can now travel between encoded word hits with a configurable
20
+ arc and land on the next word's authoritative frame, while retaining the
21
+ existing after-hit mode.
22
+ - projectM stages accept source-time preset timelines and perform bounded soft
23
+ transitions on the existing feedback instance, with every switch recorded in
24
+ render evidence.
25
+ - Lyric layers can feed the visualiser only during their final bounded pips when
26
+ a real blank gap follows, allowing the departing line to dissolve into the
27
+ background without contaminating immediate line-to-line changes.
28
+ - Project-format and visualiser documentation describe the new contracts;
29
+ focused regression coverage preserves clock, preset trust, routing and
30
+ evidence behaviour. Private campaign artwork and audio remain outside the
31
+ distribution.
32
+
33
+ ## High-detail, palette-controlled visualiser output
34
+
35
+ - Butterchurn stages accept bounded `mesh_width` and `mesh_height` values. The
36
+ existing 64×48 mesh remains the default; projects can request additional
37
+ spatial detail without changing their encoded dimensions or frame rate.
38
+ - Any Butterchurn or projectM stage can apply `output_palette_grade` after the
39
+ engine output and before the next stage or clean routing overlay. Two to
40
+ sixteen ordered CSS colours define a deterministic luminance palette; alpha,
41
+ PCM, feedback state and the source clock remain unchanged.
42
+ - The exact mesh and grade configuration is retained in plan and manifest
43
+ evidence. Focused validation covers schema bounds, deterministic pixels,
44
+ alpha preservation, stage order and routed overlays.
45
+
46
+ ![Generated comparison of ungraded and palette-graded synthetic output](docs/media/palette-grade-comparison.png)
47
+
48
+ Generated test illustration; no private campaign media. A video is omitted
49
+ because this stateless grade adds no temporal behaviour to demonstrate.
50
+
1
51
  # FrameRudiments 0.4.4
2
52
 
3
53
  - 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` | 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
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; see [full schema and examples](rich-lyric-effects.md).
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 to the next word. Selecting `bounce.axis` projects one native
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
@@ -22,9 +22,71 @@ Original paired presets are `feedback` (audio-reactive swirl/echo),
22
22
  distortion of the frame's texture coordinates). They are examples, not a claim
23
23
  that two independent MilkDrop implementations produce identical pixels.
24
24
 
25
+ Butterchurn stages may set `mesh_width` (16–256) and `mesh_height` (12–256).
26
+ The default remains 64×48 for pixel compatibility. A denser mesh such as
27
+ 128×72 gives the warp field more spatial detail at 1920×1080, but it does not
28
+ change the encoded dimensions or frame rate and can increase GPU cost.
29
+
30
+ Each stage may also apply a deterministic multi-stop `output_palette_grade`
31
+ after its visualiser output and before the next stage or the clean routing
32
+ overlay:
33
+
34
+ ```json
35
+ {
36
+ "id": "clay-feedback",
37
+ "engine": "butterchurn",
38
+ "preset": "feedback",
39
+ "mesh_width": 128,
40
+ "mesh_height": 72,
41
+ "output_palette_grade": {
42
+ "stops": ["#100b19", "#171b37", "#572417", "#8a3c25", "#c49986"],
43
+ "amount": 0.85
44
+ }
45
+ }
46
+ ```
47
+
48
+ The grade maps output luminance through 2–16 ordered CSS colours in
49
+ sRGB, blends that result with the original by `amount` (0–1), and preserves
50
+ alpha. It does not change PCM, motion, feedback state or the source clock.
51
+ When `visualiser_routing` keeps lyrics or sprites as a clean overlay, that
52
+ overlay is composited after the grade and retains its authored colours.
53
+
54
+ ![Generated test illustration comparing ungraded and multi-stop graded output](media/palette-grade-comparison.png)
55
+
56
+ Generated test illustration; it demonstrates the colour transform rather than
57
+ evidence from a private campaign. A video is omitted because the grade is a
58
+ stateless per-frame colour operation with no temporal behaviour.
59
+
25
60
  ## Per-element feed and clean overlay routing
26
61
 
27
62
  FrameRudiments supports an optional project-level `visualiser_routing` object.
63
+
64
+ ## Source-timed projectM preset changes
65
+
66
+ A projectM stage may retain one feedback instance while changing presets at
67
+ canonical source-audio times. The native projectM soft transition blends the
68
+ old and new preset; no frame is skipped and the existing PCM/output clock stays
69
+ authoritative.
70
+
71
+ ```json
72
+ {
73
+ "id": "section-visualiser",
74
+ "engine": "projectm",
75
+ "preset": "feedback",
76
+ "preset_timeline": [
77
+ {"at_source_seconds": 62.46, "preset": "kaleidoscope", "transition_seconds": 0.42},
78
+ {"at_source_seconds": 128.60, "preset": "julia", "transition_seconds": 0.42}
79
+ ]
80
+ }
81
+ ```
82
+
83
+ Times are measured in the unchanged source recording. Entries must be strictly
84
+ increasing. Built-in names retain their package hashes; external `.milk` files
85
+ still require `trusted_preset: true` on the individual timeline entry. Timeline
86
+ switches currently use projectM because its pinned native API provides an
87
+ auditable soft-cut transition on the same renderer instance. The manifest
88
+ records the scheduled boundary, actual frame/source time, preset hash and blend
89
+ duration.
28
90
  It controls the **input RGBA texture** entering the first Butterchurn or
29
91
  projectM stage and independently decides which original layers are composited
30
92
  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.4"
7
+ version = "0.4.6"
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.4"
3
+ __version__ = "0.4.6"
4
4
 
5
5
  from .project import Project
6
6
  from .render import render, preflight
@@ -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):
@@ -8,6 +8,20 @@ import shutil
8
8
  import subprocess
9
9
 
10
10
 
11
+ X264_PRESETS = (
12
+ "ultrafast",
13
+ "superfast",
14
+ "veryfast",
15
+ "faster",
16
+ "fast",
17
+ "medium",
18
+ "slow",
19
+ "slower",
20
+ "veryslow",
21
+ "placebo",
22
+ )
23
+
24
+
11
25
  def binaries(ffmpeg=None, ffprobe=None):
12
26
  ffmpeg = ffmpeg or os.environ.get("FRAMERUDIMENTS_FFMPEG") or shutil.which("ffmpeg")
13
27
  if not ffmpeg:
@@ -56,7 +70,7 @@ def audio_filters(project):
56
70
 
57
71
 
58
72
  def command(project, output, ffmpeg, *, preset="fast", crf=20):
59
- if preset not in {"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow"}:
73
+ if preset not in X264_PRESETS:
60
74
  raise ValueError("Unsupported encoder preset")
61
75
  if not isinstance(crf, int) or not 0 <= crf <= 51:
62
76
  raise ValueError("CRF must be an integer in [0,51]")
@@ -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
- if self.travel and i:
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
@@ -8,7 +8,7 @@ async function initialise(config) {
8
8
  const engine = window.butterchurn.default || window.butterchurn;
9
9
  const viz = engine.createVisualizer(null, canvas, {
10
10
  width: config.width, height: config.height, pixelRatio: 1, textureRatio: 1,
11
- meshWidth: 64, meshHeight: 48,
11
+ meshWidth: config.mesh_width, meshHeight: config.mesh_height,
12
12
  });
13
13
  const r = viz.renderer, gl = r.gl;
14
14
  for (const name of ['targetFrameBuffer', 'targetTexture', 'calcTimeAndFPS']) {
@@ -114,5 +114,6 @@ async function initialise(config) {
114
114
  return {engine: 'butterchurn', version: '2.6.7', opengl: gl.getParameter(gl.VERSION),
115
115
  renderer: info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER),
116
116
  input: 'full composed RGBA injected into feedback before real MilkDrop warp/composite',
117
- clock: 'pinned calcTimeAndFPS bridge: output frame / fps; fixed fps', seed: config.seed};
117
+ clock: 'pinned calcTimeAndFPS bridge: output frame / fps; fixed fps', seed: config.seed,
118
+ mesh_width: config.mesh_width, mesh_height: config.mesh_height};
118
119
  }
@@ -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 == "first_pips":
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