} inputs
124 | * @param {HTMLElement} nodeEditor
125 | */
126 | function importLinks(content, outputs, inputs, nodeEditor) {
127 | content.links.forEach((link) => {
128 | const graphLink = /** @type {GraphLink} */ (document.createElement('w-graph-link'))
129 | graphLink.from = outputs.get(link.from)
130 | graphLink.to = inputs.get(link.to)
131 | nodeEditor.appendChild(graphLink)
132 | nodeEditor.dispatchEvent(
133 | new CustomEvent('graph-link-connect', {
134 | detail: {
135 | from: graphLink.from,
136 | to: graphLink.to,
137 | },
138 | }),
139 | )
140 | })
141 | }
142 |
143 | /**
144 | *
145 | * @param {FileContent} content
146 | * @param {HTMLElement} audioNodeEditor
147 | * @param {AudioTracker} audioTracker
148 | */
149 | export default function importFile(content, audioTracker, audioNodeEditor) {
150 | /** @type {HTMLElement} */
151 | const nodeEditor = audioNodeEditor.shadowRoot.querySelector('w-node-editor')
152 |
153 | clearAll(audioTracker, audioNodeEditor)
154 | importTracker(content, audioTracker)
155 | const trackLabels = importTracks(content, audioTracker)
156 | importAttributes(content.nodeEditor, nodeEditor)
157 | importAudioFiles(content)
158 | const { nodeOutputs, nodeInputs } = importNodes(content, trackLabels, nodeEditor)
159 | let wasAudioNodeEditorHidden = false
160 | if (audioNodeEditor.hidden) {
161 | wasAudioNodeEditorHidden = true
162 |
163 | // Audio node editor must not be hidden for the graph links
164 | // to be positionned correctly
165 | audioNodeEditor.hidden = false
166 | }
167 | importLinks(content, nodeOutputs, nodeInputs, nodeEditor)
168 | if (wasAudioNodeEditorHidden) {
169 | audioNodeEditor.hidden = true
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/audio-tracker/audio-tracker.js:
--------------------------------------------------------------------------------
1 | import { css, defineCustomElement, html } from '../shared/core/element.js'
2 | import useKeyboardNavigation from './use-keyboard-navigation.js'
3 |
4 | /**
5 | * @typedef {import('../shared/base/number-field.js').default} NumberField
6 | * @typedef {import('../shared/base/menu.js').default} Menu
7 | * @typedef {import('./audio-track.js').default} AudioTrack
8 | * @typedef {import('./track-effect.js').default} TrackEffect
9 | */
10 |
11 | const tempoLabel = 'Tempo'
12 | const linesLabel = 'Lines'
13 | const linesPerBeatLabel = 'Lines per beat'
14 |
15 | export default defineCustomElement('audio-tracker', {
16 | styles: css`
17 | w-fab {
18 | position: absolute;
19 | left: 8px;
20 | opacity: 0;
21 | transform: translateY(-50%) scale(0.3);
22 | transition: opacity 75ms linear 75ms, transform 150ms var(--easing-accelerated);
23 | --color-fab: var(--color-secondary);
24 | --color-on-fab: var(--color-on-secondary);
25 | }
26 |
27 | :host([active]) w-fab {
28 | opacity: 1;
29 | transform: translateY(-50%);
30 | transition: opacity 75ms linear, transform 150ms var(--easing-decelerated);
31 | }
32 |
33 | .root {
34 | position: absolute;
35 | top: 0;
36 | right: 0;
37 | bottom: 0;
38 | left: 0;
39 | display: flex;
40 | background-color: rgb(var(--color-surface));
41 | opacity: 0;
42 | transform: translateX(-100px);
43 | transition: opacity 150ms var(--easing-accelerated), transform 150ms var(--easing-accelerated);
44 | }
45 |
46 | :host([active]) .root {
47 | opacity: 1;
48 | transform: none;
49 | transition-timing-function: var(--easing-decelerated), var(--easing-decelerated);
50 | }
51 |
52 | aside {
53 | margin: 32px 16px 16px 16px;
54 | width: 160px;
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | .tracks {
60 | flex: 1;
61 | display: flex;
62 | align-items: flex-start;
63 | overflow: auto;
64 | }
65 |
66 | .tracks > *:last-child::after {
67 | content: '';
68 | position: absolute;
69 | right: -16px;
70 | bottom: -16px;
71 | width: 1px;
72 | height: 1px;
73 | }
74 | `,
75 | template: html`
76 |
77 | add
78 | Add track
79 |
80 |
88 |
89 |
90 | delete
91 | Delete track
92 |
93 |
94 | `,
95 | properties: {
96 | active: Boolean,
97 | tempo: Number,
98 | lines: Number,
99 | linesPerBeat: Number,
100 | },
101 | setup({ host, observe }) {
102 | /** @type {HTMLElement} */
103 | const fab = host.shadowRoot.querySelector('w-fab')
104 |
105 | /** @type {HTMLElement} */
106 | const tracks = host.shadowRoot.querySelector('.tracks')
107 |
108 | /** @type {NumberField} */
109 | const tempoField = host.shadowRoot.querySelector(`w-number-field[label='${tempoLabel}']`)
110 |
111 | /** @type {NumberField} */
112 | const linesField = host.shadowRoot.querySelector(`w-number-field[label='${linesLabel}']`)
113 |
114 | /** @type {NumberField} */
115 | const linesPerBeatField = host.shadowRoot.querySelector(`w-number-field[label='${linesPerBeatLabel}']`)
116 |
117 | /** @type {Menu} */
118 | const menu = host.shadowRoot.querySelector('w-menu')
119 |
120 | /** @type {HTMLElement} */
121 | const menuItemDelete = host.shadowRoot.querySelector('#delete')
122 |
123 | useKeyboardNavigation(tracks)
124 |
125 | /** @type {AudioTrack} */
126 | let selectedAudioTrack = null
127 |
128 | observe('tempo', () => {
129 | tempoField.value = host.tempo
130 | })
131 |
132 | observe('lines', () => {
133 | linesField.value = host.lines
134 | tracks.querySelectorAll('audio-track').forEach((audioTrack) => {
135 | const trackEffects = audioTrack.querySelectorAll('track-effect')
136 | if (trackEffects.length < host.lines) {
137 | for (let i = trackEffects.length; i < host.lines; i++) {
138 | const trackEffect = /** @type {TrackEffect} */ (document.createElement('track-effect'))
139 | trackEffect.beat = i % host.linesPerBeat === 0
140 | audioTrack.appendChild(trackEffect)
141 | }
142 | } else {
143 | trackEffects.forEach((trackEffect, i) => {
144 | if (i >= host.lines) {
145 | trackEffect.remove()
146 | }
147 | })
148 | }
149 | })
150 | })
151 |
152 | observe('linesPerBeat', () => {
153 | linesPerBeatField.value = host.linesPerBeat
154 | tracks.querySelectorAll('audio-track').forEach((audioTrack) => {
155 | audioTrack.querySelectorAll('track-effect').forEach((/** @type {TrackEffect} */ trackEffect, i) => {
156 | trackEffect.beat = i % host.linesPerBeat === 0
157 | })
158 | })
159 | })
160 |
161 | fab.addEventListener('click', () => {
162 | const audioTrack = /** @type {AudioTrack} */ (document.createElement('audio-track'))
163 | for (let i = 0; i < host.lines; i++) {
164 | const trackEffect = /** @type {TrackEffect} */ (document.createElement('track-effect'))
165 | trackEffect.beat = i % host.linesPerBeat === 0
166 | audioTrack.appendChild(trackEffect)
167 | }
168 | tracks.appendChild(audioTrack)
169 | })
170 |
171 | tempoField.addEventListener('input', () => {
172 | host.tempo = tempoField.value
173 | })
174 |
175 | linesField.addEventListener('input', () => {
176 | host.lines = linesField.value
177 | })
178 |
179 | linesPerBeatField.addEventListener('input', () => {
180 | host.linesPerBeat = linesPerBeatField.value
181 | })
182 |
183 | tracks.addEventListener('contextmenu', (event) => {
184 | const element = /** @type {Element} */ (event.target)
185 | selectedAudioTrack = element.closest('audio-track')
186 | if (!selectedAudioTrack) {
187 | return
188 | }
189 | menu.open = true
190 | menu.x = event.clientX
191 | menu.y = event.clientY
192 | })
193 |
194 | menuItemDelete.addEventListener('click', () => {
195 | selectedAudioTrack.remove()
196 | selectedAudioTrack = null
197 | })
198 | },
199 | })
200 |
--------------------------------------------------------------------------------
/src/core/audio-editor.js:
--------------------------------------------------------------------------------
1 | import useAudioTracker from '../audio-tracker/use-audio-tracker.js'
2 | import exportFile from '../helpers/export-file.js'
3 | import { clearAll } from '../helpers/file-helper.js'
4 | import importFile from '../helpers/import-file.js'
5 | import { css, defineCustomElement, html } from '../shared/core/element.js'
6 | import elevation from '../shared/core/elevation.js'
7 |
8 | /**
9 | * @typedef {import('../shared/base/tab.js').default} Tab
10 | * @typedef {import('../shared/base/menu.js').default} Menu
11 | * @typedef {import('./button-play-pause.js').default} ButtonPlayPause
12 | * @typedef {import('../audio-tracker/audio-tracker.js').default} AudioTracker
13 | * @typedef {import('../audio-node-editor/audio-node-editor.js').default} AudioNodeEditor
14 | * @typedef {import('../helpers/import-file.js').FileContent} FileContent
15 | */
16 |
17 | export default defineCustomElement('audio-editor', {
18 | styles: css`
19 | :host {
20 | height: 100%;
21 | display: flex;
22 | flex-direction: column;
23 | overflow: hidden;
24 | }
25 |
26 | header {
27 | position: relative;
28 | display: flex;
29 | justify-content: center;
30 | background-color: rgb(var(--color-surface));
31 | ${elevation(4)}
32 | }
33 |
34 | .actions {
35 | position: absolute;
36 | top: 0;
37 | right: 4px;
38 | height: 100%;
39 | display: flex;
40 | align-items: center;
41 | }
42 |
43 | .actions > *:not(w-menu) {
44 | margin: 0 6px;
45 | }
46 |
47 | w-menu {
48 | position: absolute;
49 | top: 100%;
50 | right: 0;
51 | }
52 |
53 | main {
54 | position: relative;
55 | flex: 1;
56 | }
57 | `,
58 | template: html`
59 |
84 |
85 |
86 |
87 |
88 | `,
89 | setup({ host }) {
90 | const [tracksTab, nodesTab] = /** @type {NodeListOf} */ (host.shadowRoot.querySelectorAll('w-tab'))
91 |
92 | const buttonPlayPause = /** @type {ButtonPlayPause} */ (host.shadowRoot.querySelector('button-play-pause'))
93 |
94 | /** @type {HTMLElement} */
95 | const buttonMore = host.shadowRoot.querySelector('w-icon-button')
96 |
97 | /** @type {Menu} */
98 | const menu = host.shadowRoot.querySelector('w-menu')
99 |
100 | /** @type {HTMLElement} */
101 | const menuItemNew = host.shadowRoot.querySelector('#new')
102 |
103 | /** @type {HTMLElement} */
104 | const menuItemOpen = host.shadowRoot.querySelector('#open')
105 |
106 | /** @type {HTMLElement} */
107 | const menuItemExport = host.shadowRoot.querySelector('#export')
108 |
109 | const audioTracker = /** @type {AudioTracker} */ (host.shadowRoot.querySelector('audio-tracker'))
110 | const audioNodeEditor = /** @type {AudioNodeEditor} */ (host.shadowRoot.querySelector('audio-node-editor'))
111 |
112 | /** @type {boolean} */
113 | let isMenuOpenOnMouseDown
114 |
115 | const { startAudioTracker, stopAudioTracker, isAudioTrackerStarted } = useAudioTracker(audioTracker)
116 |
117 | function play() {
118 | startAudioTracker()
119 | buttonPlayPause.active = true
120 | }
121 |
122 | function pause() {
123 | stopAudioTracker()
124 | buttonPlayPause.active = false
125 | }
126 |
127 | host.addEventListener('contextmenu', (event) => {
128 | event.preventDefault()
129 | })
130 |
131 | tracksTab.addEventListener('click', () => {
132 | nodesTab.active = false
133 | audioTracker.hidden = false
134 | audioNodeEditor.active = false
135 | setTimeout(() => {
136 | tracksTab.active = true
137 | audioTracker.active = true
138 | audioNodeEditor.hidden = true
139 | }, 150)
140 | })
141 |
142 | nodesTab.addEventListener('click', () => {
143 | tracksTab.active = false
144 | audioTracker.active = false
145 | audioNodeEditor.hidden = false
146 | setTimeout(() => {
147 | nodesTab.active = true
148 | audioTracker.hidden = true
149 | audioNodeEditor.active = true
150 | }, 150)
151 | })
152 |
153 | buttonPlayPause.addEventListener('click', () => {
154 | if (isAudioTrackerStarted()) {
155 | pause()
156 | } else {
157 | play()
158 | }
159 | })
160 |
161 | buttonMore.addEventListener('mousedown', () => {
162 | isMenuOpenOnMouseDown = menu.open
163 | })
164 |
165 | buttonMore.addEventListener('click', () => {
166 | if (!isMenuOpenOnMouseDown) {
167 | menu.open = true
168 | }
169 | })
170 |
171 | menuItemNew.addEventListener('click', () => {
172 | pause()
173 | clearAll(audioTracker, audioNodeEditor)
174 | })
175 |
176 | menuItemOpen.addEventListener('click', () => {
177 | const input = document.createElement('input')
178 | input.type = 'file'
179 | input.addEventListener('change', async () => {
180 | if (input.files.length !== 1) {
181 | return
182 | }
183 | pause()
184 | const file = input.files[0]
185 | const fileReader = new FileReader()
186 | fileReader.addEventListener('load', () => {
187 | /** @type {FileContent} */
188 | const content = JSON.parse(/** @type {string} */ (fileReader.result))
189 |
190 | importFile(content, audioTracker, audioNodeEditor)
191 | })
192 | fileReader.readAsText(file)
193 | })
194 | input.click()
195 | })
196 |
197 | menuItemExport.addEventListener('click', () => {
198 | const content = exportFile(audioTracker, audioNodeEditor)
199 |
200 | const blob = new Blob([JSON.stringify(content, null, 2)], {
201 | type: 'application/json',
202 | })
203 | const url = URL.createObjectURL(blob)
204 | const link = document.createElement('a')
205 | link.href = url
206 | link.download = 'waane-export.json'
207 | link.click()
208 | URL.revokeObjectURL(url)
209 | })
210 | },
211 | })
212 |
--------------------------------------------------------------------------------
/src/audio-node-editor/__tests__/graph-link.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@jest/globals'
2 | import { click, contextMenu, setup } from '../../testing/helpers'
3 |
4 | test('adds a link from an output to an input', () => {
5 | const { addAudioNode, getGraphNodes, addGraphLink, getGraphLinks } = setup('Nodes')
6 | addAudioNode('Oscillator')
7 | addAudioNode('Audio destination')
8 | const [oscillator, audioDestination] = getGraphNodes()
9 | addGraphLink(oscillator, audioDestination)
10 |
11 | expect(getGraphLinks()).toHaveLength(1)
12 | })
13 |
14 | test('adds a link from an input to an output', () => {
15 | const { nodeEditor, addAudioNode, getGraphNodes, getGraphLinks } = setup('Nodes')
16 | addAudioNode('Oscillator')
17 | addAudioNode('Audio destination')
18 | const [oscillator, audioDestination] = getGraphNodes()
19 |
20 | const graphNodeInput = audioDestination.querySelector('w-graph-node-input')
21 | const inputSocket = graphNodeInput.shadowRoot.querySelector('w-graph-node-socket')
22 | const graphNodeOutput = oscillator.querySelector('w-graph-node-output')
23 | const outputSocket = graphNodeOutput.shadowRoot.querySelector('w-graph-node-socket')
24 | inputSocket.dispatchEvent(new MouseEvent('mousedown'))
25 | outputSocket.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, composed: true }))
26 | outputSocket.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, composed: true }))
27 | nodeEditor.click()
28 |
29 | expect(getGraphLinks()).toHaveLength(1)
30 | })
31 |
32 | test('cancels adding a link when releasing on node editor', () => {
33 | const { nodeEditor, addAudioNode, getGraphLinks } = setup('Nodes')
34 | addAudioNode('Oscillator')
35 |
36 | const graphNodeOutput = nodeEditor.querySelector('w-graph-node-output')
37 | const outputSocket = graphNodeOutput.shadowRoot.querySelector('w-graph-node-socket')
38 | outputSocket.dispatchEvent(new MouseEvent('mousedown'))
39 | nodeEditor.dispatchEvent(new MouseEvent('mousemove'))
40 |
41 | expect(getGraphLinks()).toHaveLength(1)
42 |
43 | nodeEditor.dispatchEvent(new MouseEvent('mouseup'))
44 | nodeEditor.click()
45 |
46 | expect(getGraphLinks()).toHaveLength(0)
47 | })
48 |
49 | test('cancels adding a link if sockets are already linked', () => {
50 | const { addAudioNode, getGraphNodes, addGraphLink, getGraphLinks } = setup('Nodes')
51 | addAudioNode('Oscillator')
52 | addAudioNode('Audio destination')
53 | const [oscillator, audioDestination] = getGraphNodes()
54 | addGraphLink(oscillator, audioDestination)
55 |
56 | expect(getGraphLinks()).toHaveLength(1)
57 |
58 | addGraphLink(oscillator, audioDestination)
59 |
60 | expect(getGraphLinks()).toHaveLength(1)
61 | })
62 |
63 | test('deletes links when deleting output node', () => {
64 | const { getMenuItem, addAudioNode, getGraphNodes, addGraphLink, getGraphLinks } = setup('Nodes')
65 | addAudioNode('Oscillator')
66 | addAudioNode('Oscillator')
67 | addAudioNode('Audio destination')
68 | addAudioNode('Audio destination')
69 | const [oscillator1, oscillator2, audioDestination1, audioDestination2] = getGraphNodes()
70 |
71 | addGraphLink(oscillator1, audioDestination1)
72 | addGraphLink(oscillator2, audioDestination1)
73 | addGraphLink(oscillator2, audioDestination2)
74 | const [graphLink1] = getGraphLinks()
75 |
76 | click(oscillator2)
77 | contextMenu(oscillator2)
78 | getMenuItem('Delete').click()
79 |
80 | expect(getGraphNodes()).toEqual([oscillator1, audioDestination1, audioDestination2])
81 | expect(getGraphLinks()).toEqual([graphLink1])
82 | })
83 |
84 | test('deletes links when deleting input node', () => {
85 | const { getMenuItem, addAudioNode, getGraphNodes, addGraphLink, getGraphLinks } = setup('Nodes')
86 | addAudioNode('Oscillator')
87 | addAudioNode('Oscillator')
88 | addAudioNode('Audio destination')
89 | addAudioNode('Audio destination')
90 | const [oscillator1, oscillator2, audioDestination1, audioDestination2] = getGraphNodes()
91 |
92 | addGraphLink(oscillator1, audioDestination1)
93 | addGraphLink(oscillator1, audioDestination2)
94 | addGraphLink(oscillator2, audioDestination2)
95 | const [graphLink1] = getGraphLinks()
96 |
97 | click(audioDestination2)
98 | contextMenu(audioDestination2)
99 | getMenuItem('Delete').click()
100 |
101 | expect(getGraphNodes()).toEqual([oscillator1, oscillator2, audioDestination1])
102 | expect(getGraphLinks()).toEqual([graphLink1])
103 | })
104 |
105 | test('duplicates links when duplicating nodes', () => {
106 | const { getMenuItem, addAudioNode, getGraphNodes, addGraphLink, getGraphLinks } = setup('Nodes')
107 | addAudioNode('Oscillator')
108 | addAudioNode('Oscillator')
109 | addAudioNode('Audio destination')
110 | addAudioNode('Audio destination')
111 | const [oscillator1, oscillator2, audioDestination1, audioDestination2] = getGraphNodes()
112 |
113 | addGraphLink(oscillator1, audioDestination1)
114 | addGraphLink(oscillator2, audioDestination1)
115 | addGraphLink(oscillator2, audioDestination2)
116 | const [graphLink1, graphLink2, graphLink3] = getGraphLinks()
117 |
118 | click(oscillator2)
119 | click(audioDestination1, { ctrlKey: true })
120 | contextMenu(oscillator2)
121 | getMenuItem('Duplicate').click()
122 |
123 | expect(getGraphNodes()).toEqual([
124 | oscillator1,
125 | oscillator2,
126 | audioDestination1,
127 | audioDestination2,
128 | expect.objectContaining({
129 | textContent: expect.stringContaining('Oscillator'),
130 | }),
131 | expect.objectContaining({
132 | textContent: expect.stringContaining('Audio destination'),
133 | }),
134 | ])
135 | expect(getGraphLinks()).toEqual([graphLink1, graphLink2, graphLink3, expect.anything()])
136 | })
137 |
138 | test('disconnects a link', () => {
139 | const { nodeEditor, addAudioNode, getGraphNodes, addGraphLink, getGraphLinks } = setup('Nodes')
140 | addAudioNode('Oscillator')
141 | addAudioNode('Audio destination')
142 | const [oscillator, audioDestination] = getGraphNodes()
143 |
144 | addGraphLink(oscillator, audioDestination)
145 |
146 | expect(getGraphLinks()).toHaveLength(1)
147 |
148 | const graphNodeInput = audioDestination.querySelector('w-graph-node-input')
149 | const inputSocket = graphNodeInput.shadowRoot.querySelector('w-graph-node-socket')
150 | inputSocket.dispatchEvent(new MouseEvent('mousedown'))
151 | nodeEditor.dispatchEvent(new MouseEvent('mousemove'))
152 |
153 | expect(getGraphLinks()).toHaveLength(1)
154 |
155 | nodeEditor.dispatchEvent(new MouseEvent('mouseup'))
156 | nodeEditor.click()
157 |
158 | expect(getGraphLinks()).toHaveLength(0)
159 | })
160 |
161 | test('disconnects a specific link from a node', () => {
162 | const { nodeEditor, addAudioNode, getGraphNodes, addGraphLink, getGraphLinks } = setup('Nodes')
163 | addAudioNode('Oscillator')
164 | addAudioNode('Oscillator')
165 | addAudioNode('Audio destination')
166 | const [oscillator1, oscillator2, audioDestination] = getGraphNodes()
167 |
168 | addGraphLink(oscillator1, audioDestination)
169 | addGraphLink(oscillator2, audioDestination)
170 |
171 | expect(getGraphLinks()).toHaveLength(2)
172 | const [graphLink1] = getGraphLinks()
173 |
174 | const graphNodeInput = audioDestination.querySelector('w-graph-node-input')
175 | const inputSocket = graphNodeInput.shadowRoot.querySelector('w-graph-node-socket')
176 |
177 | inputSocket.dispatchEvent(new MouseEvent('mousedown'))
178 | inputSocket.dispatchEvent(new MouseEvent('mouseup'))
179 | nodeEditor.click()
180 |
181 | inputSocket.dispatchEvent(new MouseEvent('mousedown'))
182 | nodeEditor.dispatchEvent(new MouseEvent('mousemove'))
183 | nodeEditor.dispatchEvent(new MouseEvent('mouseup'))
184 | nodeEditor.click()
185 |
186 | expect(getGraphLinks()).toEqual([graphLink1])
187 | })
188 |
--------------------------------------------------------------------------------
/src/testing/bass-drum-0.3.js:
--------------------------------------------------------------------------------
1 | export default {
2 | nodeEditor: {
3 | zoom: '0.6289999999999997',
4 | 'pan-x': '-449.7001009262679',
5 | 'pan-y': '-253.46960258430437',
6 | },
7 | nodes: [
8 | {
9 | name: 'node-track',
10 | x: -1003.1595382890781,
11 | y: 17.526804905206035,
12 | attributes: {
13 | track: '1',
14 | },
15 | outputs: ['output-1'],
16 | inputs: [],
17 | },
18 | {
19 | name: 'node-schedule',
20 | x: -729,
21 | y: -541,
22 | attributes: {
23 | 'start-time': '0.003',
24 | 'time-constant': '0.06',
25 | },
26 | outputs: ['output-2'],
27 | inputs: ['input-1'],
28 | },
29 | {
30 | name: 'node-schedule',
31 | x: -727,
32 | y: -90,
33 | attributes: {
34 | 'start-time': '0',
35 | 'time-constant': '0.001',
36 | 'target-value': '1',
37 | },
38 | outputs: ['output-3'],
39 | inputs: ['input-2'],
40 | },
41 | {
42 | name: 'node-constant',
43 | x: -372.60414325481185,
44 | y: -379.75106318595414,
45 | attributes: {
46 | offset: '0',
47 | },
48 | outputs: ['output-4'],
49 | inputs: ['input-3'],
50 | },
51 | {
52 | name: 'node-gain',
53 | x: 12.103986495540756,
54 | y: -216.77428701187904,
55 | attributes: {
56 | gain: '130',
57 | },
58 | outputs: ['output-5'],
59 | inputs: ['input-4', 'input-5'],
60 | },
61 | {
62 | name: 'node-oscillator',
63 | x: 356.65354866465316,
64 | y: -265.22661408843317,
65 | attributes: {
66 | type: 'sine',
67 | frequency: '0',
68 | detune: '0',
69 | },
70 | outputs: ['output-6'],
71 | inputs: ['input-6', 'input-7'],
72 | },
73 | {
74 | name: 'node-biquad-filter',
75 | x: 726.9356424947939,
76 | y: -371.6694446525491,
77 | attributes: {
78 | type: 'lowpass',
79 | frequency: '300',
80 | detune: '0',
81 | q: '20',
82 | gain: '0',
83 | },
84 | outputs: ['output-7'],
85 | inputs: ['input-8', 'input-9', 'input-10', 'input-11', 'input-12'],
86 | },
87 | {
88 | name: 'node-gain',
89 | x: 1095.6431254879901,
90 | y: -84.59461472057603,
91 | attributes: {
92 | gain: '1',
93 | },
94 | outputs: ['output-8'],
95 | inputs: ['input-13', 'input-14'],
96 | },
97 | {
98 | name: 'node-gain',
99 | x: 1595.5793906250203,
100 | y: -678.7856372098778,
101 | attributes: {
102 | gain: '0',
103 | },
104 | outputs: ['output-9'],
105 | inputs: ['input-15', 'input-16'],
106 | },
107 | {
108 | name: 'node-audio-destination',
109 | x: 1928.046450106558,
110 | y: -607.2777223804663,
111 | attributes: {},
112 | outputs: [],
113 | inputs: ['input-17'],
114 | },
115 | {
116 | name: 'node-schedule',
117 | x: -725.8203497615269,
118 | y: 368.37996820349827,
119 | attributes: {
120 | 'start-time': '0.003',
121 | 'time-constant': '0.001',
122 | },
123 | outputs: ['output-10'],
124 | inputs: ['input-18'],
125 | },
126 | {
127 | name: 'node-constant',
128 | x: -367.8346678971009,
129 | y: 78.11857115426857,
130 | attributes: {
131 | offset: '0',
132 | },
133 | outputs: ['output-11'],
134 | inputs: ['input-19'],
135 | },
136 | {
137 | name: 'node-oscillator',
138 | x: -725.6398685940358,
139 | y: 827.3642422564245,
140 | attributes: {
141 | type: 'triangle',
142 | frequency: '66',
143 | detune: '0',
144 | },
145 | outputs: ['output-12'],
146 | inputs: ['input-20', 'input-21'],
147 | },
148 | {
149 | name: 'node-gain',
150 | x: -363.9760363944888,
151 | y: 828.0161696940673,
152 | attributes: {
153 | gain: '500',
154 | },
155 | outputs: ['output-13'],
156 | inputs: ['input-22', 'input-23'],
157 | },
158 | {
159 | name: 'node-oscillator',
160 | x: 8.813079251769526,
161 | y: 785.8392989457668,
162 | attributes: {
163 | type: 'triangle',
164 | frequency: '40',
165 | detune: '0',
166 | },
167 | outputs: ['output-14'],
168 | inputs: ['input-24', 'input-25'],
169 | },
170 | {
171 | name: 'node-biquad-filter',
172 | x: 363.19496449168616,
173 | y: 561.4241560483201,
174 | attributes: {
175 | type: 'highpass',
176 | frequency: '40',
177 | detune: '0',
178 | q: '30',
179 | gain: '0',
180 | },
181 | outputs: ['output-15'],
182 | inputs: ['input-26', 'input-27', 'input-28', 'input-29', 'input-30'],
183 | },
184 | {
185 | name: 'node-biquad-filter',
186 | x: 728.4019181546178,
187 | y: 235.36867851460656,
188 | attributes: {
189 | type: 'lowpass',
190 | frequency: '130',
191 | detune: '0',
192 | q: '30',
193 | gain: '0',
194 | },
195 | outputs: ['output-16'],
196 | inputs: ['input-31', 'input-32', 'input-33', 'input-34', 'input-35'],
197 | },
198 | {
199 | name: 'node-gain',
200 | x: 365.4764205131359,
201 | y: 237.77116753357572,
202 | attributes: {
203 | gain: '5000',
204 | },
205 | outputs: ['output-17'],
206 | inputs: ['input-36', 'input-37'],
207 | },
208 | {
209 | name: 'node-gain',
210 | x: 1095.6431254879903,
211 | y: 240.9185817603625,
212 | attributes: {
213 | gain: '0.2',
214 | },
215 | outputs: ['output-18'],
216 | inputs: ['input-38', 'input-39'],
217 | },
218 | ],
219 | links: [
220 | {
221 | from: 'output-1',
222 | to: 'input-1',
223 | },
224 | {
225 | from: 'output-1',
226 | to: 'input-2',
227 | },
228 | {
229 | from: 'output-2',
230 | to: 'input-3',
231 | },
232 | {
233 | from: 'output-3',
234 | to: 'input-3',
235 | },
236 | {
237 | from: 'output-4',
238 | to: 'input-5',
239 | },
240 | {
241 | from: 'output-5',
242 | to: 'input-6',
243 | },
244 | {
245 | from: 'output-6',
246 | to: 'input-12',
247 | },
248 | {
249 | from: 'output-4',
250 | to: 'input-15',
251 | },
252 | {
253 | from: 'output-8',
254 | to: 'input-16',
255 | },
256 | {
257 | from: 'output-1',
258 | to: 'input-18',
259 | },
260 | {
261 | from: 'output-3',
262 | to: 'input-19',
263 | },
264 | {
265 | from: 'output-10',
266 | to: 'input-19',
267 | },
268 | {
269 | from: 'output-12',
270 | to: 'input-23',
271 | },
272 | {
273 | from: 'output-13',
274 | to: 'input-24',
275 | },
276 | {
277 | from: 'output-14',
278 | to: 'input-30',
279 | },
280 | {
281 | from: 'output-15',
282 | to: 'input-35',
283 | },
284 | {
285 | from: 'output-11',
286 | to: 'input-37',
287 | },
288 | {
289 | from: 'output-18',
290 | to: 'input-16',
291 | },
292 | {
293 | from: 'output-16',
294 | to: 'input-39',
295 | },
296 | {
297 | from: 'output-17',
298 | to: 'input-31',
299 | },
300 | {
301 | from: 'output-7',
302 | to: 'input-14',
303 | },
304 | {
305 | from: 'output-9',
306 | to: 'input-17',
307 | },
308 | ],
309 | tracker: {
310 | tempo: 120,
311 | lines: 64,
312 | linesPerBeat: 4,
313 | },
314 | tracks: [
315 | {
316 | label: '1',
317 | effects: {
318 | 0: 'FF',
319 | 2: 'FF',
320 | 3: 'FF',
321 | 6: 'FF',
322 | 7: 'FF',
323 | 9: 'FF',
324 | 10: 'FF',
325 | 11: 'FF',
326 | 13: 'FF',
327 | 14: 'FF',
328 | },
329 | },
330 | ],
331 | audioFiles: [],
332 | }
333 |
--------------------------------------------------------------------------------
/src/shared/core/element.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Alias for String.raw
3 | * Use it along with VS Code extension lit-html
4 | * to get template syntax highlighting.
5 | *
6 | * See https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
7 | */
8 | export const html = String.raw
9 |
10 | /**
11 | * Alias for String.raw
12 | * Use it along with VS Code extension vscode-styled-components
13 | * to get styles syntax highlighting.
14 | *
15 | * See https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components
16 | */
17 | export const css = String.raw
18 |
19 | /** @typedef {typeof String | typeof Number | typeof Boolean} PropertyType */
20 |
21 | /**
22 | * @template {PropertyType} T
23 | * @typedef {T extends typeof Number
24 | * ? number
25 | * : T extends typeof Boolean
26 | * ? boolean
27 | * : T extends typeof String
28 | * ? string
29 | * : never
30 | * } PrimitiveType
31 | */
32 |
33 | /**
34 | * @template T
35 | * @typedef {object} AccessorProperty
36 | * @property {() => T} get
37 | * @property {(value: T) => void} set
38 | */
39 |
40 | /**
41 | * @param {string} attributeName
42 | * @returns {AccessorProperty}
43 | */
44 | function getStringProperty(attributeName) {
45 | return {
46 | /** @this {HTMLElement} */
47 | get() {
48 | return this.getAttribute(attributeName)
49 | },
50 |
51 | /** @this {HTMLElement} */
52 | set(value) {
53 | if (value === null) {
54 | this.removeAttribute(attributeName)
55 | } else {
56 | this.setAttribute(attributeName, value)
57 | }
58 | },
59 | }
60 | }
61 |
62 | /**
63 | * @param {string} attributeName
64 | * @returns {AccessorProperty}
65 | */
66 | function getNumberProperty(attributeName) {
67 | return {
68 | /** @this {HTMLElement} */
69 | get() {
70 | return Number(this.getAttribute(attributeName))
71 | },
72 |
73 | /** @this {HTMLElement} */
74 | set(value) {
75 | if (value === null) {
76 | this.removeAttribute(attributeName)
77 | } else {
78 | this.setAttribute(attributeName, String(value))
79 | }
80 | },
81 | }
82 | }
83 |
84 | /**
85 | * @param {string} attributeName
86 | * @returns {AccessorProperty}
87 | */
88 | function getBooleanProperty(attributeName) {
89 | return {
90 | /** @this {HTMLElement} */
91 | get() {
92 | return this.hasAttribute(attributeName)
93 | },
94 |
95 | /** @this {HTMLElement} */
96 | set(value) {
97 | if (value) {
98 | this.setAttribute(attributeName, '')
99 | } else {
100 | this.removeAttribute(attributeName)
101 | }
102 | },
103 | }
104 | }
105 |
106 | /**
107 | * @param {string} attributeName
108 | * @param {PropertyType} propertyType
109 | * @returns {AccessorProperty>}
110 | */
111 | function getProperty(attributeName, propertyType) {
112 | switch (propertyType) {
113 | case Number:
114 | return getNumberProperty(attributeName)
115 | case Boolean:
116 | return getBooleanProperty(attributeName)
117 | default:
118 | return getStringProperty(attributeName)
119 | }
120 | }
121 |
122 | /** @typedef {Object} PropertyTypes */
123 |
124 | /**
125 | * @template {PropertyTypes} T
126 | * @typedef {{[P in keyof T]: PrimitiveType}} Properties
127 | */
128 |
129 | /**
130 | * @template {PropertyTypes} T
131 | * @callback Observe
132 | * @param {keyof T} propertyName
133 | * @param {() => void} callback
134 | * @returns {void}
135 | */
136 |
137 | /**
138 | * @template {PropertyTypes} T
139 | * @typedef {object} SetupOptions
140 | * @property {HTMLElement & Properties} host
141 | * @property {(callback: () => void) => void} connected
142 | * @property {(callback: () => void) => void} disconnected
143 | * @property {Observe} observe
144 | */
145 |
146 | /**
147 | * @template {PropertyTypes} T
148 | * @callback Setup
149 | * @param {SetupOptions} options
150 | * @returns {void}
151 | */
152 |
153 | /**
154 | * @template {PropertyTypes} T
155 | * @typedef {object} DefineCustomElementOptions
156 | * @property {string} [styles]
157 | * @property {string} [template]
158 | * @property {boolean} [shadow]
159 | * @property {T} [properties]
160 | * @property {Setup} [setup]
161 | */
162 |
163 | /**
164 | * @template {PropertyTypes} T
165 | * @param {string} name
166 | * @param {DefineCustomElementOptions} options
167 | * @returns {HTMLElement & Properties}
168 | */
169 | export function defineCustomElement(
170 | name,
171 | { styles, template = html``, shadow = true, properties = /** @type {T} */ ({}), setup = () => {} },
172 | ) {
173 | const templateElement = document.createElement('template')
174 | templateElement.innerHTML = styles
175 | ? html`
176 |
179 | ${template}
180 | `
181 | : template
182 |
183 | /** @type {Object} */
184 | const attributesByProperty = Object.keys(properties).reduce(
185 | (result, propertyName) =>
186 | Object.assign(result, {
187 | [propertyName]: (
188 | propertyName.substring(0, 1) + propertyName.substring(1).replace(/[A-Z]/g, '-$&')
189 | ).toLowerCase(),
190 | }),
191 | {},
192 | )
193 |
194 | const reflectedProperties = Object.entries(properties).reduce(
195 | (result, [propertyName, propertyType]) =>
196 | Object.assign(result, {
197 | [propertyName]: getProperty(attributesByProperty[propertyName], propertyType),
198 | }),
199 | {},
200 | )
201 |
202 | class CustomElement extends HTMLElement {
203 | _connectedCallback = () => {}
204 | _disconnectedCallback = () => {}
205 |
206 | /** @type {Object void>} */
207 | _attributeChangedCallbacks = Object.values(attributesByProperty).reduce(
208 | (callbacks, attributeName) => Object.assign(callbacks, { [attributeName]: () => {} }),
209 | {},
210 | )
211 |
212 | /** @this {CustomElement & Properties} */
213 | constructor() {
214 | super()
215 |
216 | if (shadow) {
217 | this.attachShadow({ mode: 'open' })
218 | this.shadowRoot.appendChild(templateElement.content.cloneNode(true))
219 | }
220 |
221 | const propertiesBackup = Object.keys(reflectedProperties).reduce(
222 | (result, propertyName) =>
223 | this[propertyName] === undefined ? result : Object.assign(result, { [propertyName]: this[propertyName] }),
224 | /** @type {Properties} */ ({}),
225 | )
226 |
227 | Object.defineProperties(this, reflectedProperties)
228 |
229 | setup({
230 | host: this,
231 | connected: (callback) => {
232 | this._connectedCallback = callback
233 | },
234 | disconnected: (callback) => {
235 | this._disconnectedCallback = callback
236 | },
237 | observe: /** @type {Observe} */ ((/** @type {string} */ propertyName, callback) => {
238 | const attributeName = attributesByProperty[propertyName]
239 | this._attributeChangedCallbacks[attributeName] = callback
240 | }),
241 | })
242 |
243 | Object.entries(propertiesBackup).forEach(([propertyName, value]) => {
244 | this[/** @type {keyof T} */ (propertyName)] = value
245 | const attributeName = attributesByProperty[propertyName]
246 | this._attributeChangedCallbacks[attributeName]()
247 | })
248 | }
249 |
250 | static get observedAttributes() {
251 | return Object.values(attributesByProperty)
252 | }
253 |
254 | connectedCallback() {
255 | if (!shadow) {
256 | this.appendChild(templateElement.content.cloneNode(true))
257 | }
258 | this._connectedCallback()
259 | }
260 |
261 | disconnectedCallback() {
262 | this._disconnectedCallback()
263 | }
264 |
265 | /**
266 | * @template {PropertyType} T
267 | * @param {string} name
268 | * @param {PrimitiveType} oldValue
269 | * @param {PrimitiveType} newValue
270 | */
271 | attributeChangedCallback(name, oldValue, newValue) {
272 | if (newValue !== oldValue) {
273 | this._attributeChangedCallbacks[name]()
274 | }
275 | }
276 | }
277 |
278 | customElements.define(name, CustomElement)
279 |
280 | return /** @type {HTMLElement & Properties} */ (/** @type {unknown} */ (CustomElement))
281 | }
282 |
--------------------------------------------------------------------------------
/src/testing/bass-drum-0.1.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {import("../helpers/file-helper.js").FileContent} FileContent
3 | */
4 |
5 | export default /** @type {FileContent} */ (
6 | /** @type {unknown} */ ({
7 | nodeEditor: {
8 | zoom: '0.6289999999999997',
9 | 'pan-x': '-449.7001009262679',
10 | 'pan-y': '-253.46960258430437',
11 | },
12 | nodes: [
13 | {
14 | name: 'node-track',
15 | x: -1003.1595382890781,
16 | y: 17.526804905206035,
17 | attributes: {
18 | track: '1',
19 | },
20 | outputs: ['output-1'],
21 | inputs: [],
22 | },
23 | {
24 | name: 'node-schedule',
25 | x: -729,
26 | y: -541,
27 | attributes: {
28 | 'start-time': '0.003',
29 | 'time-constant': '0.06',
30 | },
31 | outputs: ['output-2'],
32 | inputs: ['input-1'],
33 | },
34 | {
35 | name: 'node-schedule',
36 | x: -727,
37 | y: -90,
38 | attributes: {
39 | 'start-time': '0',
40 | 'time-constant': '0.001',
41 | 'target-value': '1',
42 | },
43 | outputs: ['output-3'],
44 | inputs: ['input-2'],
45 | },
46 | {
47 | name: 'node-constant',
48 | x: -372.60414325481185,
49 | y: -379.75106318595414,
50 | attributes: {
51 | offset: '0',
52 | },
53 | outputs: ['output-4'],
54 | inputs: ['input-3'],
55 | },
56 | {
57 | name: 'node-gain',
58 | x: 12.103986495540756,
59 | y: -216.77428701187904,
60 | attributes: {
61 | gain: '130',
62 | },
63 | outputs: ['output-5'],
64 | inputs: ['input-4', 'input-5'],
65 | },
66 | {
67 | name: 'node-oscillator',
68 | x: 356.65354866465316,
69 | y: -265.22661408843317,
70 | attributes: {
71 | type: 'sine',
72 | frequency: '0',
73 | detune: '0',
74 | },
75 | outputs: ['output-6'],
76 | inputs: ['input-6', 'input-7'],
77 | },
78 | {
79 | name: 'node-biquad-filter',
80 | x: 726.9356424947939,
81 | y: -371.6694446525491,
82 | attributes: {
83 | type: 'lowpass',
84 | frequency: '300',
85 | detune: '0',
86 | q: '20',
87 | gain: '0',
88 | },
89 | outputs: ['output-7'],
90 | inputs: ['input-8', 'input-9', 'input-10', 'input-11', 'input-12'],
91 | },
92 | {
93 | name: 'node-gain',
94 | x: 1095.6431254879901,
95 | y: -84.59461472057603,
96 | attributes: {
97 | gain: '1',
98 | },
99 | outputs: ['output-8'],
100 | inputs: ['input-13', 'input-14'],
101 | },
102 | {
103 | name: 'node-gain',
104 | x: 1595.5793906250203,
105 | y: -678.7856372098778,
106 | attributes: {
107 | gain: '0',
108 | },
109 | outputs: ['output-9'],
110 | inputs: ['input-15', 'input-16'],
111 | },
112 | {
113 | name: 'node-audio-destination',
114 | x: 1928.046450106558,
115 | y: -607.2777223804663,
116 | attributes: {},
117 | outputs: [],
118 | inputs: ['input-17'],
119 | },
120 | {
121 | name: 'node-schedule',
122 | x: -725.8203497615269,
123 | y: 368.37996820349827,
124 | attributes: {
125 | 'start-time': '0.003',
126 | 'time-constant': '0.001',
127 | },
128 | outputs: ['output-10'],
129 | inputs: ['input-18'],
130 | },
131 | {
132 | name: 'node-constant',
133 | x: -367.8346678971009,
134 | y: 78.11857115426857,
135 | attributes: {
136 | offset: '0',
137 | },
138 | outputs: ['output-11'],
139 | inputs: ['input-19'],
140 | },
141 | {
142 | name: 'node-oscillator',
143 | x: -725.6398685940358,
144 | y: 827.3642422564245,
145 | attributes: {
146 | type: 'triangle',
147 | frequency: '66',
148 | detune: '0',
149 | },
150 | outputs: ['output-12'],
151 | inputs: ['input-20', 'input-21'],
152 | },
153 | {
154 | name: 'node-gain',
155 | x: -363.9760363944888,
156 | y: 828.0161696940673,
157 | attributes: {
158 | gain: '500',
159 | },
160 | outputs: ['output-13'],
161 | inputs: ['input-22', 'input-23'],
162 | },
163 | {
164 | name: 'node-oscillator',
165 | x: 8.813079251769526,
166 | y: 785.8392989457668,
167 | attributes: {
168 | type: 'triangle',
169 | frequency: '40',
170 | detune: '0',
171 | },
172 | outputs: ['output-14'],
173 | inputs: ['input-24', 'input-25'],
174 | },
175 | {
176 | name: 'node-biquad-filter',
177 | x: 363.19496449168616,
178 | y: 561.4241560483201,
179 | attributes: {
180 | type: 'highpass',
181 | frequency: '40',
182 | detune: '0',
183 | q: '30',
184 | gain: '0',
185 | },
186 | outputs: ['output-15'],
187 | inputs: ['input-26', 'input-27', 'input-28', 'input-29', 'input-30'],
188 | },
189 | {
190 | name: 'node-biquad-filter',
191 | x: 728.4019181546178,
192 | y: 235.36867851460656,
193 | attributes: {
194 | type: 'lowpass',
195 | frequency: '130',
196 | detune: '0',
197 | q: '30',
198 | gain: '0',
199 | },
200 | outputs: ['output-16'],
201 | inputs: ['input-31', 'input-32', 'input-33', 'input-34', 'input-35'],
202 | },
203 | {
204 | name: 'node-gain',
205 | x: 365.4764205131359,
206 | y: 237.77116753357572,
207 | attributes: {
208 | gain: '5000',
209 | },
210 | outputs: ['output-17'],
211 | inputs: ['input-36', 'input-37'],
212 | },
213 | {
214 | name: 'node-gain',
215 | x: 1095.6431254879903,
216 | y: 240.9185817603625,
217 | attributes: {
218 | gain: '0.2',
219 | },
220 | outputs: ['output-18'],
221 | inputs: ['input-38', 'input-39'],
222 | },
223 | ],
224 | links: [
225 | {
226 | from: 'output-1',
227 | to: 'input-1',
228 | },
229 | {
230 | from: 'output-1',
231 | to: 'input-2',
232 | },
233 | {
234 | from: 'output-2',
235 | to: 'input-3',
236 | },
237 | {
238 | from: 'output-3',
239 | to: 'input-3',
240 | },
241 | {
242 | from: 'output-4',
243 | to: 'input-5',
244 | },
245 | {
246 | from: 'output-5',
247 | to: 'input-6',
248 | },
249 | {
250 | from: 'output-6',
251 | to: 'input-12',
252 | },
253 | {
254 | from: 'output-4',
255 | to: 'input-15',
256 | },
257 | {
258 | from: 'output-8',
259 | to: 'input-16',
260 | },
261 | {
262 | from: 'output-1',
263 | to: 'input-18',
264 | },
265 | {
266 | from: 'output-3',
267 | to: 'input-19',
268 | },
269 | {
270 | from: 'output-10',
271 | to: 'input-19',
272 | },
273 | {
274 | from: 'output-12',
275 | to: 'input-23',
276 | },
277 | {
278 | from: 'output-13',
279 | to: 'input-24',
280 | },
281 | {
282 | from: 'output-14',
283 | to: 'input-30',
284 | },
285 | {
286 | from: 'output-15',
287 | to: 'input-35',
288 | },
289 | {
290 | from: 'output-11',
291 | to: 'input-37',
292 | },
293 | {
294 | from: 'output-18',
295 | to: 'input-16',
296 | },
297 | {
298 | from: 'output-16',
299 | to: 'input-39',
300 | },
301 | {
302 | from: 'output-17',
303 | to: 'input-31',
304 | },
305 | {
306 | from: 'output-7',
307 | to: 'input-14',
308 | },
309 | {
310 | from: 'output-9',
311 | to: 'input-17',
312 | },
313 | ],
314 | tracks: [
315 | {
316 | label: '1',
317 | effects: {
318 | 0: 'FF',
319 | 2: 'FF',
320 | 3: 'FF',
321 | 6: 'FF',
322 | 7: 'FF',
323 | 9: 'FF',
324 | 10: 'FF',
325 | 11: 'FF',
326 | 13: 'FF',
327 | 14: 'FF',
328 | },
329 | },
330 | ],
331 | })
332 | )
333 |
--------------------------------------------------------------------------------
/src/testing/bass-drum-0.2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {import("../helpers/file-helper.js").FileContent} FileContent
3 | */
4 |
5 | export default /** @type {FileContent} */ (
6 | /** @type {unknown} */ ({
7 | nodeEditor: {
8 | zoom: '0.6289999999999997',
9 | 'pan-x': '-449.7001009262679',
10 | 'pan-y': '-253.46960258430437',
11 | },
12 | nodes: [
13 | {
14 | name: 'node-track',
15 | x: -1003.1595382890781,
16 | y: 17.526804905206035,
17 | attributes: {
18 | track: '1',
19 | },
20 | outputs: ['output-1'],
21 | inputs: [],
22 | },
23 | {
24 | name: 'node-schedule',
25 | x: -729,
26 | y: -541,
27 | attributes: {
28 | 'start-time': '0.003',
29 | 'time-constant': '0.06',
30 | },
31 | outputs: ['output-2'],
32 | inputs: ['input-1'],
33 | },
34 | {
35 | name: 'node-schedule',
36 | x: -727,
37 | y: -90,
38 | attributes: {
39 | 'start-time': '0',
40 | 'time-constant': '0.001',
41 | 'target-value': '1',
42 | },
43 | outputs: ['output-3'],
44 | inputs: ['input-2'],
45 | },
46 | {
47 | name: 'node-constant',
48 | x: -372.60414325481185,
49 | y: -379.75106318595414,
50 | attributes: {
51 | offset: '0',
52 | },
53 | outputs: ['output-4'],
54 | inputs: ['input-3'],
55 | },
56 | {
57 | name: 'node-gain',
58 | x: 12.103986495540756,
59 | y: -216.77428701187904,
60 | attributes: {
61 | gain: '130',
62 | },
63 | outputs: ['output-5'],
64 | inputs: ['input-4', 'input-5'],
65 | },
66 | {
67 | name: 'node-oscillator',
68 | x: 356.65354866465316,
69 | y: -265.22661408843317,
70 | attributes: {
71 | type: 'sine',
72 | frequency: '0',
73 | detune: '0',
74 | },
75 | outputs: ['output-6'],
76 | inputs: ['input-6', 'input-7'],
77 | },
78 | {
79 | name: 'node-biquad-filter',
80 | x: 726.9356424947939,
81 | y: -371.6694446525491,
82 | attributes: {
83 | type: 'lowpass',
84 | frequency: '300',
85 | detune: '0',
86 | q: '20',
87 | gain: '0',
88 | },
89 | outputs: ['output-7'],
90 | inputs: ['input-8', 'input-9', 'input-10', 'input-11', 'input-12'],
91 | },
92 | {
93 | name: 'node-gain',
94 | x: 1095.6431254879901,
95 | y: -84.59461472057603,
96 | attributes: {
97 | gain: '1',
98 | },
99 | outputs: ['output-8'],
100 | inputs: ['input-13', 'input-14'],
101 | },
102 | {
103 | name: 'node-gain',
104 | x: 1595.5793906250203,
105 | y: -678.7856372098778,
106 | attributes: {
107 | gain: '0',
108 | },
109 | outputs: ['output-9'],
110 | inputs: ['input-15', 'input-16'],
111 | },
112 | {
113 | name: 'node-audio-destination',
114 | x: 1928.046450106558,
115 | y: -607.2777223804663,
116 | attributes: {},
117 | outputs: [],
118 | inputs: ['input-17'],
119 | },
120 | {
121 | name: 'node-schedule',
122 | x: -725.8203497615269,
123 | y: 368.37996820349827,
124 | attributes: {
125 | 'start-time': '0.003',
126 | 'time-constant': '0.001',
127 | },
128 | outputs: ['output-10'],
129 | inputs: ['input-18'],
130 | },
131 | {
132 | name: 'node-constant',
133 | x: -367.8346678971009,
134 | y: 78.11857115426857,
135 | attributes: {
136 | offset: '0',
137 | },
138 | outputs: ['output-11'],
139 | inputs: ['input-19'],
140 | },
141 | {
142 | name: 'node-oscillator',
143 | x: -725.6398685940358,
144 | y: 827.3642422564245,
145 | attributes: {
146 | type: 'triangle',
147 | frequency: '66',
148 | detune: '0',
149 | },
150 | outputs: ['output-12'],
151 | inputs: ['input-20', 'input-21'],
152 | },
153 | {
154 | name: 'node-gain',
155 | x: -363.9760363944888,
156 | y: 828.0161696940673,
157 | attributes: {
158 | gain: '500',
159 | },
160 | outputs: ['output-13'],
161 | inputs: ['input-22', 'input-23'],
162 | },
163 | {
164 | name: 'node-oscillator',
165 | x: 8.813079251769526,
166 | y: 785.8392989457668,
167 | attributes: {
168 | type: 'triangle',
169 | frequency: '40',
170 | detune: '0',
171 | },
172 | outputs: ['output-14'],
173 | inputs: ['input-24', 'input-25'],
174 | },
175 | {
176 | name: 'node-biquad-filter',
177 | x: 363.19496449168616,
178 | y: 561.4241560483201,
179 | attributes: {
180 | type: 'highpass',
181 | frequency: '40',
182 | detune: '0',
183 | q: '30',
184 | gain: '0',
185 | },
186 | outputs: ['output-15'],
187 | inputs: ['input-26', 'input-27', 'input-28', 'input-29', 'input-30'],
188 | },
189 | {
190 | name: 'node-biquad-filter',
191 | x: 728.4019181546178,
192 | y: 235.36867851460656,
193 | attributes: {
194 | type: 'lowpass',
195 | frequency: '130',
196 | detune: '0',
197 | q: '30',
198 | gain: '0',
199 | },
200 | outputs: ['output-16'],
201 | inputs: ['input-31', 'input-32', 'input-33', 'input-34', 'input-35'],
202 | },
203 | {
204 | name: 'node-gain',
205 | x: 365.4764205131359,
206 | y: 237.77116753357572,
207 | attributes: {
208 | gain: '5000',
209 | },
210 | outputs: ['output-17'],
211 | inputs: ['input-36', 'input-37'],
212 | },
213 | {
214 | name: 'node-gain',
215 | x: 1095.6431254879903,
216 | y: 240.9185817603625,
217 | attributes: {
218 | gain: '0.2',
219 | },
220 | outputs: ['output-18'],
221 | inputs: ['input-38', 'input-39'],
222 | },
223 | ],
224 | links: [
225 | {
226 | from: 'output-1',
227 | to: 'input-1',
228 | },
229 | {
230 | from: 'output-1',
231 | to: 'input-2',
232 | },
233 | {
234 | from: 'output-2',
235 | to: 'input-3',
236 | },
237 | {
238 | from: 'output-3',
239 | to: 'input-3',
240 | },
241 | {
242 | from: 'output-4',
243 | to: 'input-5',
244 | },
245 | {
246 | from: 'output-5',
247 | to: 'input-6',
248 | },
249 | {
250 | from: 'output-6',
251 | to: 'input-12',
252 | },
253 | {
254 | from: 'output-4',
255 | to: 'input-15',
256 | },
257 | {
258 | from: 'output-8',
259 | to: 'input-16',
260 | },
261 | {
262 | from: 'output-1',
263 | to: 'input-18',
264 | },
265 | {
266 | from: 'output-3',
267 | to: 'input-19',
268 | },
269 | {
270 | from: 'output-10',
271 | to: 'input-19',
272 | },
273 | {
274 | from: 'output-12',
275 | to: 'input-23',
276 | },
277 | {
278 | from: 'output-13',
279 | to: 'input-24',
280 | },
281 | {
282 | from: 'output-14',
283 | to: 'input-30',
284 | },
285 | {
286 | from: 'output-15',
287 | to: 'input-35',
288 | },
289 | {
290 | from: 'output-11',
291 | to: 'input-37',
292 | },
293 | {
294 | from: 'output-18',
295 | to: 'input-16',
296 | },
297 | {
298 | from: 'output-16',
299 | to: 'input-39',
300 | },
301 | {
302 | from: 'output-17',
303 | to: 'input-31',
304 | },
305 | {
306 | from: 'output-7',
307 | to: 'input-14',
308 | },
309 | {
310 | from: 'output-9',
311 | to: 'input-17',
312 | },
313 | ],
314 | tracker: {
315 | tempo: 120,
316 | lines: 64,
317 | linesPerBeat: 4,
318 | },
319 | tracks: [
320 | {
321 | label: '1',
322 | effects: {
323 | 0: 'FF',
324 | 2: 'FF',
325 | 3: 'FF',
326 | 6: 'FF',
327 | 7: 'FF',
328 | 9: 'FF',
329 | 10: 'FF',
330 | 11: 'FF',
331 | 13: 'FF',
332 | 14: 'FF',
333 | },
334 | },
335 | ],
336 | })
337 | )
338 |
--------------------------------------------------------------------------------