zentris 1.0.0__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 (41) hide show
  1. zentris-1.0.0/.gitignore +16 -0
  2. zentris-1.0.0/CMakeLists.txt +83 -0
  3. zentris-1.0.0/LICENSE +21 -0
  4. zentris-1.0.0/PKG-INFO +130 -0
  5. zentris-1.0.0/README.md +109 -0
  6. zentris-1.0.0/pyproject.toml +59 -0
  7. zentris-1.0.0/python/zentris/__init__.py +3 -0
  8. zentris-1.0.0/python/zentris/__main__.py +3 -0
  9. zentris-1.0.0/python/zentris/_launcher.py +48 -0
  10. zentris-1.0.0/src/analysis.cpp +732 -0
  11. zentris-1.0.0/src/analysis.hpp +69 -0
  12. zentris-1.0.0/src/audio.cpp +108 -0
  13. zentris-1.0.0/src/audio.hpp +51 -0
  14. zentris-1.0.0/src/game.cpp +402 -0
  15. zentris-1.0.0/src/game.hpp +115 -0
  16. zentris-1.0.0/src/input.cpp +219 -0
  17. zentris-1.0.0/src/input.hpp +50 -0
  18. zentris-1.0.0/src/library.cpp +198 -0
  19. zentris-1.0.0/src/library.hpp +46 -0
  20. zentris-1.0.0/src/main.cpp +724 -0
  21. zentris-1.0.0/src/mathutil.hpp +165 -0
  22. zentris-1.0.0/src/platform.cpp +136 -0
  23. zentris-1.0.0/src/platform.hpp +28 -0
  24. zentris-1.0.0/src/renderer.cpp +1295 -0
  25. zentris-1.0.0/src/renderer.hpp +153 -0
  26. zentris-1.0.0/src/shaders.hpp +1213 -0
  27. zentris-1.0.0/src/songplan.cpp +77 -0
  28. zentris-1.0.0/src/songplan.hpp +37 -0
  29. zentris-1.0.0/src/theme.cpp +459 -0
  30. zentris-1.0.0/src/theme.hpp +126 -0
  31. zentris-1.0.0/src/third_party_impl.cpp +8 -0
  32. zentris-1.0.0/src/youtube.cpp +193 -0
  33. zentris-1.0.0/src/youtube.hpp +25 -0
  34. zentris-1.0.0/src/zenscope.cpp +740 -0
  35. zentris-1.0.0/third_party/gamecontrollerdb.txt +2292 -0
  36. zentris-1.0.0/third_party/glad/include/KHR/khrplatform.h +311 -0
  37. zentris-1.0.0/third_party/glad/include/glad/gl.h +2135 -0
  38. zentris-1.0.0/third_party/glad/src/gl.c +951 -0
  39. zentris-1.0.0/third_party/miniaudio.h +93468 -0
  40. zentris-1.0.0/third_party/stb_easy_font.h +305 -0
  41. zentris-1.0.0/third_party/stb_image_write.h +1724 -0
