pycodedj 0.1.2__tar.gz → 0.1.4__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.
- {pycodedj-0.1.2 → pycodedj-0.1.4}/.gitignore +3 -1
- {pycodedj-0.1.2 → pycodedj-0.1.4}/CHANGELOG.md +14 -0
- pycodedj-0.1.4/PKG-INFO +275 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/README.ja.md +1 -1
- {pycodedj-0.1.2 → pycodedj-0.1.4}/README.md +1 -1
- {pycodedj-0.1.2 → pycodedj-0.1.4}/pyproject.toml +2 -5
- {pycodedj-0.1.2 → pycodedj-0.1.4}/sc/synths.scd +29 -26
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/__init__.py +1 -1
- pycodedj-0.1.2/PKG-INFO +0 -60
- pycodedj-0.1.2/docs/visualizer-concept.html +0 -438
- {pycodedj-0.1.2 → pycodedj-0.1.4}/.github/workflows/workflow.yml +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/LICENSE +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/docs/CNAME +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/docs/index.html +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/docs/manual.ja.md +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/docs/manual.md +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/examples/club_set.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/examples/demo.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/examples/hello_sc.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/__main__.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/analyzer.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/block_parser.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/engine.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/mapper.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/osc_bridge.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/src/pycodedj/watcher.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/tests/__init__.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/tests/test_analyzer.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/tests/test_block_parser.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/tests/test_engine.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/tests/test_mapper.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/tests/test_osc_bridge.py +0 -0
- {pycodedj-0.1.2 → pycodedj-0.1.4}/tests/test_watcher.py +0 -0
|
@@ -50,9 +50,10 @@ logs/
|
|
|
50
50
|
.claude/settings.local.json
|
|
51
51
|
.claude/todos/
|
|
52
52
|
.claude/mcp*.json
|
|
53
|
-
.codex
|
|
53
|
+
.codex/
|
|
54
54
|
.claude/
|
|
55
55
|
AGENT.md
|
|
56
|
+
.agents/
|
|
56
57
|
|
|
57
58
|
# Claude Code
|
|
58
59
|
CLAUDE.local.md
|
|
@@ -77,3 +78,4 @@ runs/
|
|
|
77
78
|
tmp/
|
|
78
79
|
temp/
|
|
79
80
|
.cache/
|
|
81
|
+
docs/visualizer-*.html
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.4] - 2026-05-07
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- Fix editable installs so `pycodedj.__main__` resolves from `src/pycodedj`
|
|
8
|
+
- Fix SuperCollider OSC receiving and address parsing for `/pycodedj/loop/<name>/<param>` messages
|
|
9
|
+
- Keep the SuperCollider OSC receiver active across repeated `synths.scd` reloads
|
|
10
|
+
|
|
11
|
+
## [0.1.3] - 2026-05-07
|
|
12
|
+
|
|
13
|
+
### Fixes
|
|
14
|
+
|
|
15
|
+
- Add `readme = "README.md"` to `pyproject.toml` so PyPI shows the project description
|
|
16
|
+
|
|
3
17
|
## [0.1.2] - 2026-05-07
|
|
4
18
|
|
|
5
19
|
### Features
|
pycodedj-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycodedj
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
License: MIT License with Commons Clause
|
|
5
|
+
|
|
6
|
+
"Commons Clause" License Condition v1.0
|
|
7
|
+
|
|
8
|
+
The Software is provided to you by the Licensor under the License,
|
|
9
|
+
as defined below, subject to the following condition.
|
|
10
|
+
|
|
11
|
+
Without limiting other conditions in the License, the grant of rights
|
|
12
|
+
under the License will not include, and the License does not grant to
|
|
13
|
+
you, the right to Sell the Software.
|
|
14
|
+
|
|
15
|
+
For purposes of the foregoing, "Sell" means practicing any or all of
|
|
16
|
+
the rights granted to you under the License to provide to third
|
|
17
|
+
parties, for a fee or other consideration (including without limitation
|
|
18
|
+
fees for hosting or consulting/support services related to the
|
|
19
|
+
Software), a product or service whose value derives, entirely or
|
|
20
|
+
substantially, from the functionality of the Software. Any license
|
|
21
|
+
notice or attribution required by the License must also include this
|
|
22
|
+
Commons Clause License Condition notice.
|
|
23
|
+
|
|
24
|
+
Software: PyCodeDJ
|
|
25
|
+
License: MIT
|
|
26
|
+
Licensor: Yuichi Kaneko
|
|
27
|
+
|
|
28
|
+
-------------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
MIT License
|
|
31
|
+
|
|
32
|
+
Copyright (c) 2026 Yuichi Kaneko
|
|
33
|
+
|
|
34
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
35
|
+
a copy of this software and associated documentation files (the
|
|
36
|
+
"Software"), to deal in the Software without restriction, including
|
|
37
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
38
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
39
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
40
|
+
the following conditions:
|
|
41
|
+
|
|
42
|
+
The above copyright notice and this permission notice shall be included
|
|
43
|
+
in all copies or substantial portions of the Software.
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
46
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
47
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
48
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
49
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
50
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
51
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
52
|
+
License-File: LICENSE
|
|
53
|
+
Requires-Python: >=3.10
|
|
54
|
+
Requires-Dist: python-osc>=1.8
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
57
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
58
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
59
|
+
Provides-Extra: watch
|
|
60
|
+
Requires-Dist: watchdog>=3.0; extra == 'watch'
|
|
61
|
+
Description-Content-Type: text/markdown
|
|
62
|
+
|
|
63
|
+
# PyCodeDJ
|
|
64
|
+
|
|
65
|
+
[日本語版 README はこちら](https://github.com/kanekoyuichi/pycodedj/blob/main/README.ja.md) · [Full Manual (EN)](https://github.com/kanekoyuichi/pycodedj/blob/main/docs/manual.md) · [マニュアル (JA)](https://github.com/kanekoyuichi/pycodedj/blob/main/docs/manual.ja.md)
|
|
66
|
+
|
|
67
|
+
A live-coding environment that translates Python code structure into music in real time. Every save changes the performance.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Concept
|
|
72
|
+
|
|
73
|
+
PyCodeDJ connects "writing code" directly to "making sound."
|
|
74
|
+
|
|
75
|
+
Add more functions and the polyphony widens. Deepen nesting and the filter opens up. Fill in comments and the space grows. The structure of your code is the instrument.
|
|
76
|
+
|
|
77
|
+
Two things set it apart from existing Python ↔ SuperCollider bridges (sc3nb, supriya):
|
|
78
|
+
|
|
79
|
+
- **Hot-reload performance** — swap out a loop without stopping it. Saving a file becomes an immediate sound change.
|
|
80
|
+
- **Audible code structure** — structural features extracted via AST analysis (depth, branch count, function count, etc.) are automatically mapped to musical parameters.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
[Python engine] →OSC→ [SuperCollider] →audio out→ speakers
|
|
88
|
+
↓ OSC
|
|
89
|
+
[Hydra etc.] →video out→ screen
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Layer | Role | Technology |
|
|
93
|
+
| :--- | :--- | :--- |
|
|
94
|
+
| Control | Code analysis, scheduling, OSC dispatch | Python 3.10+, python-osc, watchdog |
|
|
95
|
+
| Audio | Real-time sound synthesis | SuperCollider (scsynth) |
|
|
96
|
+
| Visual | Music-synced visuals | Hydra or Pyxel |
|
|
97
|
+
|
|
98
|
+
BPM clock is held by SuperCollider's `TempoClock`. Python only sends parameter updates over OSC; timing accuracy is delegated to SuperCollider.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Code Structure → Music Parameter Mapping
|
|
103
|
+
|
|
104
|
+
| Code feature | Music parameter | Musical rationale |
|
|
105
|
+
| :--- | :--- | :--- |
|
|
106
|
+
| Max nesting depth | Filter Cutoff (200–4000 Hz) | Deep structure = complexity = brightness |
|
|
107
|
+
| Control-flow count (if/for/while) | LFO rate (0.1–5.0 Hz) | More branches = faster modulation |
|
|
108
|
+
| Function definition count | Polyphony voice count (1–4) | Functions = independent voices |
|
|
109
|
+
| Comment ratio | Reverb depth (0.0–0.8) | More whitespace = more space |
|
|
110
|
+
|
|
111
|
+
Tempo (BPM) and root pitch are controlled explicitly by the performer, to prevent the foundation of the piece from shifting on every save.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Installation
|
|
116
|
+
|
|
117
|
+
**Requirements**
|
|
118
|
+
|
|
119
|
+
- Python 3.10 or later
|
|
120
|
+
- SuperCollider (with scsynth available)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install 'pycodedj[watch]'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The `[watch]` extra enables the `pycodedj watch` command.
|
|
127
|
+
|
|
128
|
+
Development install:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git clone https://github.com/yourname/pycodedj
|
|
132
|
+
cd pycodedj
|
|
133
|
+
pip install -e ".[dev]"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Quick Start
|
|
139
|
+
|
|
140
|
+
**1. Boot SuperCollider and load the synths**
|
|
141
|
+
|
|
142
|
+
Open `sc/synths.scd` in the SuperCollider IDE and evaluate it.
|
|
143
|
+
|
|
144
|
+
**2. Write a live-coding file**
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# @loop bass interval=2.0
|
|
148
|
+
def bass():
|
|
149
|
+
for i in range(8):
|
|
150
|
+
if i % 2 == 0:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
# @loop melody interval=0.5
|
|
154
|
+
def melody():
|
|
155
|
+
x = 1
|
|
156
|
+
y = 2
|
|
157
|
+
return x + y
|
|
158
|
+
|
|
159
|
+
# @loop pad interval=4.0
|
|
160
|
+
def pad():
|
|
161
|
+
# make space
|
|
162
|
+
# a little more
|
|
163
|
+
pass
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**3. Evaluate a block**
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pycodedj eval demo.py::bass
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
On success, feedback is printed immediately:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
[pycodedj] bass cutoff=418Hz lfo=1.08Hz reverb=0.00 voices=1
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Other loops keep playing without interruption.
|
|
179
|
+
|
|
180
|
+
**4. Live-code with watch mode**
|
|
181
|
+
|
|
182
|
+
Instead of running eval manually, use watch to re-evaluate all loops on every save:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pycodedj watch demo.py
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
From here, just write code and save.
|
|
189
|
+
|
|
190
|
+
> **Note on `interval` (current MVP):** Values like `interval=2.0` are parsed and stored, but are not yet sent over OSC. Loop repeat timing is managed by SuperCollider's `TempoClock`. A mechanism to pass interval to SC is planned for a future release.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Example Files
|
|
195
|
+
|
|
196
|
+
| File | Contents |
|
|
197
|
+
| :--- | :--- |
|
|
198
|
+
| `examples/demo.py` | Intro demo with bass / melody / pad |
|
|
199
|
+
| `examples/club_set.py` | Club-style demo: sub_bass / hat_engine / neon_stab / acid_lead / warehouse_air |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Live-Coding Examples
|
|
204
|
+
|
|
205
|
+
### Deeper nesting opens the filter
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# @loop bass interval=2.0
|
|
209
|
+
def bass():
|
|
210
|
+
for i in range(4): # control flow +1
|
|
211
|
+
for j in range(4): # depth +1, control flow +1
|
|
212
|
+
if i == j: # depth +1, control flow +1
|
|
213
|
+
pass
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### More functions = more polyphony
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
# @loop chord interval=1.0
|
|
220
|
+
def voice_a(): pass
|
|
221
|
+
def voice_b(): pass
|
|
222
|
+
def voice_c(): pass
|
|
223
|
+
def voice_d(): pass
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### More comments = more space (reverb)
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
# @loop pad interval=4.0
|
|
230
|
+
# leave space here
|
|
231
|
+
# a little more
|
|
232
|
+
# silence is music
|
|
233
|
+
def pad(): pass
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## OSC Address Reference
|
|
239
|
+
|
|
240
|
+
Addresses used to communicate with SuperCollider.
|
|
241
|
+
|
|
242
|
+
| Address | Type | Range | Parameter |
|
|
243
|
+
| :--- | :--- | :--- | :--- |
|
|
244
|
+
| `/pycodedj/loop/<name>/cutoff` | float | 200–4000 Hz | Filter Cutoff |
|
|
245
|
+
| `/pycodedj/loop/<name>/lfo_rate` | float | 0.1–5.0 Hz | LFO rate |
|
|
246
|
+
| `/pycodedj/loop/<name>/reverb` | float | 0.0–0.8 | Reverb depth |
|
|
247
|
+
| `/pycodedj/loop/<name>/voice_count` | int | 1–4 | Polyphony voice count |
|
|
248
|
+
|
|
249
|
+
`<name>` is the loop name (e.g. `bass`, `melody`). Each loop has its own address namespace, so multiple loops never overwrite each other's parameters.
|
|
250
|
+
|
|
251
|
+
External visualisers such as Hydra can receive the same parameters on a separate port.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Requirements
|
|
256
|
+
|
|
257
|
+
- **Recommended OS:** macOS (low-latency Core Audio) or Linux (Raspberry Pi 5, etc.)
|
|
258
|
+
- **Python:** 3.10 or later
|
|
259
|
+
- **SuperCollider:** 3.12 or later
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Roadmap
|
|
264
|
+
|
|
265
|
+
- [x] Spec design and mapping design
|
|
266
|
+
- [ ] Phase 0: Listening validation of mapping hypotheses
|
|
267
|
+
- [x] Phase 1: Python → SuperCollider OSC prototype
|
|
268
|
+
- [x] Phase 2: Hot-reload live loop implementation (`pycodedj watch`)
|
|
269
|
+
- [ ] Phase 3: Hydra visualiser integration
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT + Commons Clause — free to use, modify, and perform (including paid live performances). Selling or commercially distributing the software itself is not permitted. See [LICENSE](LICENSE) for details.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PyCodeDJ
|
|
2
2
|
|
|
3
|
-
[English README](https://github.com/kanekoyuichi/pycodedj/blob/main/README.md)
|
|
3
|
+
[English README](https://github.com/kanekoyuichi/pycodedj/blob/main/README.md) · [マニュアル (JA)](https://github.com/kanekoyuichi/pycodedj/blob/main/docs/manual.ja.md) · [Full Manual (EN)](https://github.com/kanekoyuichi/pycodedj/blob/main/docs/manual.md)
|
|
4
4
|
|
|
5
5
|
Pythonコードの構造をリアルタイムに音楽へ変換するライブコーディング環境。ファイルを保存するたびに演奏が変わる。
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PyCodeDJ
|
|
2
2
|
|
|
3
|
-
[日本語版 README はこちら](https://github.com/kanekoyuichi/pycodedj/blob/main/README.ja.md)
|
|
3
|
+
[日本語版 README はこちら](https://github.com/kanekoyuichi/pycodedj/blob/main/README.ja.md) · [Full Manual (EN)](https://github.com/kanekoyuichi/pycodedj/blob/main/docs/manual.md) · [マニュアル (JA)](https://github.com/kanekoyuichi/pycodedj/blob/main/docs/manual.ja.md)
|
|
4
4
|
|
|
5
5
|
A live-coding environment that translates Python code structure into music in real time. Every save changes the performance.
|
|
6
6
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pycodedj"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4"
|
|
4
4
|
requires-python = ">=3.10"
|
|
5
|
+
readme = "README.md"
|
|
5
6
|
license = {file = "LICENSE"}
|
|
6
7
|
dependencies = [
|
|
7
8
|
"python-osc>=1.8",
|
|
@@ -21,10 +22,6 @@ build-backend = "hatchling.build"
|
|
|
21
22
|
[tool.hatch.build.targets.wheel]
|
|
22
23
|
packages = ["src/pycodedj"]
|
|
23
24
|
|
|
24
|
-
[tool.hatch.build.targets.wheel.force-include]
|
|
25
|
-
"sc" = "pycodedj/sc"
|
|
26
|
-
"examples" = "pycodedj/examples"
|
|
27
|
-
|
|
28
25
|
[tool.ruff]
|
|
29
26
|
line-length = 100
|
|
30
27
|
target-version = "py310"
|
|
@@ -118,34 +118,37 @@ s.waitForBoot({
|
|
|
118
118
|
// OSCFunc with nil path receives every incoming OSC message and we filter by prefix.
|
|
119
119
|
// var は SC では実行文より前に宣言しなければならないため、
|
|
120
120
|
// すべての var をクロージャ先頭にまとめる。
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// parts: ["", "pycodedj", "loop", <name>, <param>]
|
|
125
|
-
if (parts.size != 5) { ^nil };
|
|
126
|
-
if (parts[1] != "pycodedj") { ^nil };
|
|
127
|
-
if (parts[2] != "loop") { ^nil };
|
|
121
|
+
if (~pycodedjOSC.notNil) {
|
|
122
|
+
thisProcess.removeOSCRecvFunc(~pycodedjOSC);
|
|
123
|
+
};
|
|
128
124
|
|
|
129
|
-
|
|
130
|
-
param
|
|
131
|
-
|
|
125
|
+
~pycodedjOSC = { |msg, time, addr, recvPort|
|
|
126
|
+
var parts, name, param, val;
|
|
127
|
+
parts = msg[0].asString.split($/).reject({ |part| part.size == 0 });
|
|
128
|
+
// parts: ["pycodedj", "loop", <name>, <param>]
|
|
129
|
+
if ((parts.size == 4) and: { parts[0] == "pycodedj" } and: { parts[1] == "loop" }) {
|
|
130
|
+
name = parts[2];
|
|
131
|
+
param = parts[3];
|
|
132
|
+
val = msg[1];
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
switch (param,
|
|
135
|
+
"voice_count", {
|
|
136
|
+
if (val.asInteger <= 0) {
|
|
137
|
+
if (~loops[name].notNil) {
|
|
138
|
+
~loops[name].do({ |synth| synth.set(\gate, 0) });
|
|
139
|
+
~loops[name] = nil;
|
|
140
|
+
};
|
|
141
|
+
} {
|
|
142
|
+
~startLoop.value(name, val.asInteger);
|
|
139
143
|
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}); // nil path = receive all OSC messages
|
|
144
|
+
},
|
|
145
|
+
"cutoff", { ~loops[name].do({ |synth| synth.set(\cutoff, val.asFloat) }) },
|
|
146
|
+
"lfo_rate", { ~loops[name].do({ |synth| synth.set(\lfoRate, val.asFloat) }) },
|
|
147
|
+
"reverb", { ~loops[name].do({ |synth| synth.set(\reverbMix, val.asFloat) }) }
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
thisProcess.addOSCRecvFunc(~pycodedjOSC);
|
|
149
152
|
|
|
150
|
-
"PyCodeDJ synths loaded. Ready.".postln;
|
|
153
|
+
("PyCodeDJ synths loaded. Ready. OSC port: " ++ NetAddr.langPort).postln;
|
|
151
154
|
});
|
pycodedj-0.1.2/PKG-INFO
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pycodedj
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
License: MIT License with Commons Clause
|
|
5
|
-
|
|
6
|
-
"Commons Clause" License Condition v1.0
|
|
7
|
-
|
|
8
|
-
The Software is provided to you by the Licensor under the License,
|
|
9
|
-
as defined below, subject to the following condition.
|
|
10
|
-
|
|
11
|
-
Without limiting other conditions in the License, the grant of rights
|
|
12
|
-
under the License will not include, and the License does not grant to
|
|
13
|
-
you, the right to Sell the Software.
|
|
14
|
-
|
|
15
|
-
For purposes of the foregoing, "Sell" means practicing any or all of
|
|
16
|
-
the rights granted to you under the License to provide to third
|
|
17
|
-
parties, for a fee or other consideration (including without limitation
|
|
18
|
-
fees for hosting or consulting/support services related to the
|
|
19
|
-
Software), a product or service whose value derives, entirely or
|
|
20
|
-
substantially, from the functionality of the Software. Any license
|
|
21
|
-
notice or attribution required by the License must also include this
|
|
22
|
-
Commons Clause License Condition notice.
|
|
23
|
-
|
|
24
|
-
Software: PyCodeDJ
|
|
25
|
-
License: MIT
|
|
26
|
-
Licensor: Yuichi Kaneko
|
|
27
|
-
|
|
28
|
-
-------------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
MIT License
|
|
31
|
-
|
|
32
|
-
Copyright (c) 2026 Yuichi Kaneko
|
|
33
|
-
|
|
34
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
35
|
-
a copy of this software and associated documentation files (the
|
|
36
|
-
"Software"), to deal in the Software without restriction, including
|
|
37
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
38
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
39
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
40
|
-
the following conditions:
|
|
41
|
-
|
|
42
|
-
The above copyright notice and this permission notice shall be included
|
|
43
|
-
in all copies or substantial portions of the Software.
|
|
44
|
-
|
|
45
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
46
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
47
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
48
|
-
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
49
|
-
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
50
|
-
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
51
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
52
|
-
License-File: LICENSE
|
|
53
|
-
Requires-Python: >=3.10
|
|
54
|
-
Requires-Dist: python-osc>=1.8
|
|
55
|
-
Provides-Extra: dev
|
|
56
|
-
Requires-Dist: mypy; extra == 'dev'
|
|
57
|
-
Requires-Dist: pytest; extra == 'dev'
|
|
58
|
-
Requires-Dist: ruff; extra == 'dev'
|
|
59
|
-
Provides-Extra: watch
|
|
60
|
-
Requires-Dist: watchdog>=3.0; extra == 'watch'
|
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>PyCodeDJ — Visualizer Concept</title>
|
|
7
|
-
<style>
|
|
8
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
-
body {
|
|
10
|
-
background: #000;
|
|
11
|
-
color: #cdd6f4;
|
|
12
|
-
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
13
|
-
overflow: hidden;
|
|
14
|
-
height: 100vh;
|
|
15
|
-
display: flex;
|
|
16
|
-
flex-direction: column;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
#canvas { display: block; flex: 1; }
|
|
20
|
-
|
|
21
|
-
/* ── HUD overlay ── */
|
|
22
|
-
#hud {
|
|
23
|
-
position: fixed;
|
|
24
|
-
top: 0; left: 0; right: 0; bottom: 0;
|
|
25
|
-
pointer-events: none;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/* top-left: loop name + code snapshot */
|
|
29
|
-
#loop-info {
|
|
30
|
-
position: absolute;
|
|
31
|
-
top: 1.5rem; left: 1.5rem;
|
|
32
|
-
pointer-events: none;
|
|
33
|
-
}
|
|
34
|
-
.loop-name {
|
|
35
|
-
font-size: 0.7rem;
|
|
36
|
-
letter-spacing: 0.15em;
|
|
37
|
-
text-transform: uppercase;
|
|
38
|
-
color: #585b70;
|
|
39
|
-
margin-bottom: 0.25rem;
|
|
40
|
-
}
|
|
41
|
-
.loop-code {
|
|
42
|
-
font-size: 0.75rem;
|
|
43
|
-
line-height: 1.6;
|
|
44
|
-
color: rgba(166,227,161,0.7);
|
|
45
|
-
white-space: pre;
|
|
46
|
-
}
|
|
47
|
-
.loop-code .kw { color: #cba6f7; }
|
|
48
|
-
.loop-code .fn { color: #89b4fa; }
|
|
49
|
-
.loop-code .cm { color: #45475a; font-style: italic; }
|
|
50
|
-
.loop-code .num { color: #f9e2af; }
|
|
51
|
-
|
|
52
|
-
/* top-right: param meters */
|
|
53
|
-
#params {
|
|
54
|
-
position: absolute;
|
|
55
|
-
top: 1.5rem; right: 1.5rem;
|
|
56
|
-
display: flex;
|
|
57
|
-
flex-direction: column;
|
|
58
|
-
gap: 0.75rem;
|
|
59
|
-
width: 180px;
|
|
60
|
-
}
|
|
61
|
-
.param {
|
|
62
|
-
display: flex;
|
|
63
|
-
flex-direction: column;
|
|
64
|
-
gap: 0.2rem;
|
|
65
|
-
}
|
|
66
|
-
.param-header {
|
|
67
|
-
display: flex;
|
|
68
|
-
justify-content: space-between;
|
|
69
|
-
font-size: 0.65rem;
|
|
70
|
-
letter-spacing: 0.1em;
|
|
71
|
-
}
|
|
72
|
-
.param-label { color: #585b70; text-transform: uppercase; }
|
|
73
|
-
.param-value { color: #cdd6f4; }
|
|
74
|
-
.param-track {
|
|
75
|
-
height: 3px;
|
|
76
|
-
background: rgba(255,255,255,0.06);
|
|
77
|
-
border-radius: 2px;
|
|
78
|
-
overflow: hidden;
|
|
79
|
-
}
|
|
80
|
-
.param-fill {
|
|
81
|
-
height: 100%;
|
|
82
|
-
border-radius: 2px;
|
|
83
|
-
transition: width 1.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/* bottom: scene label */
|
|
87
|
-
#scene-label {
|
|
88
|
-
position: absolute;
|
|
89
|
-
bottom: 1.75rem;
|
|
90
|
-
left: 50%;
|
|
91
|
-
transform: translateX(-50%);
|
|
92
|
-
font-size: 0.65rem;
|
|
93
|
-
letter-spacing: 0.2em;
|
|
94
|
-
text-transform: uppercase;
|
|
95
|
-
color: rgba(88,91,112,0.8);
|
|
96
|
-
text-align: center;
|
|
97
|
-
}
|
|
98
|
-
#scene-label span {
|
|
99
|
-
display: block;
|
|
100
|
-
font-size: 0.5rem;
|
|
101
|
-
margin-top: 0.3rem;
|
|
102
|
-
color: rgba(88,91,112,0.4);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/* bottom-right: PyCodeDJ badge */
|
|
106
|
-
#badge {
|
|
107
|
-
position: absolute;
|
|
108
|
-
bottom: 1.5rem; right: 1.5rem;
|
|
109
|
-
font-size: 0.6rem;
|
|
110
|
-
letter-spacing: 0.15em;
|
|
111
|
-
color: rgba(166,227,161,0.3);
|
|
112
|
-
}
|
|
113
|
-
</style>
|
|
114
|
-
</head>
|
|
115
|
-
<body>
|
|
116
|
-
<canvas id="canvas"></canvas>
|
|
117
|
-
|
|
118
|
-
<div id="hud">
|
|
119
|
-
<!-- loop code -->
|
|
120
|
-
<div id="loop-info">
|
|
121
|
-
<div class="loop-name" id="loopName">loop: bass</div>
|
|
122
|
-
<div class="loop-code" id="loopCode"></div>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<!-- params -->
|
|
126
|
-
<div id="params">
|
|
127
|
-
<div class="param">
|
|
128
|
-
<div class="param-header">
|
|
129
|
-
<span class="param-label">cutoff</span>
|
|
130
|
-
<span class="param-value" id="vCutoff">580 Hz</span>
|
|
131
|
-
</div>
|
|
132
|
-
<div class="param-track"><div class="param-fill" id="fCutoff" style="width:10%;background:#a6e3a1"></div></div>
|
|
133
|
-
</div>
|
|
134
|
-
<div class="param">
|
|
135
|
-
<div class="param-header">
|
|
136
|
-
<span class="param-label">lfo rate</span>
|
|
137
|
-
<span class="param-value" id="vLfo">0.10 Hz</span>
|
|
138
|
-
</div>
|
|
139
|
-
<div class="param-track"><div class="param-fill" id="fLfo" style="width:0%;background:#89b4fa"></div></div>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="param">
|
|
142
|
-
<div class="param-header">
|
|
143
|
-
<span class="param-label">reverb</span>
|
|
144
|
-
<span class="param-value" id="vReverb">0.27</span>
|
|
145
|
-
</div>
|
|
146
|
-
<div class="param-track"><div class="param-fill" id="fReverb" style="width:34%;background:#cba6f7"></div></div>
|
|
147
|
-
</div>
|
|
148
|
-
<div class="param">
|
|
149
|
-
<div class="param-header">
|
|
150
|
-
<span class="param-label">voices</span>
|
|
151
|
-
<span class="param-value" id="vVoices">1</span>
|
|
152
|
-
</div>
|
|
153
|
-
<div class="param-track"><div class="param-fill" id="fVoices" style="width:25%;background:#f9e2af"></div></div>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<div id="scene-label">
|
|
158
|
-
<div id="sceneTitle">minimal</div>
|
|
159
|
-
<span id="sceneDesc">def bass(): pass</span>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
<div id="badge">PyCodeDJ / visualizer concept</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
<script>
|
|
166
|
-
const canvas = document.getElementById('canvas');
|
|
167
|
-
const ctx = canvas.getContext('2d');
|
|
168
|
-
|
|
169
|
-
function resize() {
|
|
170
|
-
canvas.width = window.innerWidth;
|
|
171
|
-
canvas.height = window.innerHeight;
|
|
172
|
-
}
|
|
173
|
-
resize();
|
|
174
|
-
window.addEventListener('resize', resize);
|
|
175
|
-
|
|
176
|
-
// ── Scene definitions ──────────────────────────────────────────────────────
|
|
177
|
-
const scenes = [
|
|
178
|
-
{
|
|
179
|
-
name: 'bass',
|
|
180
|
-
title: 'minimal',
|
|
181
|
-
desc: 'def bass(): pass',
|
|
182
|
-
cutoff: 0.10, lfo: 0.00, reverb: 0.34, voices: 1,
|
|
183
|
-
cutoffHz: '580 Hz', lfoHz: '0.10 Hz', reverbVal: '0.27',
|
|
184
|
-
code: '<span class="cm"># @loop bass interval=2.0</span>\n<span class="kw">def</span> <span class="fn">bass</span>():\n <span class="kw">pass</span>',
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: 'bass',
|
|
188
|
-
title: 'filter opens',
|
|
189
|
-
desc: 'deeper nesting → brighter cutoff',
|
|
190
|
-
cutoff: 0.40, lfo: 0.20, reverb: 0.20, voices: 1,
|
|
191
|
-
cutoffHz: '1720 Hz', lfoHz: '1.08 Hz', reverbVal: '0.16',
|
|
192
|
-
code: '<span class="cm"># @loop bass interval=2.0</span>\n<span class="kw">def</span> <span class="fn">bass</span>():\n <span class="kw">for</span> i <span class="kw">in</span> <span class="fn">range</span>(<span class="num">8</span>):\n <span class="kw">if</span> i % <span class="num">2</span> == <span class="num">0</span>:\n <span class="kw">pass</span>',
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
name: 'bass',
|
|
196
|
-
title: 'deep structure',
|
|
197
|
-
desc: 'triple nesting → wide open filter',
|
|
198
|
-
cutoff: 0.60, lfo: 0.30, reverb: 0.16, voices: 1,
|
|
199
|
-
cutoffHz: '2480 Hz', lfoHz: '1.57 Hz', reverbVal: '0.13',
|
|
200
|
-
code: '<span class="cm"># @loop bass interval=2.0</span>\n<span class="kw">def</span> <span class="fn">bass</span>():\n <span class="kw">for</span> i <span class="kw">in</span> <span class="fn">range</span>(<span class="num">8</span>):\n <span class="kw">for</span> j <span class="kw">in</span> <span class="fn">range</span>(<span class="num">4</span>):\n <span class="kw">if</span> i == j:\n <span class="kw">pass</span>',
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
name: 'chord',
|
|
204
|
-
title: '4 voices',
|
|
205
|
-
desc: 'four functions → four layers',
|
|
206
|
-
cutoff: 0.10, lfo: 0.00, reverb: 0.20, voices: 4,
|
|
207
|
-
cutoffHz: '580 Hz', lfoHz: '0.10 Hz', reverbVal: '0.16',
|
|
208
|
-
code: '<span class="cm"># @loop chord interval=1.0</span>\n<span class="kw">def</span> <span class="fn">voice_a</span>(): <span class="kw">pass</span>\n<span class="kw">def</span> <span class="fn">voice_b</span>(): <span class="kw">pass</span>\n<span class="kw">def</span> <span class="fn">voice_c</span>(): <span class="kw">pass</span>\n<span class="kw">def</span> <span class="fn">voice_d</span>(): <span class="kw">pass</span>',
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: 'pad',
|
|
212
|
-
title: 'warehouse air',
|
|
213
|
-
desc: 'comments only → deep reverb space',
|
|
214
|
-
cutoff: 0.10, lfo: 0.00, reverb: 0.84, voices: 1,
|
|
215
|
-
cutoffHz: '580 Hz', lfoHz: '0.10 Hz', reverbVal: '0.67',
|
|
216
|
-
code: '<span class="cm"># @loop pad interval=4.0</span>\n<span class="cm"># smoke above the kick</span>\n<span class="cm"># late reflections</span>\n<span class="cm"># concrete room tail</span>\n<span class="kw">def</span> <span class="fn">pad</span>(): <span class="kw">pass</span>',
|
|
217
|
-
},
|
|
218
|
-
];
|
|
219
|
-
|
|
220
|
-
// ── State ──────────────────────────────────────────────────────────────────
|
|
221
|
-
let state = {
|
|
222
|
-
cutoff: 0.10, // 0-1
|
|
223
|
-
lfo: 0.00, // 0-1
|
|
224
|
-
reverb: 0.34, // 0-1
|
|
225
|
-
voices: 1,
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// smoothed state for rendering
|
|
229
|
-
let smooth = { ...state };
|
|
230
|
-
|
|
231
|
-
let sceneIdx = 0;
|
|
232
|
-
let t = 0;
|
|
233
|
-
const SCENE_DURATION = 4000; // ms
|
|
234
|
-
let lastSwitch = performance.now();
|
|
235
|
-
|
|
236
|
-
// ── Particles ──────────────────────────────────────────────────────────────
|
|
237
|
-
const MAX_PARTICLES = 320;
|
|
238
|
-
const particles = [];
|
|
239
|
-
|
|
240
|
-
class Particle {
|
|
241
|
-
constructor(cx, cy) {
|
|
242
|
-
this.reset(cx, cy);
|
|
243
|
-
}
|
|
244
|
-
reset(cx, cy) {
|
|
245
|
-
const a = Math.random() * Math.PI * 2;
|
|
246
|
-
const r = 40 + Math.random() * 160;
|
|
247
|
-
this.x = cx + Math.cos(a) * r;
|
|
248
|
-
this.y = cy + Math.sin(a) * r;
|
|
249
|
-
this.vx = (Math.random() - 0.5) * 0.6;
|
|
250
|
-
this.vy = (Math.random() - 0.5) * 0.6;
|
|
251
|
-
this.life = 0;
|
|
252
|
-
this.maxLife = 120 + Math.random() * 200;
|
|
253
|
-
this.size = 1 + Math.random() * 2.5;
|
|
254
|
-
this.hue = 120 + Math.random() * 200; // green→mauve
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── Rings ──────────────────────────────────────────────────────────────────
|
|
259
|
-
const rings = Array.from({ length: 6 }, (_, i) => ({
|
|
260
|
-
phase: (i / 6) * Math.PI * 2,
|
|
261
|
-
baseR: 60 + i * 55,
|
|
262
|
-
speed: 0.003 + i * 0.001,
|
|
263
|
-
}));
|
|
264
|
-
|
|
265
|
-
// ── Lissajous trails ───────────────────────────────────────────────────────
|
|
266
|
-
const lissajousHistory = [];
|
|
267
|
-
const LISSA_LEN = 320;
|
|
268
|
-
|
|
269
|
-
// ── HUD update ─────────────────────────────────────────────────────────────
|
|
270
|
-
function applyScene(s) {
|
|
271
|
-
state = { cutoff: s.cutoff, lfo: s.lfo, reverb: s.reverb, voices: s.voices };
|
|
272
|
-
|
|
273
|
-
document.getElementById('loopName').textContent = `loop: ${s.name}`;
|
|
274
|
-
document.getElementById('loopCode').innerHTML = s.code;
|
|
275
|
-
document.getElementById('sceneTitle').textContent = s.title;
|
|
276
|
-
document.getElementById('sceneDesc').textContent = s.desc;
|
|
277
|
-
|
|
278
|
-
document.getElementById('vCutoff').textContent = s.cutoffHz;
|
|
279
|
-
document.getElementById('vLfo').textContent = s.lfoHz;
|
|
280
|
-
document.getElementById('vReverb').textContent = s.reverbVal;
|
|
281
|
-
document.getElementById('vVoices').textContent = String(s.voices);
|
|
282
|
-
|
|
283
|
-
document.getElementById('fCutoff').style.width = (s.cutoff * 100) + '%';
|
|
284
|
-
document.getElementById('fLfo').style.width = (s.lfo * 100) + '%';
|
|
285
|
-
document.getElementById('fReverb').style.width = (s.reverb * 100) + '%';
|
|
286
|
-
document.getElementById('fVoices').style.width = ((s.voices / 4) * 100) + '%';
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
applyScene(scenes[0]);
|
|
290
|
-
|
|
291
|
-
// ── Main render loop ───────────────────────────────────────────────────────
|
|
292
|
-
function lerp(a, b, k) { return a + (b - a) * k; }
|
|
293
|
-
|
|
294
|
-
let last = performance.now();
|
|
295
|
-
|
|
296
|
-
function frame(now) {
|
|
297
|
-
const dt = Math.min(now - last, 32);
|
|
298
|
-
last = now;
|
|
299
|
-
t += dt * 0.001;
|
|
300
|
-
|
|
301
|
-
// scene switch
|
|
302
|
-
if (now - lastSwitch > SCENE_DURATION) {
|
|
303
|
-
sceneIdx = (sceneIdx + 1) % scenes.length;
|
|
304
|
-
applyScene(scenes[sceneIdx]);
|
|
305
|
-
lastSwitch = now;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// smooth params
|
|
309
|
-
const k = 1 - Math.pow(0.92, dt);
|
|
310
|
-
smooth.cutoff = lerp(smooth.cutoff, state.cutoff, k);
|
|
311
|
-
smooth.lfo = lerp(smooth.lfo, state.lfo, k);
|
|
312
|
-
smooth.reverb = lerp(smooth.reverb, state.reverb, k);
|
|
313
|
-
smooth.voices = lerp(smooth.voices, state.voices, k);
|
|
314
|
-
|
|
315
|
-
const W = canvas.width, H = canvas.height;
|
|
316
|
-
const cx = W / 2, cy = H / 2;
|
|
317
|
-
|
|
318
|
-
// ── background ──
|
|
319
|
-
const trailAlpha = 0.04 + smooth.reverb * 0.1; // more reverb = longer trails
|
|
320
|
-
ctx.fillStyle = `rgba(0,0,0,${trailAlpha < 0.06 ? 0.06 : trailAlpha})`;
|
|
321
|
-
ctx.fillRect(0, 0, W, H);
|
|
322
|
-
|
|
323
|
-
// ── base radial glow ──
|
|
324
|
-
const glowR = 180 + smooth.cutoff * 220;
|
|
325
|
-
const hue = 120 + smooth.cutoff * 160; // green(120) → mauve(280)
|
|
326
|
-
const grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, glowR);
|
|
327
|
-
grd.addColorStop(0, `hsla(${hue},60%,55%,${0.04 + smooth.cutoff * 0.06})`);
|
|
328
|
-
grd.addColorStop(0.5, `hsla(${hue},50%,40%,${0.02})`);
|
|
329
|
-
grd.addColorStop(1, 'transparent');
|
|
330
|
-
ctx.fillStyle = grd;
|
|
331
|
-
ctx.fillRect(0, 0, W, H);
|
|
332
|
-
|
|
333
|
-
// ── Lissajous ──
|
|
334
|
-
const lfoSpeed = 0.3 + smooth.lfo * 3.5;
|
|
335
|
-
const lx = Math.sin(t * lfoSpeed * 1.00) * (cx * 0.55);
|
|
336
|
-
const ly = Math.sin(t * lfoSpeed * 1.31 + 0.7) * (cy * 0.45);
|
|
337
|
-
|
|
338
|
-
lissajousHistory.push({ x: cx + lx, y: cy + ly });
|
|
339
|
-
if (lissajousHistory.length > LISSA_LEN) lissajousHistory.shift();
|
|
340
|
-
|
|
341
|
-
if (lissajousHistory.length > 2) {
|
|
342
|
-
ctx.beginPath();
|
|
343
|
-
ctx.moveTo(lissajousHistory[0].x, lissajousHistory[0].y);
|
|
344
|
-
for (let i = 1; i < lissajousHistory.length; i++) {
|
|
345
|
-
const p = lissajousHistory[i];
|
|
346
|
-
const alpha = i / lissajousHistory.length;
|
|
347
|
-
ctx.lineTo(p.x, p.y);
|
|
348
|
-
}
|
|
349
|
-
ctx.strokeStyle = `hsla(${hue},70%,65%,0.45)`;
|
|
350
|
-
ctx.lineWidth = 1.2;
|
|
351
|
-
ctx.stroke();
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ── Rings (voice_count layers) ──
|
|
355
|
-
const voiceLayers = Math.round(smooth.voices);
|
|
356
|
-
for (let v = 0; v < voiceLayers; v++) {
|
|
357
|
-
const ring = rings[v % rings.length];
|
|
358
|
-
const phaseOffset = (v / voiceLayers) * Math.PI * 2;
|
|
359
|
-
const pulse = 1 + Math.sin(t * lfoSpeed + ring.phase + phaseOffset) * (0.08 + smooth.cutoff * 0.12);
|
|
360
|
-
const r = ring.baseR * pulse * (0.7 + smooth.cutoff * 0.5);
|
|
361
|
-
const ringHue = hue + v * 30;
|
|
362
|
-
const alpha = 0.25 + smooth.cutoff * 0.35;
|
|
363
|
-
|
|
364
|
-
ctx.beginPath();
|
|
365
|
-
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
366
|
-
ctx.strokeStyle = `hsla(${ringHue},65%,60%,${alpha})`;
|
|
367
|
-
ctx.lineWidth = 1 + smooth.reverb * 2;
|
|
368
|
-
ctx.stroke();
|
|
369
|
-
|
|
370
|
-
// inner glow dot at ring intersection with lissajous angle
|
|
371
|
-
const ang = t * ring.speed + ring.phase + phaseOffset;
|
|
372
|
-
const dx = cx + Math.cos(ang) * r;
|
|
373
|
-
const dy = cy + Math.sin(ang) * r;
|
|
374
|
-
ctx.beginPath();
|
|
375
|
-
ctx.arc(dx, dy, 3 + smooth.cutoff * 4, 0, Math.PI * 2);
|
|
376
|
-
ctx.fillStyle = `hsla(${ringHue},80%,70%,${0.6 + smooth.cutoff * 0.3})`;
|
|
377
|
-
ctx.fill();
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ── Particles ──
|
|
381
|
-
// spawn
|
|
382
|
-
const spawnRate = Math.round(1 + smooth.lfo * 5 + smooth.voices * 1.5);
|
|
383
|
-
for (let i = 0; i < spawnRate; i++) {
|
|
384
|
-
if (particles.length < MAX_PARTICLES) {
|
|
385
|
-
particles.push(new Particle(cx, cy));
|
|
386
|
-
} else {
|
|
387
|
-
// recycle oldest dead particle
|
|
388
|
-
const dead = particles.find(p => p.life >= p.maxLife);
|
|
389
|
-
if (dead) dead.reset(cx, cy);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// update & draw
|
|
394
|
-
for (const p of particles) {
|
|
395
|
-
p.life++;
|
|
396
|
-
const progress = p.life / p.maxLife;
|
|
397
|
-
if (progress > 1) continue;
|
|
398
|
-
|
|
399
|
-
const speed = 0.4 + smooth.lfo * 2.5;
|
|
400
|
-
p.x += p.vx * speed;
|
|
401
|
-
p.y += p.vy * speed;
|
|
402
|
-
|
|
403
|
-
const alpha = Math.sin(progress * Math.PI) * (0.4 + smooth.cutoff * 0.4);
|
|
404
|
-
const size = p.size * (1 - progress * 0.5);
|
|
405
|
-
|
|
406
|
-
ctx.beginPath();
|
|
407
|
-
ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
|
|
408
|
-
ctx.fillStyle = `hsla(${p.hue + smooth.cutoff * 60},70%,65%,${alpha})`;
|
|
409
|
-
ctx.fill();
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ── Reverb shimmer — horizontal scan lines ──
|
|
413
|
-
if (smooth.reverb > 0.1) {
|
|
414
|
-
const lineCount = Math.round(smooth.reverb * 18);
|
|
415
|
-
for (let i = 0; i < lineCount; i++) {
|
|
416
|
-
const y = ((t * 60 * (i % 3 === 0 ? 1 : -0.7) + i * (H / lineCount)) % H + H) % H;
|
|
417
|
-
const a = smooth.reverb * 0.06 * (1 - Math.abs(y / H - 0.5) * 2);
|
|
418
|
-
ctx.fillStyle = `hsla(${hue + i * 12},50%,65%,${a})`;
|
|
419
|
-
ctx.fillRect(0, y, W, 1);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ── Center crosshair ──
|
|
424
|
-
const chAlpha = 0.12 + smooth.cutoff * 0.08;
|
|
425
|
-
ctx.strokeStyle = `rgba(166,227,161,${chAlpha})`;
|
|
426
|
-
ctx.lineWidth = 0.5;
|
|
427
|
-
ctx.setLineDash([4, 8]);
|
|
428
|
-
ctx.beginPath(); ctx.moveTo(cx - 30, cy); ctx.lineTo(cx + 30, cy); ctx.stroke();
|
|
429
|
-
ctx.beginPath(); ctx.moveTo(cx, cy - 30); ctx.lineTo(cx, cy + 30); ctx.stroke();
|
|
430
|
-
ctx.setLineDash([]);
|
|
431
|
-
|
|
432
|
-
requestAnimationFrame(frame);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
requestAnimationFrame(frame);
|
|
436
|
-
</script>
|
|
437
|
-
</body>
|
|
438
|
-
</html>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|