4track 0.1.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/LICENSE +674 -0
- package/README.md +80 -0
- package/dist/assets/btn_fwd.svg +30 -0
- package/dist/assets/btn_normal.png +0 -0
- package/dist/assets/btn_pause.svg +30 -0
- package/dist/assets/btn_play.svg +25 -0
- package/dist/assets/btn_pressed.png +0 -0
- package/dist/assets/btn_rec.svg +25 -0
- package/dist/assets/btn_rew.svg +30 -0
- package/dist/assets/btn_stop.svg +25 -0
- package/dist/assets/casette_hiss.mp3 +0 -0
- package/dist/assets/casette_hiss_compressed.mp3 +0 -0
- package/dist/assets/cassette.jpg +0 -0
- package/dist/assets/counter_bg.png +0 -0
- package/dist/assets/fx/counter.wav +0 -0
- package/dist/assets/fx/ffwd.wav +0 -0
- package/dist/assets/fx/pause.wav +0 -0
- package/dist/assets/fx/play.wav +0 -0
- package/dist/assets/fx/record.wav +0 -0
- package/dist/assets/fx/stop.wav +0 -0
- package/dist/assets/fx/track.wav +0 -0
- package/dist/assets/logo.svg +51 -0
- package/dist/assets/noise_50.jpg +0 -0
- package/dist/assets/openstudio.svg +38 -0
- package/dist/assets/recorder-worklet.d.ts +8 -0
- package/dist/assets/recorder-worklet.js +30 -0
- package/dist/assets/rotator.png +0 -0
- package/dist/assets/slider-indicator.svg +139 -0
- package/dist/assets/slider.png +0 -0
- package/dist/assets/slideselect-indicator.svg +64 -0
- package/dist/assets/slideselect-thumb.png +0 -0
- package/dist/assets/svg-icons.d.ts +6 -0
- package/dist/assets/svg-icons.js +8 -0
- package/dist/assets.d.ts +34 -0
- package/dist/audio/constants.d.ts +4 -0
- package/dist/audio/constants.js +27 -0
- package/dist/audio/engine.svelte.d.ts +90 -0
- package/dist/audio/engine.svelte.js +604 -0
- package/dist/audio/input-fx.d.ts +8 -0
- package/dist/audio/input-fx.js +44 -0
- package/dist/audio/metering.d.ts +3 -0
- package/dist/audio/metering.js +20 -0
- package/dist/audio/pcm.d.ts +2 -0
- package/dist/audio/pcm.js +43 -0
- package/dist/audio/project-io.d.ts +6 -0
- package/dist/audio/project-io.js +85 -0
- package/dist/audio/recording.d.ts +2 -0
- package/dist/audio/recording.js +80 -0
- package/dist/audio/track.svelte.d.ts +13 -0
- package/dist/audio/track.svelte.js +17 -0
- package/dist/components/Cassette.svelte +179 -0
- package/dist/components/Cassette.svelte.d.ts +9 -0
- package/dist/components/FourTrack.svelte +443 -0
- package/dist/components/FourTrack.svelte.d.ts +16 -0
- package/dist/components/Mixer.svelte +105 -0
- package/dist/components/Mixer.svelte.d.ts +7 -0
- package/dist/components/TransportButtons.svelte +299 -0
- package/dist/components/TransportButtons.svelte.d.ts +10 -0
- package/dist/components/els/DigitRoller.svelte +82 -0
- package/dist/components/els/DigitRoller.svelte.d.ts +5 -0
- package/dist/components/els/Knob.svelte +267 -0
- package/dist/components/els/Knob.svelte.d.ts +12 -0
- package/dist/components/els/Light.svelte +104 -0
- package/dist/components/els/Light.svelte.d.ts +8 -0
- package/dist/components/els/Lights.svelte +101 -0
- package/dist/components/els/Lights.svelte.d.ts +11 -0
- package/dist/components/els/SlideSelect.svelte +159 -0
- package/dist/components/els/SlideSelect.svelte.d.ts +15 -0
- package/dist/components/els/Slider.svelte +139 -0
- package/dist/components/els/Slider.svelte.d.ts +21 -0
- package/dist/components/els/Timestamp.svelte +92 -0
- package/dist/components/els/Timestamp.svelte.d.ts +5 -0
- package/dist/fx/soundfx.d.ts +14 -0
- package/dist/fx/soundfx.js +65 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { AudioEngine } from "../audio/engine.svelte.js"
|
|
3
|
+
import type { HiddenTrackConfig, LoadStatus } from "../types.js"
|
|
4
|
+
import casetteHissUrl from "../assets/casette_hiss.mp3"
|
|
5
|
+
import noiseImg from "../assets/noise_50.jpg"
|
|
6
|
+
import logoImg from "../assets/logo.svg?url"
|
|
7
|
+
import openstudioImg from "../assets/openstudio.svg?url"
|
|
8
|
+
import { onMount } from "svelte"
|
|
9
|
+
import Knob from "./els/Knob.svelte"
|
|
10
|
+
import Light from "./els/Light.svelte"
|
|
11
|
+
import Lights from "./els/Lights.svelte"
|
|
12
|
+
import Slider from "./els/Slider.svelte"
|
|
13
|
+
import SlideSelect from "./els/SlideSelect.svelte"
|
|
14
|
+
import Timestamp from "./els/Timestamp.svelte"
|
|
15
|
+
import Cassette from "./Cassette.svelte"
|
|
16
|
+
import TransportButtons from "./TransportButtons.svelte"
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
hiddenTracks = [{ url: casetteHissUrl, volume: 0.08 }],
|
|
20
|
+
onready,
|
|
21
|
+
save = $bindable(),
|
|
22
|
+
load = $bindable(),
|
|
23
|
+
initialProject,
|
|
24
|
+
status = $bindable<LoadStatus>("idle"),
|
|
25
|
+
loadProgress = $bindable(0),
|
|
26
|
+
}: {
|
|
27
|
+
hiddenTracks?: HiddenTrackConfig[]
|
|
28
|
+
onready?: (detail: { engine: AudioEngine }) => void
|
|
29
|
+
save?: () => Blob
|
|
30
|
+
load?: (source: File | string) => Promise<void>
|
|
31
|
+
initialProject?: string | File
|
|
32
|
+
status?: LoadStatus
|
|
33
|
+
loadProgress?: number
|
|
34
|
+
} = $props()
|
|
35
|
+
|
|
36
|
+
let engine: AudioEngine | null = $state(null)
|
|
37
|
+
let selectedTrack = $state(0)
|
|
38
|
+
let speed = $state(0)
|
|
39
|
+
let recordEngaged = $state(false)
|
|
40
|
+
|
|
41
|
+
async function fetchWithProgress(url: string): Promise<File> {
|
|
42
|
+
const response = await fetch(url)
|
|
43
|
+
const contentLength = response.headers.get("Content-Length")
|
|
44
|
+
|
|
45
|
+
if (!contentLength || !response.body) {
|
|
46
|
+
const buffer = await response.arrayBuffer()
|
|
47
|
+
loadProgress = 1
|
|
48
|
+
return new File([buffer], "project.4trk")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const total = parseInt(contentLength, 10)
|
|
52
|
+
const reader = response.body.getReader()
|
|
53
|
+
const chunks: Uint8Array[] = []
|
|
54
|
+
let received = 0
|
|
55
|
+
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read()
|
|
58
|
+
if (done) break
|
|
59
|
+
chunks.push(value)
|
|
60
|
+
received += value.length
|
|
61
|
+
loadProgress = received / total
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const buffer = new Uint8Array(received)
|
|
65
|
+
let offset = 0
|
|
66
|
+
for (const chunk of chunks) {
|
|
67
|
+
buffer.set(chunk, offset)
|
|
68
|
+
offset += chunk.length
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return new File([buffer], "project.4trk")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onMount(async () => {
|
|
75
|
+
engine = new AudioEngine({ hiddenTracks })
|
|
76
|
+
engine.initAudioContext()
|
|
77
|
+
|
|
78
|
+
save = () => engine!.exportProject()
|
|
79
|
+
load = async (source: File | string) => {
|
|
80
|
+
status = "loading"
|
|
81
|
+
loadProgress = 0
|
|
82
|
+
try {
|
|
83
|
+
const file =
|
|
84
|
+
typeof source === "string" ? await fetchWithProgress(source) : source
|
|
85
|
+
await engine!.importProject(file)
|
|
86
|
+
status = "ready"
|
|
87
|
+
} catch (e) {
|
|
88
|
+
status = "error"
|
|
89
|
+
throw e
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (initialProject) {
|
|
94
|
+
await load(initialProject)
|
|
95
|
+
} else {
|
|
96
|
+
status = "ready"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onready?.({ engine })
|
|
100
|
+
return () => engine?.dispose()
|
|
101
|
+
})
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
{#if engine}
|
|
105
|
+
{@const tracks = engine.tracks}
|
|
106
|
+
|
|
107
|
+
{#snippet channelStrip(track, i)}
|
|
108
|
+
<div
|
|
109
|
+
class="channel-lights cell-center"
|
|
110
|
+
style="grid-area: {i + 3} / 2 / {i + 4} / 3"
|
|
111
|
+
>
|
|
112
|
+
<Lights level={track.level} />
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="cell-center" style="grid-area: {i + 3} / 3 / {i + 4} / 4">
|
|
116
|
+
<Knob
|
|
117
|
+
min={0}
|
|
118
|
+
max={1.5}
|
|
119
|
+
bind:value={track.volume}
|
|
120
|
+
onchange={(vol) => engine.setTrackVolume(i, vol)}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="cell-center" style="grid-area: {i + 3} / 4 / {i + 4} / 5">
|
|
125
|
+
<Knob
|
|
126
|
+
min={-1}
|
|
127
|
+
max={1}
|
|
128
|
+
bind:value={track.pan}
|
|
129
|
+
onchange={(pan) => engine.setTrackPan(i, pan)}
|
|
130
|
+
label={"TRK " + (i + 1)}
|
|
131
|
+
labelLeft="L"
|
|
132
|
+
labelRight="R"
|
|
133
|
+
color="pink"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
{/snippet}
|
|
137
|
+
|
|
138
|
+
<div
|
|
139
|
+
class="fourtrack"
|
|
140
|
+
style:--bg-noise="url({noiseImg})"
|
|
141
|
+
style:--bg-logo="url({logoImg})"
|
|
142
|
+
style:--bg-openstudio="url({openstudioImg})"
|
|
143
|
+
>
|
|
144
|
+
<div class="frame">
|
|
145
|
+
<div class="app">
|
|
146
|
+
<div class="parent">
|
|
147
|
+
<div class="cell-center" style="grid-area: 2 / 2 / 3 / 3">
|
|
148
|
+
<span class="ui-label">Phones</span>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="master cell-center" style="grid-area: 2 / 3 / 3 / 4">
|
|
152
|
+
<div class="phonos-button">
|
|
153
|
+
<Knob
|
|
154
|
+
min={0}
|
|
155
|
+
max={1.5}
|
|
156
|
+
value={engine.masterVolume}
|
|
157
|
+
onchange={(vol) => engine.setMasterVolume(vol)}
|
|
158
|
+
color="green"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Mixer: Channel strips -->
|
|
164
|
+
{#each tracks as track, i}
|
|
165
|
+
{#if !track.hidden}
|
|
166
|
+
{@render channelStrip(track, i)}
|
|
167
|
+
{/if}
|
|
168
|
+
{/each}
|
|
169
|
+
|
|
170
|
+
<div class="ui-label cell-center" style="grid-area: 7 / 3 / 8 / 4">
|
|
171
|
+
Level
|
|
172
|
+
</div>
|
|
173
|
+
<div class="ui-label cell-center" style="grid-area: 7 / 4 / 8 / 5">
|
|
174
|
+
Pan
|
|
175
|
+
</div>
|
|
176
|
+
<div class="cell-center" style="grid-area: 8 / 3 / 9 / 5">
|
|
177
|
+
<span class="output ui-label">└── Output ──┘</span>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="cell-center" style="grid-area: 1 / 5 / 9 / 6">
|
|
181
|
+
<div class="separator"></div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div class="cell-center" style="grid-area: 5 / 6 / 7 / 7">
|
|
185
|
+
<SlideSelect bind:value={selectedTrack} disabled={recordEngaged} />
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div
|
|
189
|
+
class="ui-label cell-right"
|
|
190
|
+
style="grid-area: 7 / 6 / 8 / 7; text-align: right"
|
|
191
|
+
>
|
|
192
|
+
Track
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div class="cell-center" style="grid-area: 2 / 6 / 3 / 8">
|
|
196
|
+
<div class="mic-status">
|
|
197
|
+
<div class="ui-label">mic status</div>
|
|
198
|
+
<div
|
|
199
|
+
class="mic-status-light"
|
|
200
|
+
title="Microphone status: {engine.micStatus}"
|
|
201
|
+
>
|
|
202
|
+
<Light
|
|
203
|
+
color={engine.micStatus === "prompt" ||
|
|
204
|
+
engine.micStatus === "active"
|
|
205
|
+
? "green"
|
|
206
|
+
: "red"}
|
|
207
|
+
active={engine.micStatus === "active"}
|
|
208
|
+
pulsing={engine.micStatus === "no-device"
|
|
209
|
+
? "fast"
|
|
210
|
+
: engine.micStatus === "inactive" ||
|
|
211
|
+
engine.micStatus === "active"
|
|
212
|
+
? false
|
|
213
|
+
: "slow"}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Input Controls -->
|
|
220
|
+
<div class="cell-center" style="grid-area: 3 / 6 / 4 / 8">
|
|
221
|
+
<Knob
|
|
222
|
+
min={-1}
|
|
223
|
+
max={1}
|
|
224
|
+
bind:value={engine.trimValue}
|
|
225
|
+
onchange={(trim) => engine.setTrim(trim)}
|
|
226
|
+
label="TRIM"
|
|
227
|
+
labelLeft="LINE"
|
|
228
|
+
labelRight="MIC"
|
|
229
|
+
color="red"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div class="cell-center" style="grid-area: 4 / 7 / 7 / 8">
|
|
234
|
+
<Slider
|
|
235
|
+
min={0}
|
|
236
|
+
max={1.5}
|
|
237
|
+
bind:value={engine.recordingVolume}
|
|
238
|
+
onchange={(vol) => engine.setRecordingVolume(vol)}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="ui-label cell-center" style="grid-area: 7 / 7 / 8 / 8">
|
|
243
|
+
Volume
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<div class="ui-label cell-center" style="grid-area: 8 / 6 / 9 / 8">
|
|
247
|
+
└─ Input ─┘
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div class="cell-center" style="grid-area: 1 / 8 / 9 / 9">
|
|
251
|
+
<div class="separator"></div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div class="cell-center" style="grid-area: 2 / 9 / 3 / 10">
|
|
255
|
+
<div class="mic-status">
|
|
256
|
+
<div class="ui-label">power</div>
|
|
257
|
+
<div class="mic-status-light" title="Cassette status: xx">
|
|
258
|
+
<Light color="green" active={true} pulsing={false} />
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div class="cell-timestamp" style="grid-area: 2 / 10 / 3 / 11">
|
|
264
|
+
<Timestamp timestamp={engine.position} />
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div class="logos" style="grid-area: 2 / 11 / 3 / 12">
|
|
268
|
+
<div class="logo"></div>
|
|
269
|
+
<div class="logo-tag"></div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div style="grid-area: 3 / 9 / 6 / 13">
|
|
273
|
+
<Cassette
|
|
274
|
+
{speed}
|
|
275
|
+
time={engine.position}
|
|
276
|
+
max={engine.duration || 180}
|
|
277
|
+
onchange={(ts) => engine.seek(ts)}
|
|
278
|
+
isRecording={engine.playState === "recording"}
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div class="cell-bottom" style="grid-area: 6 / 9 / 9 / 12">
|
|
283
|
+
<TransportButtons
|
|
284
|
+
{engine}
|
|
285
|
+
{selectedTrack}
|
|
286
|
+
bind:speed
|
|
287
|
+
bind:recordEngaged
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
{/if}
|
|
295
|
+
|
|
296
|
+
<style>
|
|
297
|
+
/* Scoped utility classes */
|
|
298
|
+
.fourtrack :global(.cell-center) {
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
justify-content: center;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.fourtrack :global(.cell-bottom) {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: end;
|
|
307
|
+
justify-content: center;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.fourtrack :global(.cell-right) {
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: right;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.fourtrack :global(.ui-label) {
|
|
317
|
+
color: rgba(255, 255, 255, 0.6);
|
|
318
|
+
text-transform: uppercase;
|
|
319
|
+
font-size: 1.8cqh;
|
|
320
|
+
font-family: sans-serif;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.fourtrack {
|
|
324
|
+
container-type: size;
|
|
325
|
+
aspect-ratio: 1 / 0.6;
|
|
326
|
+
width: 100%;
|
|
327
|
+
/* max-height: 75dvh; */
|
|
328
|
+
/* max-width: min(90vw, calc(75dvh / 0.6)); */
|
|
329
|
+
user-select: none;
|
|
330
|
+
|
|
331
|
+
@media (max-width: 1024px) {
|
|
332
|
+
max-height: 90dvh;
|
|
333
|
+
/* max-width: min(95vw, calc(90dvh / 0.6)); */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.separator {
|
|
338
|
+
height: 100%;
|
|
339
|
+
width: 0.45cqw;
|
|
340
|
+
border-radius: 0 0 0.15cqw 0.15cqw;
|
|
341
|
+
box-shadow:
|
|
342
|
+
inset 2px 2px 1px rgb(19 18 18 / 75%),
|
|
343
|
+
inset 1px 1px 1px rgba(31, 31, 31, 0.45),
|
|
344
|
+
inset -1px -1px 1px rgba(255, 252, 252, 0.35);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.parent {
|
|
348
|
+
display: grid;
|
|
349
|
+
grid-template-columns: 4cqw 4cqw 10cqw 10cqw 4cqw 4cqw 8cqw 5cqw 10cqw 1fr 1fr 4cqw;
|
|
350
|
+
grid-template-rows: 3cqw 9cqw 1fr 1fr 1fr 1fr 2.4cqw 2.4cqw 4.8cqw;
|
|
351
|
+
grid-column-gap: 0px;
|
|
352
|
+
grid-row-gap: 0px;
|
|
353
|
+
height: 100%;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.frame {
|
|
357
|
+
background: linear-gradient(to bottom, #616161, #3b3b3b);
|
|
358
|
+
padding: 1px;
|
|
359
|
+
border-radius: 1cqw 1cqw 2.5cqw 2.5cqw;
|
|
360
|
+
height: 100%;
|
|
361
|
+
|
|
362
|
+
box-shadow: 30px 20px 30px 0px rgb(33 34 36 / 31%);
|
|
363
|
+
position: relative;
|
|
364
|
+
&:before {
|
|
365
|
+
position: absolute;
|
|
366
|
+
content: " ";
|
|
367
|
+
width: 96%;
|
|
368
|
+
height: 0.5cqw;
|
|
369
|
+
margin: 0 2%;
|
|
370
|
+
background: linear-gradient(to right, #6f7074, #505252);
|
|
371
|
+
top: -0.5cqw;
|
|
372
|
+
border-radius: 10cqw 10cqw 0 0;
|
|
373
|
+
box-shadow: inset 0cqw 0.3cqh 0.2cqw rgb(225 225 225 / 40%);
|
|
374
|
+
border: 1px solid #686868;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.app {
|
|
379
|
+
background: radial-gradient(ellipse at top left, #686b71, #383840);
|
|
380
|
+
border-radius: 1cqw 1cqw 3cqw 3cqw;
|
|
381
|
+
height: 100cqh;
|
|
382
|
+
box-shadow:
|
|
383
|
+
inset 0.2cqw 0.5cqh 0.4cqw rgb(225 225 225 / 50%),
|
|
384
|
+
inset -0.2cqw -1.5cqh 0.2cqw rgb(0 0 0 / 26.6%);
|
|
385
|
+
|
|
386
|
+
position: relative;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.app:before {
|
|
390
|
+
content: " ";
|
|
391
|
+
width: 100%;
|
|
392
|
+
height: 100%;
|
|
393
|
+
display: block;
|
|
394
|
+
background: var(--bg-noise);
|
|
395
|
+
background-size: 50px;
|
|
396
|
+
mix-blend-mode: multiply;
|
|
397
|
+
position: absolute;
|
|
398
|
+
opacity: 0.9;
|
|
399
|
+
border-radius: 10px 10px 36px 36px;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.logos {
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-direction: column;
|
|
405
|
+
gap: 5cqh;
|
|
406
|
+
}
|
|
407
|
+
.logo {
|
|
408
|
+
background: var(--bg-logo);
|
|
409
|
+
background-repeat: no-repeat;
|
|
410
|
+
background-size: contain;
|
|
411
|
+
background-position: top right;
|
|
412
|
+
width: 100%;
|
|
413
|
+
height: 2.75cqh;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.logo-tag {
|
|
417
|
+
background: var(--bg-openstudio);
|
|
418
|
+
background-repeat: no-repeat;
|
|
419
|
+
background-size: contain;
|
|
420
|
+
background-position: top right;
|
|
421
|
+
width: 100%;
|
|
422
|
+
height: 2.75cqh;
|
|
423
|
+
opacity: 0.6;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.mic-status {
|
|
427
|
+
text-align: center;
|
|
428
|
+
cursor: help;
|
|
429
|
+
transform: translateY(-2cqh);
|
|
430
|
+
.ui-label {
|
|
431
|
+
margin-bottom: 2cqh;
|
|
432
|
+
}
|
|
433
|
+
.mic-status-light {
|
|
434
|
+
display: flex;
|
|
435
|
+
justify-content: center;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.cell-timestamp {
|
|
440
|
+
padding-top: 3cqh;
|
|
441
|
+
padding-left: 2cqw;
|
|
442
|
+
}
|
|
443
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AudioEngine } from "../audio/engine.svelte.js";
|
|
2
|
+
import type { HiddenTrackConfig, LoadStatus } from "../types.js";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
hiddenTracks?: HiddenTrackConfig[];
|
|
5
|
+
onready?: (detail: {
|
|
6
|
+
engine: AudioEngine;
|
|
7
|
+
}) => void;
|
|
8
|
+
save?: () => Blob;
|
|
9
|
+
load?: (source: File | string) => Promise<void>;
|
|
10
|
+
initialProject?: string | File;
|
|
11
|
+
status?: LoadStatus;
|
|
12
|
+
loadProgress?: number;
|
|
13
|
+
};
|
|
14
|
+
declare const FourTrack: import("svelte").Component<$$ComponentProps, {}, "save" | "load" | "status" | "loadProgress">;
|
|
15
|
+
type FourTrack = ReturnType<typeof FourTrack>;
|
|
16
|
+
export default FourTrack;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AudioEngine } from ".."
|
|
3
|
+
import Knob from "./els/Knob.svelte"
|
|
4
|
+
import Lights from "./els/Lights.svelte"
|
|
5
|
+
let { engine }: { engine: AudioEngine } = $props()
|
|
6
|
+
|
|
7
|
+
let mVolume = $state(0)
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="row master">
|
|
11
|
+
<!-- <span class="col1 ui-label">Phones</span> -->
|
|
12
|
+
<div class="col2 phonos-button">
|
|
13
|
+
<Knob
|
|
14
|
+
min={0}
|
|
15
|
+
max={1.5}
|
|
16
|
+
value={engine.masterVolume}
|
|
17
|
+
onchange={(vol) => engine.setMasterVolume(vol)}
|
|
18
|
+
color="green"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
{#snippet channelStrip(track, i)}
|
|
24
|
+
<div class="col1 channel-lights" style="grid-area: {i + 2} / 2 / {i + 3} / 3">
|
|
25
|
+
<Lights level={track.level} />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="col2 channel-knob" style="grid-area: {i + 2} / 3 / {i + 3} / 4">
|
|
29
|
+
<Knob
|
|
30
|
+
min={0}
|
|
31
|
+
max={1.5}
|
|
32
|
+
bind:value={track.volume}
|
|
33
|
+
onchange={(vol) => engine.setTrackVolume(i, vol)}
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="col3 channel-knob" style="grid-area: {i + 2} / 4 / {i + 3} / 5">
|
|
38
|
+
<Knob
|
|
39
|
+
min={-1}
|
|
40
|
+
max={1}
|
|
41
|
+
bind:value={track.pan}
|
|
42
|
+
onchange={(pan) => engine.setTrackPan(i, pan)}
|
|
43
|
+
labelLeft="L"
|
|
44
|
+
labelRight="R"
|
|
45
|
+
color="pink"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
{/snippet}
|
|
49
|
+
|
|
50
|
+
<!-- Not sure if sunippets are most useful here, but wanted to ttry them -->
|
|
51
|
+
{#each engine.tracks as track, i}
|
|
52
|
+
{#if !track.hidden}
|
|
53
|
+
{@render channelStrip(track, i)}
|
|
54
|
+
{/if}
|
|
55
|
+
{/each}
|
|
56
|
+
|
|
57
|
+
<div class="ui-label" style="grid-area: 6 / 3 / 7 / 6">Level</div>
|
|
58
|
+
<div class="ui-label" style="grid-area: 6 / 4 / 7 / 7">Pan</div>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.master {
|
|
62
|
+
height: 14cqw;
|
|
63
|
+
padding-top: 5cqw;
|
|
64
|
+
aspect-ratio: 1 / 1;
|
|
65
|
+
grid-area: 1 / 3 / 2 / 4;
|
|
66
|
+
}
|
|
67
|
+
.channel-strip {
|
|
68
|
+
/* align-items: center; */
|
|
69
|
+
height: 10cqw;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.channel-knob {
|
|
73
|
+
/* width: 7cqw; */
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.bg {
|
|
79
|
+
background: rgba(46, 46, 46, 0.5);
|
|
80
|
+
width: 20px;
|
|
81
|
+
height: 40px;
|
|
82
|
+
transform: rotate(-50deg);
|
|
83
|
+
border-radius: 20px;
|
|
84
|
+
outline: 1px solid rgb(36, 36, 36);
|
|
85
|
+
padding: 2px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.light {
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
/* margin: 40%; */
|
|
91
|
+
width: 1cqw;
|
|
92
|
+
height: 1cqw;
|
|
93
|
+
aspect-ratio: 1 / 1;
|
|
94
|
+
opacity: 0.4;
|
|
95
|
+
&.low {
|
|
96
|
+
background-color: rgb(166, 255, 0);
|
|
97
|
+
}
|
|
98
|
+
&.high {
|
|
99
|
+
background-color: red;
|
|
100
|
+
}
|
|
101
|
+
&.active {
|
|
102
|
+
opacity: 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|