@@ -0,0 +1,16 @@
1
+ # Build output (also holds test screenshots)
2
+ build/
3
+
4
+ # Songs are not versioned (copyrighted, large): put your own in audio/
5
+ audio/*
6
+ !audio/.gitkeep
7
+
8
+ # Editors / OS
9
+ .vscode/
10
+ .idea/
11
+ *.swp
12
+ .DS_Store
13
+
14
+ # Python packaging
15
+ dist/
16
+ *.egg-info/
@@ -0,0 +1,83 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(Zentris C CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ if(NOT CMAKE_BUILD_TYPE)
7
+ set(CMAKE_BUILD_TYPE Release)
8
+ endif()
9
+
10
+ # GLFW: the system one by default; ZENTRIS_BUNDLE_GLFW=ON (used for Python wheels) builds it from
11
+ # source and links it statically, so the binaries only need the system's OpenGL/X11 at runtime.
12
+ option(ZENTRIS_BUNDLE_GLFW "Build GLFW from source and link it statically" OFF)
13
+ if(NOT ZENTRIS_BUNDLE_GLFW)
14
+ find_package(glfw3 QUIET)
15
+ endif()
16
+ if(ZENTRIS_BUNDLE_GLFW OR NOT glfw3_FOUND)
17
+ include(FetchContent)
18
+ set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
19
+ set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
20
+ set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
21
+ set(GLFW_INSTALL OFF CACHE BOOL "" FORCE)
22
+ set(GLFW_BUILD_WAYLAND OFF CACHE BOOL "" FORCE) # X11 (also runs under XWayland)
23
+ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
24
+ FetchContent_Declare(glfw GIT_REPOSITORY https://github.com/glfw/glfw.git GIT_TAG 3.4 GIT_SHALLOW TRUE)
25
+ FetchContent_MakeAvailable(glfw)
26
+ endif()
27
+ find_package(Threads REQUIRED)
28
+
29
+ # OpenGL 3.3 core function loader (generated by glad, loaded at runtime through GLFW).
30
+ add_library(glad STATIC third_party/glad/src/gl.c)
31
+ target_include_directories(glad PUBLIC third_party/glad/include)
32
+ set_target_properties(glad PROPERTIES POSITION_INDEPENDENT_CODE ON)
33
+
34
+ # Audio decoding, analysis and song planning, shared by the game and the analysis viewer.
35
+ add_library(zencore STATIC
36
+ src/analysis.cpp
37
+ src/audio.cpp
38
+ src/library.cpp
39
+ src/songplan.cpp
40
+ src/youtube.cpp
41
+ src/platform.cpp
42
+ src/third_party_impl.cpp
43
+ )
44
+ target_include_directories(zencore PUBLIC src third_party)
45
+ target_link_libraries(zencore PUBLIC Threads::Threads ${CMAKE_DL_LIBS})
46
+ if(UNIX)
47
+ target_link_libraries(zencore PUBLIC m)
48
+ endif()
49
+ if(MSVC)
50
+ target_compile_options(zencore PUBLIC /utf-8 /bigobj)
51
+ target_compile_definitions(zencore PUBLIC _CRT_SECURE_NO_WARNINGS NOMINMAX _USE_MATH_DEFINES)
52
+ endif()
53
+
54
+ # The game.
55
+ add_executable(zentris
56
+ src/main.cpp
57
+ src/game.cpp
58
+ src/input.cpp
59
+ src/theme.cpp
60
+ src/renderer.cpp
61
+ )
62
+ target_link_libraries(zentris PRIVATE zencore glfw glad)
63
+ # Gamepad mappings (SDL_GameControllerDB): looked up next to the executable, then in the sources.
64
+ target_compile_definitions(zentris PRIVATE ZEN_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
65
+ add_custom_command(TARGET zentris POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
66
+ ${CMAKE_SOURCE_DIR}/third_party/gamecontrollerdb.txt $<TARGET_FILE_DIR:zentris>/gamecontrollerdb.txt)
67
+
68
+ # zenscope: shows what the analysis hears in a song (structure, scene levels, pulses, spectrum, beats).
69
+ add_executable(zenscope src/zenscope.cpp)
70
+ target_link_libraries(zenscope PRIVATE zencore glfw glad)
71
+
72
+ foreach(t zencore zentris zenscope)
73
+ target_compile_definitions(${t} PRIVATE GLFW_INCLUDE_NONE)
74
+ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
75
+ target_compile_options(${t} PRIVATE -Wall -Wno-unused-function -Wno-missing-field-initializers)
76
+ endif()
77
+ endforeach()
78
+
79
+ # Python wheel (scikit-build-core): the binaries and data go inside the zentris package.
80
+ if(SKBUILD)
81
+ install(TARGETS zentris zenscope RUNTIME DESTINATION zentris/bin)
82
+ install(FILES third_party/gamecontrollerdb.txt DESTINATION zentris/bin)
83
+ endif()
zentris-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Grégoire Passault
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
zentris-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: zentris
3
+ Version: 1.0.0
4
+ Summary: A Tetris Effect inspired game that plays any playlist (local music or YouTube)
5
+ Keywords: tetris,game,music,visualizer,opengl
6
+ Author: Grégoire Passault
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Topic :: Games/Entertainment :: Puzzle Games
10
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
11
+ Classifier: Programming Language :: C++
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Environment :: X11 Applications
16
+ Project-URL: Homepage, https://github.com/Gregwar/zentris
17
+ Requires-Python: >=3.9
18
+ Requires-Dist: yt-dlp[default]
19
+ Requires-Dist: imageio-ffmpeg
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Zentris
23
+
24
+ A Tetris game inspired by **Tetris Effect**, but playing **any playlist you like**: your own music folder or
25
+ a YouTube playlist. Each song is analysed ahead of time (tempo, key, structure: intro, verse, build,
26
+ chorus, drop...) and the game stages it: pieces fall on the beat, the pace follows the song's energy, and
27
+ a procedurally generated 3D scene evolves with every section. Built in C++ / OpenGL 3.3.
28
+
29
+ ![Nine scenes generated by the game](docs/scenes.jpg)
30
+
31
+ *Nine scenes generated by the game. Every song, and every run, gets its own scene.*
32
+
33
+ ## Install
34
+
35
+ Prebuilt packages for Linux, macOS and Windows are published on PyPI:
36
+
37
+ ```sh
38
+ pipx install zentris # then: zentris, zenscope
39
+ uvx zentris # or run it without installing
40
+ zentris "https://www.youtube.com/playlist?list=PLWHPu2N_Gb2lbZ8-7sYKSEbDlb3UiAVye"
41
+ zentris ~/Music # with no argument it plays ./audio or ~/Music
42
+ ```
43
+
44
+ The package brings [yt-dlp](https://github.com/yt-dlp/yt-dlp) and an ffmpeg build for YouTube playlists; YouTube
45
+ also needs a JavaScript runtime ([Deno](https://deno.com) or Node.js) on your system.
46
+
47
+ ## Build from source
48
+
49
+ Dependencies (Debian/Ubuntu): `sudo apt install cmake g++ libglfw3-dev libglew-dev`
50
+ (miniaudio and stb are vendored in `third_party/`).
51
+
52
+ ```sh
53
+ cmake -S . -B build && cmake --build build -j
54
+ ./build/zentris # plays every .mp3/.wav/.flac in ./audio, in order (--shuffle for random)
55
+ ./build/zentris song.mp3 ~/Music --fullscreen
56
+ ```
57
+
58
+ YouTube playlists (or single videos) work too, through [yt-dlp](https://github.com/yt-dlp/yt-dlp)
59
+ (install it with `pipx install "yt-dlp[default]"`; YouTube also needs a JavaScript runtime such as Deno or
60
+ Node.js, and Node.js is picked up automatically). Songs are downloaded when queued and cached as MP3 in
61
+ `~/.cache/zentris/youtube/`. Downloading from YouTube is against its terms of service: personal use only.
62
+
63
+ ```sh
64
+ ./build/zentris "https://www.youtube.com/playlist?list=PLWHPu2N_Gb2lbZ8-7sYKSEbDlb3UiAVye" # quote it: & is special in the shell
65
+ ./build/zenscope "https://www.youtube.com/watch?v=..."
66
+ ```
67
+
68
+ Other options: `--shuffle` (random song order; default is in order: folders alphabetically, playlists in their order), `--seed N` (repeat a scene), `--autoplay`, `--mute`, `--size WxH`,
69
+ `--shots PREFIX N` (renders N screenshots of different scenes and exits), `--phase-shots PREFIX` (one screenshot per scene level of a song).
70
+
71
+ ## zenscope: see what the game hears
72
+
73
+ `./build/zenscope [songs or folders...]` plays a song and shows the analysis that drives the game:
74
+ - a phase timebar with the scene levels (calm / mid / peak) and where the scene changes happen
75
+ - the detected structure: intro, verse, build, chorus, drop, break, outro, with similar parts grouped
76
+ - pulse zones, a 16-band spectrum, loudness, intensity and onsets, and bar lines
77
+ - a live readout of the current section and the game's density/speed/glow profile, with the beat in the bar
78
+ - a zoomed detail view with the beat grid
79
+
80
+ Space plays/pauses, Left/Right seek 5 s, Up/Down zoom the detail view, N/P change song, click to seek.
81
+ The game and zenscope share the same analysis and song plan code (`src/songplan.*`), so what you see is what the game uses.
82
+
83
+ ## Controls
84
+
85
+ Keyboard and gamepad both work at the same time. A gamepad is detected at startup and on hot-plug,
86
+ and the on-screen hints follow whichever device you used last.
87
+ Controllers are recognised through the bundled [SDL_GameControllerDB](https://github.com/mdqinc/SDL_GameControllerDB)
88
+ (`third_party/gamecontrollerdb.txt`; extra mappings can be given in `SDL_GAMECONTROLLERCONFIG`). A controller
89
+ missing from it still works with a generic layout.
90
+
91
+ | Action | Keyboard | Gamepad |
92
+ |---|---|---|
93
+ | Move | ← → | D-pad / left stick |
94
+ | Soft / hard drop | ↓ / Space | Down / Up |
95
+ | Rotate | ↑ or X (Z or J to rotate left) | A (B/X to rotate left) |
96
+ | Hold | C / Shift (tap) | LB / RB / triggers |
97
+ | New scene | T | Y |
98
+ | Next song | N / Tab / Enter / PageDown | Back |
99
+ | Seek ±10 s in the song (testing) | Ctrl+Shift+Left/Right | |
100
+ | Next level (debugging) | L | |
101
+ | Pause | Esc / P (Q quits while paused) | Start |
102
+ | Fullscreen | F / F11 | |
103
+
104
+ ## How the music shapes the game
105
+
106
+ Each song is decoded and analyzed in the background (under 1 s):
107
+
108
+ - **Tempo and beat grid**: gravity steps land on the beat, at 1 row every 2 beats, every beat, or every half beat, depending on the song's energy at that moment.
109
+ - **Levels**: one level per 20 lines, up to a plateau at level 20. Each level speeds up the beat-locked gravity (at the plateau: 5, 10 or 16 rows per beat for calm, mid and peak parts, on musical subdivisions, capped at 28 rows/s) and shortens the lock delay (0.55 s → 0.35 s). Game over resets the level.
110
+ - **Key**: the base hue follows the circle of fifths.
111
+ - **Brightness, bass/air balance, dynamics, density**: choose the mood (night, dusk or pale), the particle layouts, bloom, how strongly things react, and the camera's motion.
112
+ - **Song structure**: the song is split at bar lines into labelled segments (intro, verse, build, chorus, drop, break, outro), and segments that sound alike are grouped. Each segment eases the scene's density, speed, glow and saturation (builds ramp up, breaks thin out).
113
+ - **One identity per song**: background, main particles, blocks, frame and mood stay the same for the whole song. At most three intensity levels (calm, mid, peak) shift the hue slightly and add color, glow or an extra particle layer. Changes happen only when the level changes, crossfading over 8 s, and never interrupt each other.
114
+ - **Calm by design**: visuals follow slow (~1 s) envelopes of the music and there is no camera shake or flashing. Beat pulses appear only during peak sections (choruses, drops), stronger for faster songs; songs above ~110 BPM also get soft hits on strong transients there.
115
+ - **Live bands and loudness**, heavily smoothed, drive particle motion and glow.
116
+
117
+ The scene seed combines the song's fingerprint with a random seed for each run, so the same song looks
118
+ different every time. The combinatorial space covers 8 palette schemes × 3 moods, 24 backgrounds,
119
+ 38 particle layouts × 20 particle shapes (none, one or two layers), 12 continuous surface layers (smoke, silk, lava, caustics, ink, geometry, aurora, fog, beams, flowing rings, liquid, cloud shades), 22 block materials × a continuous family of
120
+ block shapes (cube → rounded → sphere → gem), 18 board frames, a rare audio equalizer (3 layouts × 6 renderings
121
+ × 4 resolutions × 3 colorings) and light rays, 22 line-clear effects, 25 transition shapes, and a
122
+ post-processing grade (bloom, vignette, chromatic aberration, grain, split-toning).
123
+ The scene name is shown in the bottom-left corner.
124
+
125
+ ## Releasing
126
+
127
+ `.github/workflows/wheels.yml` builds wheels for Linux (x86_64, aarch64), macOS (x86_64, arm64) and
128
+ Windows (x86_64) plus a source distribution on every push, and publishes them to PyPI when a `v*` tag is
129
+ pushed (`git tag v0.1.0 && git push --tags`). Publishing uses PyPI trusted publishing: on pypi.org, add a
130
+ publisher for project `zentris`, repository `Gregwar/zentris`, workflow `wheels.yml`, environment `pypi`.
@@ -0,0 +1,109 @@
1
+ # Zentris
2
+
3
+ A Tetris game inspired by **Tetris Effect**, but playing **any playlist you like**: your own music folder or
4
+ a YouTube playlist. Each song is analysed ahead of time (tempo, key, structure: intro, verse, build,
5
+ chorus, drop...) and the game stages it: pieces fall on the beat, the pace follows the song's energy, and
6
+ a procedurally generated 3D scene evolves with every section. Built in C++ / OpenGL 3.3.
7
+
8
+ ![Nine scenes generated by the game](docs/scenes.jpg)
9
+
10
+ *Nine scenes generated by the game. Every song, and every run, gets its own scene.*
11
+
12
+ ## Install
13
+
14
+ Prebuilt packages for Linux, macOS and Windows are published on PyPI:
15
+
16
+ ```sh
17
+ pipx install zentris # then: zentris, zenscope
18
+ uvx zentris # or run it without installing
19
+ zentris "https://www.youtube.com/playlist?list=PLWHPu2N_Gb2lbZ8-7sYKSEbDlb3UiAVye"
20
+ zentris ~/Music # with no argument it plays ./audio or ~/Music
21
+ ```
22
+
23
+ The package brings [yt-dlp](https://github.com/yt-dlp/yt-dlp) and an ffmpeg build for YouTube playlists; YouTube
24
+ also needs a JavaScript runtime ([Deno](https://deno.com) or Node.js) on your system.
25
+
26
+ ## Build from source
27
+
28
+ Dependencies (Debian/Ubuntu): `sudo apt install cmake g++ libglfw3-dev libglew-dev`
29
+ (miniaudio and stb are vendored in `third_party/`).
30
+
31
+ ```sh
32
+ cmake -S . -B build && cmake --build build -j
33
+ ./build/zentris # plays every .mp3/.wav/.flac in ./audio, in order (--shuffle for random)
34
+ ./build/zentris song.mp3 ~/Music --fullscreen
35
+ ```
36
+
37
+ YouTube playlists (or single videos) work too, through [yt-dlp](https://github.com/yt-dlp/yt-dlp)
38
+ (install it with `pipx install "yt-dlp[default]"`; YouTube also needs a JavaScript runtime such as Deno or
39
+ Node.js, and Node.js is picked up automatically). Songs are downloaded when queued and cached as MP3 in
40
+ `~/.cache/zentris/youtube/`. Downloading from YouTube is against its terms of service: personal use only.
41
+
42
+ ```sh
43
+ ./build/zentris "https://www.youtube.com/playlist?list=PLWHPu2N_Gb2lbZ8-7sYKSEbDlb3UiAVye" # quote it: & is special in the shell
44
+ ./build/zenscope "https://www.youtube.com/watch?v=..."
45
+ ```
46
+
47
+ Other options: `--shuffle` (random song order; default is in order: folders alphabetically, playlists in their order), `--seed N` (repeat a scene), `--autoplay`, `--mute`, `--size WxH`,
48
+ `--shots PREFIX N` (renders N screenshots of different scenes and exits), `--phase-shots PREFIX` (one screenshot per scene level of a song).
49
+
50
+ ## zenscope: see what the game hears
51
+
52
+ `./build/zenscope [songs or folders...]` plays a song and shows the analysis that drives the game:
53
+ - a phase timebar with the scene levels (calm / mid / peak) and where the scene changes happen
54
+ - the detected structure: intro, verse, build, chorus, drop, break, outro, with similar parts grouped
55
+ - pulse zones, a 16-band spectrum, loudness, intensity and onsets, and bar lines
56
+ - a live readout of the current section and the game's density/speed/glow profile, with the beat in the bar
57
+ - a zoomed detail view with the beat grid
58
+
59
+ Space plays/pauses, Left/Right seek 5 s, Up/Down zoom the detail view, N/P change song, click to seek.
60
+ The game and zenscope share the same analysis and song plan code (`src/songplan.*`), so what you see is what the game uses.
61
+
62
+ ## Controls
63
+
64
+ Keyboard and gamepad both work at the same time. A gamepad is detected at startup and on hot-plug,
65
+ and the on-screen hints follow whichever device you used last.
66
+ Controllers are recognised through the bundled [SDL_GameControllerDB](https://github.com/mdqinc/SDL_GameControllerDB)
67
+ (`third_party/gamecontrollerdb.txt`; extra mappings can be given in `SDL_GAMECONTROLLERCONFIG`). A controller
68
+ missing from it still works with a generic layout.
69
+
70
+ | Action | Keyboard | Gamepad |
71
+ |---|---|---|
72
+ | Move | ← → | D-pad / left stick |
73
+ | Soft / hard drop | ↓ / Space | Down / Up |
74
+ | Rotate | ↑ or X (Z or J to rotate left) | A (B/X to rotate left) |
75
+ | Hold | C / Shift (tap) | LB / RB / triggers |
76
+ | New scene | T | Y |
77
+ | Next song | N / Tab / Enter / PageDown | Back |
78
+ | Seek ±10 s in the song (testing) | Ctrl+Shift+Left/Right | |
79
+ | Next level (debugging) | L | |
80
+ | Pause | Esc / P (Q quits while paused) | Start |
81
+ | Fullscreen | F / F11 | |
82
+
83
+ ## How the music shapes the game
84
+
85
+ Each song is decoded and analyzed in the background (under 1 s):
86
+
87
+ - **Tempo and beat grid**: gravity steps land on the beat, at 1 row every 2 beats, every beat, or every half beat, depending on the song's energy at that moment.
88
+ - **Levels**: one level per 20 lines, up to a plateau at level 20. Each level speeds up the beat-locked gravity (at the plateau: 5, 10 or 16 rows per beat for calm, mid and peak parts, on musical subdivisions, capped at 28 rows/s) and shortens the lock delay (0.55 s → 0.35 s). Game over resets the level.
89
+ - **Key**: the base hue follows the circle of fifths.
90
+ - **Brightness, bass/air balance, dynamics, density**: choose the mood (night, dusk or pale), the particle layouts, bloom, how strongly things react, and the camera's motion.
91
+ - **Song structure**: the song is split at bar lines into labelled segments (intro, verse, build, chorus, drop, break, outro), and segments that sound alike are grouped. Each segment eases the scene's density, speed, glow and saturation (builds ramp up, breaks thin out).
92
+ - **One identity per song**: background, main particles, blocks, frame and mood stay the same for the whole song. At most three intensity levels (calm, mid, peak) shift the hue slightly and add color, glow or an extra particle layer. Changes happen only when the level changes, crossfading over 8 s, and never interrupt each other.
93
+ - **Calm by design**: visuals follow slow (~1 s) envelopes of the music and there is no camera shake or flashing. Beat pulses appear only during peak sections (choruses, drops), stronger for faster songs; songs above ~110 BPM also get soft hits on strong transients there.
94
+ - **Live bands and loudness**, heavily smoothed, drive particle motion and glow.
95
+
96
+ The scene seed combines the song's fingerprint with a random seed for each run, so the same song looks
97
+ different every time. The combinatorial space covers 8 palette schemes × 3 moods, 24 backgrounds,
98
+ 38 particle layouts × 20 particle shapes (none, one or two layers), 12 continuous surface layers (smoke, silk, lava, caustics, ink, geometry, aurora, fog, beams, flowing rings, liquid, cloud shades), 22 block materials × a continuous family of
99
+ block shapes (cube → rounded → sphere → gem), 18 board frames, a rare audio equalizer (3 layouts × 6 renderings
100
+ × 4 resolutions × 3 colorings) and light rays, 22 line-clear effects, 25 transition shapes, and a
101
+ post-processing grade (bloom, vignette, chromatic aberration, grain, split-toning).
102
+ The scene name is shown in the bottom-left corner.
103
+
104
+ ## Releasing
105
+
106
+ `.github/workflows/wheels.yml` builds wheels for Linux (x86_64, aarch64), macOS (x86_64, arm64) and
107
+ Windows (x86_64) plus a source distribution on every push, and publishes them to PyPI when a `v*` tag is
108
+ pushed (`git tag v0.1.0 && git push --tags`). Publishing uses PyPI trusted publishing: on pypi.org, add a
109
+ publisher for project `zentris`, repository `Gregwar/zentris`, workflow `wheels.yml`, environment `pypi`.
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["scikit-build-core>=0.10"]
3
+ build-backend = "scikit_build_core.build"
4
+
5
+ [project]
6
+ name = "zentris"
7
+ version = "1.0.0"
8
+ description = "A Tetris Effect inspired game that plays any playlist (local music or YouTube)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Grégoire Passault" }]
14
+ keywords = ["tetris", "game", "music", "visualizer", "opengl"]
15
+ classifiers = [
16
+ "Topic :: Games/Entertainment :: Puzzle Games",
17
+ "Topic :: Multimedia :: Sound/Audio :: Analysis",
18
+ "Programming Language :: C++",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Operating System :: MacOS",
21
+ "Operating System :: Microsoft :: Windows",
22
+ "Environment :: X11 Applications",
23
+ ]
24
+ dependencies = ["yt-dlp[default]", "imageio-ffmpeg"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Gregwar/zentris"
28
+
29
+ [project.scripts]
30
+ zentris = "zentris._launcher:main"
31
+ zenscope = "zentris._launcher:scope"
32
+
33
+ [tool.scikit-build]
34
+ minimum-version = "build-system.requires"
35
+ cmake.build-type = "Release"
36
+ wheel.packages = ["python/zentris"]
37
+ # The package holds native executables, not Python extensions: one wheel per platform, any Python 3.
38
+ wheel.py-api = "py3"
39
+ sdist.exclude = ["audio", "build", "docs", ".github"]
40
+
41
+ [tool.scikit-build.cmake.define]
42
+ ZENTRIS_BUNDLE_GLFW = "ON"
43
+
44
+ [tool.cibuildwheel]
45
+ build = "cp312-*"
46
+ skip = "*-musllinux_* *-win32 *_i686"
47
+ test-command = "zentris --help"
48
+ build-verbosity = 1
49
+
50
+ [tool.cibuildwheel.linux]
51
+ manylinux-x86_64-image = "manylinux_2_28"
52
+ manylinux-aarch64-image = "manylinux_2_28"
53
+ before-all = "dnf install -y libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel mesa-libGL-devel"
54
+
55
+ [tool.cibuildwheel.macos]
56
+ environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" }
57
+
58
+ [tool.cibuildwheel.windows]
59
+ archs = ["AMD64"]
@@ -0,0 +1,3 @@
1
+ """Zentris: a Tetris Effect inspired game that plays any playlist."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,3 @@
1
+ from ._launcher import main
2
+
3
+ main()
@@ -0,0 +1,48 @@
1
+ """Console entry points: run the bundled native binaries (zentris, zenscope)."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import sysconfig
8
+ from pathlib import Path
9
+
10
+ BIN_DIR = Path(__file__).resolve().parent / "bin"
11
+
12
+
13
+ def _tool_env() -> dict:
14
+ env = dict(os.environ)
15
+ # yt-dlp is a dependency of this package: point the game to the one installed alongside.
16
+ scripts = Path(sysconfig.get_path("scripts"))
17
+ for name in ("yt-dlp", "yt-dlp.exe"):
18
+ if (scripts / name).is_file():
19
+ env.setdefault("ZENTRIS_YTDLP", str(scripts / name))
20
+ break
21
+ # yt-dlp needs ffmpeg to convert audio; use the bundled one when the system has none.
22
+ if not shutil.which("ffmpeg"):
23
+ try:
24
+ import imageio_ffmpeg
25
+
26
+ env.setdefault("ZENTRIS_FFMPEG", imageio_ffmpeg.get_ffmpeg_exe())
27
+ except Exception:
28
+ pass
29
+ return env
30
+
31
+
32
+ def _run(name: str) -> None:
33
+ exe = BIN_DIR / (name + (".exe" if os.name == "nt" else ""))
34
+ if not exe.is_file():
35
+ sys.exit(f"{name}: bundled binary not found at {exe}")
36
+ try:
37
+ code = subprocess.call([str(exe), *sys.argv[1:]], env=_tool_env())
38
+ except KeyboardInterrupt:
39
+ code = 130
40
+ sys.exit(code)
41
+
42
+
43
+ def main() -> None:
44
+ _run("zentris")
45
+
46
+
47
+ def scope() -> None:
48
+ _run("zenscope")