norn-cli 2.6.0 → 2.7.0
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.
- package/CHANGELOG.md +28 -0
- package/README.md +33 -2
- package/dist/cli.js +647 -134798
- package/package.json +70 -10
- package/AGENTS.md +0 -95
- package/demos/tests-showcase/scripts/fake-sql-adapter.js +0 -70
- package/scripts/__pycache__/reddit_signal_miner.cpython-312.pyc +0 -0
- package/scripts/generate-coding-bed.mjs +0 -243
- package/scripts/reddit_signal_miner.py +0 -482
- package/scripts/validate-skills.mjs +0 -50
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "norn-cli",
|
|
3
|
-
"displayName": "Norn —
|
|
4
|
-
"description": "Version-controlled API
|
|
5
|
-
"version": "2.
|
|
3
|
+
"displayName": "Norn — Tests and Runbooks in Your Repo",
|
|
4
|
+
"description": "Version-controlled API, database, and Kubernetes tests and runbooks. Author in VS Code, then run the same files from the CLI and CI.",
|
|
5
|
+
"version": "2.7.0",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
"api automation",
|
|
30
30
|
"openapi",
|
|
31
31
|
"http",
|
|
32
|
-
"test automation"
|
|
32
|
+
"test automation",
|
|
33
|
+
"kubernetes",
|
|
34
|
+
"kubectl",
|
|
35
|
+
"runbooks"
|
|
33
36
|
],
|
|
34
37
|
"engines": {
|
|
35
38
|
"vscode": "^1.108.1"
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
"onLanguage:nornsql",
|
|
45
48
|
"onLanguage:nornapi",
|
|
46
49
|
"onLanguage:nornenv",
|
|
50
|
+
"onLanguage:nornk8s",
|
|
47
51
|
"workspaceContains:**/norn.config.json",
|
|
48
52
|
"onChatParticipant:norn.chat",
|
|
49
53
|
"onView:norn.sidebarHome",
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
"onCommand:norn.createStarterCatalog",
|
|
53
57
|
"onCommand:norn.createSqlStarterFiles",
|
|
54
58
|
"onCommand:norn.createMcpStarterConfig",
|
|
59
|
+
"onCommand:norn.openTerminal",
|
|
55
60
|
"onCommand:norn.debugSequence",
|
|
56
61
|
"onDebugResolve:norn"
|
|
57
62
|
],
|
|
@@ -88,6 +93,26 @@
|
|
|
88
93
|
"title": "Select Environment",
|
|
89
94
|
"category": "Norn"
|
|
90
95
|
},
|
|
96
|
+
{
|
|
97
|
+
"command": "norn.k8s.runLine",
|
|
98
|
+
"title": "Run Kubernetes Command",
|
|
99
|
+
"category": "Norn"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"command": "norn.k8s.runSequence",
|
|
103
|
+
"title": "Run Kubernetes Sequence",
|
|
104
|
+
"category": "Norn"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"command": "norn.k8s.selectContext",
|
|
108
|
+
"title": "Select Kubernetes Context",
|
|
109
|
+
"category": "Norn"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"command": "norn.openTerminal",
|
|
113
|
+
"title": "Open Terminal",
|
|
114
|
+
"category": "Norn"
|
|
115
|
+
},
|
|
91
116
|
{
|
|
92
117
|
"command": "norn.nornenv.activate",
|
|
93
118
|
"title": "Activate Norn Environment",
|
|
@@ -260,6 +285,21 @@
|
|
|
260
285
|
"dark": "./images/nornapi-icon.svg"
|
|
261
286
|
},
|
|
262
287
|
"configuration": "./language-configuration.json"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"id": "nornk8s",
|
|
291
|
+
"aliases": [
|
|
292
|
+
"Norn Kubernetes",
|
|
293
|
+
"nornk8s"
|
|
294
|
+
],
|
|
295
|
+
"extensions": [
|
|
296
|
+
".nornk8s"
|
|
297
|
+
],
|
|
298
|
+
"icon": {
|
|
299
|
+
"light": "./images/nornk8s-icon.svg",
|
|
300
|
+
"dark": "./images/nornk8s-icon.svg"
|
|
301
|
+
},
|
|
302
|
+
"configuration": "./language-configuration.json"
|
|
263
303
|
}
|
|
264
304
|
],
|
|
265
305
|
"grammars": [
|
|
@@ -285,6 +325,11 @@
|
|
|
285
325
|
"language": "nornapi",
|
|
286
326
|
"scopeName": "source.nornapi",
|
|
287
327
|
"path": "./syntaxes/nornapi.tmLanguage.json"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"language": "nornk8s",
|
|
331
|
+
"scopeName": "source.nornk8s",
|
|
332
|
+
"path": "./syntaxes/nornk8s.tmLanguage.json"
|
|
288
333
|
}
|
|
289
334
|
],
|
|
290
335
|
"jsonValidation": [
|
|
@@ -363,8 +408,8 @@
|
|
|
363
408
|
"keybindings": [
|
|
364
409
|
{
|
|
365
410
|
"command": "norn.sendRequest",
|
|
366
|
-
"key": "
|
|
367
|
-
"mac": "
|
|
411
|
+
"key": "ctrl+alt+r",
|
|
412
|
+
"mac": "ctrl+alt+r",
|
|
368
413
|
"when": "editorLangId == norn"
|
|
369
414
|
},
|
|
370
415
|
{
|
|
@@ -372,14 +417,19 @@
|
|
|
372
417
|
"key": "enter",
|
|
373
418
|
"mac": "enter",
|
|
374
419
|
"when": "editorTextFocus && editorLangId == nornenv && !suggestWidgetVisible"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"command": "norn.openTerminal",
|
|
423
|
+
"key": "ctrl+alt+n",
|
|
424
|
+
"mac": "cmd+alt+n"
|
|
375
425
|
}
|
|
376
426
|
],
|
|
377
427
|
"chatParticipants": [
|
|
378
428
|
{
|
|
379
429
|
"id": "norn.chat",
|
|
380
430
|
"name": "norn",
|
|
381
|
-
"fullName": "Norn
|
|
382
|
-
"description": "Ask questions about Norn syntax, generate requests, explain sequences, and convert cURL/HTTP to Norn",
|
|
431
|
+
"fullName": "Norn",
|
|
432
|
+
"description": "Ask questions about Norn syntax, generate requests and runbooks, explain sequences, and convert cURL/HTTP to Norn",
|
|
383
433
|
"isSticky": true,
|
|
384
434
|
"commands": [
|
|
385
435
|
{
|
|
@@ -398,7 +448,7 @@
|
|
|
398
448
|
"disambiguation": [
|
|
399
449
|
{
|
|
400
450
|
"category": "norn",
|
|
401
|
-
"description": "The user wants help writing, understanding, or debugging Norn
|
|
451
|
+
"description": "The user wants help writing, understanding, or debugging Norn files (.norn, .nornapi, .nornsql, .nornk8s, .nornenv).",
|
|
402
452
|
"examples": [
|
|
403
453
|
"How do I write an assertion in Norn?",
|
|
404
454
|
"Generate a POST request with JSON body",
|
|
@@ -433,6 +483,16 @@
|
|
|
433
483
|
"type": "string"
|
|
434
484
|
},
|
|
435
485
|
"description": "Workspace-relative glob patterns for .norn files or folders to hide from the Norn Test Explorer. Useful for negative fixtures, live-only tests, or documentation demos."
|
|
486
|
+
},
|
|
487
|
+
"norn.terminal.focusMode": {
|
|
488
|
+
"type": "boolean",
|
|
489
|
+
"default": true,
|
|
490
|
+
"description": "Close the primary Side Bar and bottom Panel when opening the Norn terminal."
|
|
491
|
+
},
|
|
492
|
+
"norn.terminal.intelliSense.selectFirstSuggestion": {
|
|
493
|
+
"type": "boolean",
|
|
494
|
+
"default": false,
|
|
495
|
+
"description": "Select the first Norn terminal IntelliSense suggestion automatically. When disabled, Enter runs exactly what was typed until a suggestion is explicitly selected."
|
|
436
496
|
}
|
|
437
497
|
}
|
|
438
498
|
},
|
|
@@ -470,7 +530,7 @@
|
|
|
470
530
|
"lint": "eslint src",
|
|
471
531
|
"validate:skills": "node ./scripts/validate-skills.mjs",
|
|
472
532
|
"test": "vscode-test",
|
|
473
|
-
"test:regression": "node ./
|
|
533
|
+
"test:regression": "node ./tests/test-scripts/run-regression.js",
|
|
474
534
|
"test:prerelease": "npm test && npm run test:regression",
|
|
475
535
|
"publish:npm": "node -e \"const fs=require('fs');const p=require('./package.json');p.name='norn-cli';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\" && npm publish; exit_code=$?; node -e \"const fs=require('fs');const p=require('./package.json');p.name='norn';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\"; exit $exit_code",
|
|
476
536
|
"publish:vsce": "node -e \"const p=require('./package.json');p.name='norn';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\" && npx vsce publish",
|
package/AGENTS.md
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# Norn Extension - Copilot Instructions
|
|
2
|
-
|
|
3
|
-
These instructions apply to all conversations about the Norn VS Code extension.
|
|
4
|
-
|
|
5
|
-
## Repo Boundary
|
|
6
|
-
|
|
7
|
-
These instructions apply only inside `/Users/petercrest/Worktable/Projects/vsApi`.
|
|
8
|
-
|
|
9
|
-
- Do **not** apply them to standalone work in sibling repos such as `/Users/petercrest/Worktable/Projects/norn_website`.
|
|
10
|
-
- If a task is website-only, prefer the website repo's local instructions instead.
|
|
11
|
-
- Only use these rules for cross-repo work when the task explicitly includes `vsApi`.
|
|
12
|
-
|
|
13
|
-
## CLI Support is Mandatory
|
|
14
|
-
|
|
15
|
-
Every feature implementation must work in both:
|
|
16
|
-
1. **VS Code Extension** - Interactive UI with response panel
|
|
17
|
-
2. **CLI** (`src/cli.ts`) - Command-line execution
|
|
18
|
-
|
|
19
|
-
Before considering any feature complete, verify it works in the CLI. The CLI shares code with the extension (`parser.ts`, `sequenceRunner.ts`, `assertionRunner.ts`, `httpClient.ts`).
|
|
20
|
-
|
|
21
|
-
### CRITICAL: Running the Local CLI
|
|
22
|
-
|
|
23
|
-
**NEVER use `npx norn`** - this runs the globally installed npm package, NOT your local changes!
|
|
24
|
-
|
|
25
|
-
**ALWAYS use the local compiled CLI:**
|
|
26
|
-
```bash
|
|
27
|
-
# Correct - runs your local development version
|
|
28
|
-
node ./dist/cli.js tests/file.norn --env prelive
|
|
29
|
-
|
|
30
|
-
# WRONG - runs the published npm package, ignores your changes
|
|
31
|
-
npx norn tests/file.norn
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Before testing CLI changes:
|
|
35
|
-
1. Run `npm run compile` to build the latest code
|
|
36
|
-
2. Use `node ./dist/cli.js` to execute
|
|
37
|
-
|
|
38
|
-
## Skills Maintenance
|
|
39
|
-
|
|
40
|
-
When implementing features, check the `.github/skills/` directory for relevant skills:
|
|
41
|
-
|
|
42
|
-
- **If a skill is incorrect or outdated:** Update it with the correct information
|
|
43
|
-
- **If a skill is missing:** Create a new one following the Agent Skills format
|
|
44
|
-
- After creating or editing skills, run `npm run validate:skills`
|
|
45
|
-
|
|
46
|
-
Skills should capture lessons learned and patterns discovered during implementation to help future development.
|
|
47
|
-
|
|
48
|
-
## Code Style
|
|
49
|
-
|
|
50
|
-
- TypeScript with strict typing
|
|
51
|
-
- Simple IntelliSense completions (no complex snippets)
|
|
52
|
-
- Keep parsing logic in dedicated functions
|
|
53
|
-
- Test both extension and CLI after changes
|
|
54
|
-
|
|
55
|
-
## Test Verification
|
|
56
|
-
|
|
57
|
-
**MANDATORY:** When the user asks to publish, prepare a release, bump version, or create a patch:
|
|
58
|
-
1. **FIRST** invoke the Test Verification subagent using `runSubagent`
|
|
59
|
-
2. Wait for it to complete and report results
|
|
60
|
-
3. Only if all tests pass, invoke the Website Documentation Review subagent using `runSubagent`
|
|
61
|
-
4. Only proceed if tests pass and the docs review reports either:
|
|
62
|
-
- website docs were updated and the website build passed, or
|
|
63
|
-
- no docs changes were needed because the release is only bug fixes, refactors, or design/styling work
|
|
64
|
-
|
|
65
|
-
The Test Verification agent runs:
|
|
66
|
-
- `npm test` (must pass compile/lint, Extension Host editor diagnostics, Test Explorer support, and runtime-negative automation)
|
|
67
|
-
- `npm run test:regression` (local CLI Regression suite; all tests must pass)
|
|
68
|
-
|
|
69
|
-
The Website Documentation Review agent:
|
|
70
|
-
- reviews release changes in `/Users/petercrest/Worktable/Projects/vsApi`
|
|
71
|
-
- updates `/Users/petercrest/Worktable/Projects/norn_website` when user-facing features changed or new ones were added
|
|
72
|
-
- skips docs work for bug fixes, refactors, internal cleanup, and design/styling-only changes
|
|
73
|
-
|
|
74
|
-
**Trigger words:** publish, release, version, patch, bump, deploy
|
|
75
|
-
|
|
76
|
-
## Publishing Ownership
|
|
77
|
-
|
|
78
|
-
When release verification and version prep are complete, **do not run publishing commands** (`npm run publish:npm`, `npm run publish:vsce`, `npm run publish:all`).
|
|
79
|
-
|
|
80
|
-
The **user runs publish commands manually**.
|
|
81
|
-
|
|
82
|
-
## Key Files
|
|
83
|
-
|
|
84
|
-
| Purpose | File |
|
|
85
|
-
|---------|------|
|
|
86
|
-
| Syntax highlighting | `syntaxes/norn.tmLanguage.json` |
|
|
87
|
-
| IntelliSense | `src/completionProvider.ts` |
|
|
88
|
-
| Sequence execution | `src/sequenceRunner.ts` |
|
|
89
|
-
| Assertions | `src/assertionRunner.ts` |
|
|
90
|
-
| HTTP requests | `src/httpClient.ts` |
|
|
91
|
-
| CLI | `src/cli.ts` |
|
|
92
|
-
| Parser | `src/parser.ts` |
|
|
93
|
-
| Response panel | `src/responsePanel.ts` |
|
|
94
|
-
| Diagnostics | `src/diagnosticProvider.ts` |
|
|
95
|
-
| Test Explorer | `src/testProvider.ts` |
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
const chunks = [];
|
|
2
|
-
|
|
3
|
-
process.stdin.on('data', chunk => {
|
|
4
|
-
chunks.push(chunk);
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
process.stdin.on('end', () => {
|
|
8
|
-
try {
|
|
9
|
-
const request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
10
|
-
const values = request.connection && request.connection.values ? request.connection.values : {};
|
|
11
|
-
|
|
12
|
-
if (!values.server || !values.database || !values.user || !values.password) {
|
|
13
|
-
process.stdout.write(JSON.stringify({
|
|
14
|
-
success: false,
|
|
15
|
-
error: 'Missing expected buyerDemoDb connection values'
|
|
16
|
-
}));
|
|
17
|
-
process.exit(0);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
switch (request.operation.name) {
|
|
22
|
-
case 'ListBuyerAccounts': {
|
|
23
|
-
const segment = request.params.segment;
|
|
24
|
-
process.stdout.write(JSON.stringify({
|
|
25
|
-
success: true,
|
|
26
|
-
result: {
|
|
27
|
-
kind: 'rows',
|
|
28
|
-
rowCount: 2,
|
|
29
|
-
rows: [
|
|
30
|
-
{
|
|
31
|
-
Id: 1,
|
|
32
|
-
Company: 'Northwind Retail',
|
|
33
|
-
Email: 'northwind@example.com',
|
|
34
|
-
Segment: segment
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
Id: 2,
|
|
38
|
-
Company: 'Contoso Services',
|
|
39
|
-
Email: 'contoso@example.com',
|
|
40
|
-
Segment: segment
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
}
|
|
44
|
-
}));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
case 'RecordDemoRun':
|
|
49
|
-
process.stdout.write(JSON.stringify({
|
|
50
|
-
success: true,
|
|
51
|
-
result: {
|
|
52
|
-
kind: 'exec',
|
|
53
|
-
affectedRows: 1
|
|
54
|
-
}
|
|
55
|
-
}));
|
|
56
|
-
return;
|
|
57
|
-
|
|
58
|
-
default:
|
|
59
|
-
process.stdout.write(JSON.stringify({
|
|
60
|
-
success: false,
|
|
61
|
-
error: `Unknown buyer showcase SQL operation: ${request.operation.name}`
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
process.stdout.write(JSON.stringify({
|
|
66
|
-
success: false,
|
|
67
|
-
error: error instanceof Error ? error.message : String(error)
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
});
|
|
Binary file
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
const args = process.argv.slice(2);
|
|
7
|
-
|
|
8
|
-
function getArg(flag, fallback) {
|
|
9
|
-
const index = args.indexOf(flag);
|
|
10
|
-
if (index === -1 || index === args.length - 1) {
|
|
11
|
-
return fallback;
|
|
12
|
-
}
|
|
13
|
-
return args[index + 1];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const seconds = Number(getArg('--seconds', '10'));
|
|
17
|
-
const outPath = path.resolve(getArg('--out', 'audio/norn-coding-bed-test.wav'));
|
|
18
|
-
const sampleRate = 44100;
|
|
19
|
-
const bpm = 122;
|
|
20
|
-
const beatLength = 60 / bpm;
|
|
21
|
-
const barLength = beatLength * 4;
|
|
22
|
-
const totalSamples = Math.floor(seconds * sampleRate);
|
|
23
|
-
const left = new Float32Array(totalSamples);
|
|
24
|
-
const right = new Float32Array(totalSamples);
|
|
25
|
-
|
|
26
|
-
const progression = [
|
|
27
|
-
{ root: 45, chord: [57, 60, 64], accent: [69, 72, 76] }, // Am
|
|
28
|
-
{ root: 41, chord: [53, 57, 60], accent: [65, 69, 72] }, // F
|
|
29
|
-
{ root: 36, chord: [48, 52, 55], accent: [64, 67, 72] }, // C
|
|
30
|
-
{ root: 43, chord: [55, 59, 62], accent: [67, 71, 74] }, // G
|
|
31
|
-
{ root: 45, chord: [57, 60, 64], accent: [69, 72, 76] }, // Am
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
let noiseSeed = 0x12345678;
|
|
35
|
-
|
|
36
|
-
function nextNoise() {
|
|
37
|
-
noiseSeed = (1664525 * noiseSeed + 1013904223) >>> 0;
|
|
38
|
-
return (noiseSeed / 0xffffffff) * 2 - 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function midiToHz(note) {
|
|
42
|
-
return 440 * Math.pow(2, (note - 69) / 12);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function clamp(value) {
|
|
46
|
-
return Math.max(-1, Math.min(1, value));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function softClip(value) {
|
|
50
|
-
return Math.tanh(value * 1.4);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function oscSine(freq, time) {
|
|
54
|
-
return Math.sin(2 * Math.PI * freq * time);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function oscTri(freq, time) {
|
|
58
|
-
return (2 / Math.PI) * Math.asin(Math.sin(2 * Math.PI * freq * time));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function oscSaw(freq, time) {
|
|
62
|
-
const phase = freq * time;
|
|
63
|
-
return 2 * (phase - Math.floor(phase + 0.5));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function expEnv(timeSince, decay) {
|
|
67
|
-
return timeSince < 0 ? 0 : Math.exp(-timeSince * decay);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function kickAt(timeSince) {
|
|
71
|
-
if (timeSince < 0 || timeSince > 0.24) {
|
|
72
|
-
return 0;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const pitch = 132 - 78 * Math.min(timeSince / 0.08, 1);
|
|
76
|
-
const body = oscSine(pitch, timeSince) * expEnv(timeSince, 17);
|
|
77
|
-
const click = oscTri(1900, timeSince) * expEnv(timeSince, 95) * 0.18;
|
|
78
|
-
return body * 0.95 + click;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function snareAt(timeSince) {
|
|
82
|
-
if (timeSince < 0 || timeSince > 0.22) {
|
|
83
|
-
return 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const noise = nextNoise() * expEnv(timeSince, 22);
|
|
87
|
-
const body = oscSine(198, timeSince) * expEnv(timeSince, 28) * 0.22;
|
|
88
|
-
return noise * 0.34 + body;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function hatAt(timeSince, open) {
|
|
92
|
-
const length = open ? 0.16 : 0.06;
|
|
93
|
-
if (timeSince < 0 || timeSince > length) {
|
|
94
|
-
return 0;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const decay = open ? 26 : 54;
|
|
98
|
-
const raw = nextNoise();
|
|
99
|
-
const brightness = oscSine(9200, timeSince) * 0.08 + oscSine(6800, timeSince) * 0.06;
|
|
100
|
-
return (raw * 0.17 + brightness) * expEnv(timeSince, decay);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function sidechainAmount(time) {
|
|
104
|
-
const phase = time % beatLength;
|
|
105
|
-
return 1 - 0.28 * Math.exp(-phase * 15);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function bassNoteForStep(barIndex, stepInBar) {
|
|
109
|
-
const current = progression[barIndex % progression.length];
|
|
110
|
-
const root = current.root;
|
|
111
|
-
const fifth = root + 7;
|
|
112
|
-
const octave = root + 12;
|
|
113
|
-
const pattern = [root, root, fifth, root, octave, root, fifth, root];
|
|
114
|
-
return midiToHz(pattern[stepInBar % pattern.length]);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function arpNoteForStep(barIndex, stepInBar) {
|
|
118
|
-
const current = progression[barIndex % progression.length];
|
|
119
|
-
const pattern = [0, 1, 2, 1, 2, 1, 0, 2];
|
|
120
|
-
return midiToHz(current.accent[pattern[stepInBar % pattern.length]]);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (let sample = 0; sample < totalSamples; sample += 1) {
|
|
124
|
-
const time = sample / sampleRate;
|
|
125
|
-
const barIndex = Math.floor(time / barLength) % progression.length;
|
|
126
|
-
const barTime = time % barLength;
|
|
127
|
-
const current = progression[barIndex];
|
|
128
|
-
|
|
129
|
-
let mixL = 0;
|
|
130
|
-
let mixR = 0;
|
|
131
|
-
|
|
132
|
-
// Pad chord.
|
|
133
|
-
let pad = 0;
|
|
134
|
-
for (const note of current.chord) {
|
|
135
|
-
const freq = midiToHz(note);
|
|
136
|
-
pad += oscSine(freq, time) * 0.12;
|
|
137
|
-
pad += oscSine(freq * 0.5, time) * 0.035;
|
|
138
|
-
}
|
|
139
|
-
pad += oscSine(midiToHz(current.root), time) * 0.03;
|
|
140
|
-
pad *= sidechainAmount(time) * 0.7;
|
|
141
|
-
mixL += pad;
|
|
142
|
-
mixR += pad;
|
|
143
|
-
|
|
144
|
-
// Bass pulse on eighth notes.
|
|
145
|
-
const bassStepLength = beatLength / 2;
|
|
146
|
-
const bassStep = Math.floor(barTime / bassStepLength);
|
|
147
|
-
const bassStepStart = bassStep * bassStepLength;
|
|
148
|
-
const bassTimeSince = barTime - bassStepStart;
|
|
149
|
-
const bassGate = bassTimeSince < 0.21 ? 1 : 0;
|
|
150
|
-
const bassFreq = bassNoteForStep(barIndex, bassStep);
|
|
151
|
-
const bassEnv = expEnv(bassTimeSince, 9) * bassGate;
|
|
152
|
-
const bass =
|
|
153
|
-
(oscTri(bassFreq, time) * 0.22 + oscSine(bassFreq * 0.5, time) * 0.1) *
|
|
154
|
-
bassEnv *
|
|
155
|
-
sidechainAmount(time);
|
|
156
|
-
mixL += bass;
|
|
157
|
-
mixR += bass;
|
|
158
|
-
|
|
159
|
-
// Bright arpeggio.
|
|
160
|
-
const arpStepLength = beatLength / 2;
|
|
161
|
-
const arpStep = Math.floor(barTime / arpStepLength);
|
|
162
|
-
const arpStepStart = arpStep * arpStepLength;
|
|
163
|
-
const arpTimeSince = barTime - arpStepStart;
|
|
164
|
-
const arpFreq = arpNoteForStep(barIndex, arpStep);
|
|
165
|
-
const arpEnv = Math.min(1, arpTimeSince * 40) * expEnv(arpTimeSince, 10);
|
|
166
|
-
const arp =
|
|
167
|
-
(oscSaw(arpFreq, time) * 0.07 + oscSine(arpFreq * 2, time) * 0.05 + oscSine(arpFreq, time) * 0.04) *
|
|
168
|
-
arpEnv;
|
|
169
|
-
const arpPan = arpStep % 2 === 0 ? -0.22 : 0.22;
|
|
170
|
-
mixL += arp * (1 - arpPan);
|
|
171
|
-
mixR += arp * (1 + arpPan);
|
|
172
|
-
|
|
173
|
-
// Kick every beat with an extra pickup before the fourth beat.
|
|
174
|
-
const kickPattern = [0, 1, 2, 2.75, 3];
|
|
175
|
-
let kick = 0;
|
|
176
|
-
for (const beat of kickPattern) {
|
|
177
|
-
kick += kickAt(barTime - beat * beatLength);
|
|
178
|
-
}
|
|
179
|
-
mixL += kick * 0.92;
|
|
180
|
-
mixR += kick * 0.92;
|
|
181
|
-
|
|
182
|
-
// Snare on beats 2 and 4.
|
|
183
|
-
let snare = 0;
|
|
184
|
-
for (const beat of [1, 3]) {
|
|
185
|
-
snare += snareAt(barTime - beat * beatLength);
|
|
186
|
-
}
|
|
187
|
-
mixL += snare * 0.75;
|
|
188
|
-
mixR += snare * 0.75;
|
|
189
|
-
|
|
190
|
-
// Hats on offbeats with occasional longer accent.
|
|
191
|
-
let hat = 0;
|
|
192
|
-
for (let step = 0; step < 8; step += 1) {
|
|
193
|
-
const start = step * bassStepLength;
|
|
194
|
-
const open = step === 3 || step === 7;
|
|
195
|
-
hat += hatAt(barTime - start, open);
|
|
196
|
-
}
|
|
197
|
-
mixL += hat * 0.4;
|
|
198
|
-
mixR += hat * 0.38;
|
|
199
|
-
|
|
200
|
-
// Gentle master fade in/out.
|
|
201
|
-
const fadeIn = Math.min(1, time / 0.35);
|
|
202
|
-
const fadeOut = Math.min(1, (seconds - time) / 0.5);
|
|
203
|
-
const masterEnv = Math.max(0, Math.min(fadeIn, fadeOut));
|
|
204
|
-
|
|
205
|
-
left[sample] = softClip(mixL * masterEnv);
|
|
206
|
-
right[sample] = softClip(mixR * masterEnv);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let peak = 0;
|
|
210
|
-
for (let i = 0; i < totalSamples; i += 1) {
|
|
211
|
-
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const targetPeak = 0.92;
|
|
215
|
-
const gain = peak > 0 ? targetPeak / peak : 1;
|
|
216
|
-
|
|
217
|
-
const pcmData = Buffer.alloc(totalSamples * 4);
|
|
218
|
-
for (let i = 0; i < totalSamples; i += 1) {
|
|
219
|
-
const l = Math.round(clamp(left[i] * gain) * 32767);
|
|
220
|
-
const r = Math.round(clamp(right[i] * gain) * 32767);
|
|
221
|
-
pcmData.writeInt16LE(l, i * 4);
|
|
222
|
-
pcmData.writeInt16LE(r, i * 4 + 2);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const header = Buffer.alloc(44);
|
|
226
|
-
header.write('RIFF', 0);
|
|
227
|
-
header.writeUInt32LE(36 + pcmData.length, 4);
|
|
228
|
-
header.write('WAVE', 8);
|
|
229
|
-
header.write('fmt ', 12);
|
|
230
|
-
header.writeUInt32LE(16, 16);
|
|
231
|
-
header.writeUInt16LE(1, 20);
|
|
232
|
-
header.writeUInt16LE(2, 22);
|
|
233
|
-
header.writeUInt32LE(sampleRate, 24);
|
|
234
|
-
header.writeUInt32LE(sampleRate * 4, 28);
|
|
235
|
-
header.writeUInt16LE(4, 32);
|
|
236
|
-
header.writeUInt16LE(16, 34);
|
|
237
|
-
header.write('data', 36);
|
|
238
|
-
header.writeUInt32LE(pcmData.length, 40);
|
|
239
|
-
|
|
240
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
241
|
-
fs.writeFileSync(outPath, Buffer.concat([header, pcmData]));
|
|
242
|
-
|
|
243
|
-
console.log(`Wrote ${outPath}`);
|