norn-cli 2.6.1 → 2.8.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 +37 -0
- package/README.md +37 -2
- package/dist/cli.js +804 -16
- package/foo.ps1 +1 -0
- package/package.json +81 -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 -490
- package/scripts/validate-skills.mjs +0 -50
package/foo.ps1
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Write-Host "Hello"
|
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.8.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,8 @@
|
|
|
52
56
|
"onCommand:norn.createStarterCatalog",
|
|
53
57
|
"onCommand:norn.createSqlStarterFiles",
|
|
54
58
|
"onCommand:norn.createMcpStarterConfig",
|
|
59
|
+
"onCommand:norn.openTerminal",
|
|
60
|
+
"onCommand:norn.terminal.focusRawTerminal",
|
|
55
61
|
"onCommand:norn.debugSequence",
|
|
56
62
|
"onDebugResolve:norn"
|
|
57
63
|
],
|
|
@@ -88,6 +94,31 @@
|
|
|
88
94
|
"title": "Select Environment",
|
|
89
95
|
"category": "Norn"
|
|
90
96
|
},
|
|
97
|
+
{
|
|
98
|
+
"command": "norn.k8s.runLine",
|
|
99
|
+
"title": "Run Kubernetes Command",
|
|
100
|
+
"category": "Norn"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"command": "norn.k8s.runSequence",
|
|
104
|
+
"title": "Run Kubernetes Sequence",
|
|
105
|
+
"category": "Norn"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"command": "norn.k8s.selectContext",
|
|
109
|
+
"title": "Select Kubernetes Context",
|
|
110
|
+
"category": "Norn"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"command": "norn.openTerminal",
|
|
114
|
+
"title": "Open Terminal",
|
|
115
|
+
"category": "Norn"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"command": "norn.terminal.focusRawTerminal",
|
|
119
|
+
"title": "Focus Raw Terminal",
|
|
120
|
+
"category": "Norn"
|
|
121
|
+
},
|
|
91
122
|
{
|
|
92
123
|
"command": "norn.nornenv.activate",
|
|
93
124
|
"title": "Activate Norn Environment",
|
|
@@ -260,6 +291,21 @@
|
|
|
260
291
|
"dark": "./images/nornapi-icon.svg"
|
|
261
292
|
},
|
|
262
293
|
"configuration": "./language-configuration.json"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"id": "nornk8s",
|
|
297
|
+
"aliases": [
|
|
298
|
+
"Norn Kubernetes",
|
|
299
|
+
"nornk8s"
|
|
300
|
+
],
|
|
301
|
+
"extensions": [
|
|
302
|
+
".nornk8s"
|
|
303
|
+
],
|
|
304
|
+
"icon": {
|
|
305
|
+
"light": "./images/nornk8s-icon.svg",
|
|
306
|
+
"dark": "./images/nornk8s-icon.svg"
|
|
307
|
+
},
|
|
308
|
+
"configuration": "./language-configuration.json"
|
|
263
309
|
}
|
|
264
310
|
],
|
|
265
311
|
"grammars": [
|
|
@@ -285,6 +331,11 @@
|
|
|
285
331
|
"language": "nornapi",
|
|
286
332
|
"scopeName": "source.nornapi",
|
|
287
333
|
"path": "./syntaxes/nornapi.tmLanguage.json"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"language": "nornk8s",
|
|
337
|
+
"scopeName": "source.nornk8s",
|
|
338
|
+
"path": "./syntaxes/nornk8s.tmLanguage.json"
|
|
288
339
|
}
|
|
289
340
|
],
|
|
290
341
|
"jsonValidation": [
|
|
@@ -363,8 +414,8 @@
|
|
|
363
414
|
"keybindings": [
|
|
364
415
|
{
|
|
365
416
|
"command": "norn.sendRequest",
|
|
366
|
-
"key": "
|
|
367
|
-
"mac": "
|
|
417
|
+
"key": "ctrl+alt+r",
|
|
418
|
+
"mac": "ctrl+alt+r",
|
|
368
419
|
"when": "editorLangId == norn"
|
|
369
420
|
},
|
|
370
421
|
{
|
|
@@ -372,14 +423,19 @@
|
|
|
372
423
|
"key": "enter",
|
|
373
424
|
"mac": "enter",
|
|
374
425
|
"when": "editorTextFocus && editorLangId == nornenv && !suggestWidgetVisible"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"command": "norn.openTerminal",
|
|
429
|
+
"key": "ctrl+alt+n",
|
|
430
|
+
"mac": "cmd+alt+n"
|
|
375
431
|
}
|
|
376
432
|
],
|
|
377
433
|
"chatParticipants": [
|
|
378
434
|
{
|
|
379
435
|
"id": "norn.chat",
|
|
380
436
|
"name": "norn",
|
|
381
|
-
"fullName": "Norn
|
|
382
|
-
"description": "Ask questions about Norn syntax, generate requests, explain sequences, and convert cURL/HTTP to Norn",
|
|
437
|
+
"fullName": "Norn",
|
|
438
|
+
"description": "Ask questions about Norn syntax, generate requests and runbooks, explain sequences, and convert cURL/HTTP to Norn",
|
|
383
439
|
"isSticky": true,
|
|
384
440
|
"commands": [
|
|
385
441
|
{
|
|
@@ -398,7 +454,7 @@
|
|
|
398
454
|
"disambiguation": [
|
|
399
455
|
{
|
|
400
456
|
"category": "norn",
|
|
401
|
-
"description": "The user wants help writing, understanding, or debugging Norn
|
|
457
|
+
"description": "The user wants help writing, understanding, or debugging Norn files (.norn, .nornapi, .nornsql, .nornk8s, .nornenv).",
|
|
402
458
|
"examples": [
|
|
403
459
|
"How do I write an assertion in Norn?",
|
|
404
460
|
"Generate a POST request with JSON body",
|
|
@@ -433,6 +489,16 @@
|
|
|
433
489
|
"type": "string"
|
|
434
490
|
},
|
|
435
491
|
"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."
|
|
492
|
+
},
|
|
493
|
+
"norn.terminal.focusMode": {
|
|
494
|
+
"type": "boolean",
|
|
495
|
+
"default": true,
|
|
496
|
+
"description": "Close the primary Side Bar and bottom Panel when opening the Norn terminal."
|
|
497
|
+
},
|
|
498
|
+
"norn.terminal.intelliSense.selectFirstSuggestion": {
|
|
499
|
+
"type": "boolean",
|
|
500
|
+
"default": false,
|
|
501
|
+
"description": "Select the first Norn terminal IntelliSense suggestion automatically. When disabled, Enter runs exactly what was typed until a suggestion is explicitly selected."
|
|
436
502
|
}
|
|
437
503
|
}
|
|
438
504
|
},
|
|
@@ -459,6 +525,8 @@
|
|
|
459
525
|
"vscode:prepublish": "npm run package",
|
|
460
526
|
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
|
461
527
|
"compile:cli": "npm run check-types && node esbuild.js --cli",
|
|
528
|
+
"stage:pty-spike-native": "node ./scripts/stage-pty-spike-native.mjs",
|
|
529
|
+
"package:pty-spike": "npm run package && npm run stage:pty-spike-native",
|
|
462
530
|
"watch": "npm-run-all -p watch:*",
|
|
463
531
|
"watch:esbuild": "node esbuild.js --watch",
|
|
464
532
|
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
|
@@ -470,7 +538,7 @@
|
|
|
470
538
|
"lint": "eslint src",
|
|
471
539
|
"validate:skills": "node ./scripts/validate-skills.mjs",
|
|
472
540
|
"test": "vscode-test",
|
|
473
|
-
"test:regression": "node ./
|
|
541
|
+
"test:regression": "node ./tests/test-scripts/run-regression.js",
|
|
474
542
|
"test:prerelease": "npm test && npm run test:regression",
|
|
475
543
|
"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
544
|
"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",
|
|
@@ -485,8 +553,11 @@
|
|
|
485
553
|
"@types/vscode": "^1.108.1",
|
|
486
554
|
"@vscode/test-cli": "^0.0.12",
|
|
487
555
|
"@vscode/test-electron": "^2.5.2",
|
|
556
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
557
|
+
"@xterm/xterm": "^6.0.0",
|
|
488
558
|
"esbuild": "^0.27.2",
|
|
489
559
|
"eslint": "^9.39.2",
|
|
560
|
+
"node-pty": "^1.1.0",
|
|
490
561
|
"npm-run-all": "^4.1.5",
|
|
491
562
|
"typescript": "^5.9.3",
|
|
492
563
|
"typescript-eslint": "^8.52.0"
|
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}`);
|