4track 0.1.9 → 0.1.12
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/dist/audio/constants.d.ts +1 -1
- package/dist/audio/constants.js +1 -1
- package/dist/audio/engine.svelte.d.ts +4 -1
- package/dist/audio/engine.svelte.js +15 -9
- package/dist/audio/project-io.d.ts +3 -2
- package/dist/audio/project-io.js +15 -2
- package/dist/components/FourTrack.svelte +9 -2
- package/dist/components/FourTrack.svelte.d.ts +3 -2
- package/dist/components/TransportButtons.svelte +2 -1
- package/dist/components/els/DigitRoller.svelte +24 -31
- package/dist/components/els/DigitRoller.svelte.d.ts +2 -1
- package/dist/components/els/Timestamp.svelte +12 -21
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/audio/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Default configuration and constraints for the audio engine.
|
|
2
2
|
import workletUrl from "../assets/recorder-worklet.js?url";
|
|
3
|
-
export const PLAYBACK_TICK_MS =
|
|
3
|
+
export const PLAYBACK_TICK_MS = 32; // 30 fps
|
|
4
4
|
export const DEFAULT_CONFIG = {
|
|
5
5
|
sampleRate: 32000, // 44100 | 48000 | 96000
|
|
6
6
|
bitDepth: 16, // 8 = lo-fi, 16 = CD quality, 32 = float (uncompressed)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AudioEngineConfig, MicStatus, PlayState } from "../types.js";
|
|
1
|
+
import type { AudioEngineConfig, MicStatus, PlayState, ProjectMeta } from "../types.js";
|
|
2
2
|
import { Track } from "./track.svelte.js";
|
|
3
3
|
export declare class AudioEngine {
|
|
4
4
|
playState: PlayState;
|
|
@@ -8,7 +8,10 @@ export declare class AudioEngine {
|
|
|
8
8
|
latencyInfo: string;
|
|
9
9
|
trimValue: number;
|
|
10
10
|
recordingVolume: number;
|
|
11
|
+
meta: ProjectMeta;
|
|
11
12
|
tracks: Track[];
|
|
13
|
+
/** Rounds a time value to 2 decimal places for position display. */
|
|
14
|
+
private roundPosition;
|
|
12
15
|
private config;
|
|
13
16
|
private audioContext;
|
|
14
17
|
private masterGainNode;
|
|
@@ -16,7 +16,12 @@ export class AudioEngine {
|
|
|
16
16
|
latencyInfo = $state("");
|
|
17
17
|
trimValue = $state(-1);
|
|
18
18
|
recordingVolume = $state(0.75);
|
|
19
|
+
meta = $state({ artist: "", title: "", comment: "" });
|
|
19
20
|
tracks;
|
|
21
|
+
/** Rounds a time value to 2 decimal places for position display. */
|
|
22
|
+
roundPosition(seconds) {
|
|
23
|
+
return Math.round(seconds * 100) / 100;
|
|
24
|
+
}
|
|
20
25
|
// ─── Private state ──────────────────────────────────────────────────
|
|
21
26
|
config;
|
|
22
27
|
// Web Audio graph
|
|
@@ -172,7 +177,7 @@ export class AudioEngine {
|
|
|
172
177
|
const max = this.getMaxDuration();
|
|
173
178
|
const clamped = Math.max(0, Math.min(seconds, max));
|
|
174
179
|
this.playbackOffset = clamped;
|
|
175
|
-
this.position =
|
|
180
|
+
this.position = this.roundPosition(clamped);
|
|
176
181
|
if (this.playState === "playing") {
|
|
177
182
|
this.stopSources(this.activePlaybackSources);
|
|
178
183
|
this.clearPlaybackTick();
|
|
@@ -219,14 +224,14 @@ export class AudioEngine {
|
|
|
219
224
|
}
|
|
220
225
|
this.playbackStartTime = startTime;
|
|
221
226
|
this.playbackOffset = offsetSeconds;
|
|
222
|
-
this.position =
|
|
227
|
+
this.position = this.roundPosition(offsetSeconds);
|
|
223
228
|
this.playState = "playing";
|
|
224
229
|
this.playbackTickId = window.setInterval(() => {
|
|
225
230
|
const elapsed = ctx.currentTime - this.playbackStartTime;
|
|
226
|
-
this.position =
|
|
231
|
+
this.position = this.roundPosition(this.playbackOffset + elapsed);
|
|
227
232
|
if (elapsed >= effectiveDuration) {
|
|
228
233
|
this.playbackOffset = maxDuration;
|
|
229
|
-
this.position =
|
|
234
|
+
this.position = this.roundPosition(maxDuration);
|
|
230
235
|
this.clearPlaybackTick();
|
|
231
236
|
this.playState = "stopped";
|
|
232
237
|
}
|
|
@@ -242,7 +247,7 @@ export class AudioEngine {
|
|
|
242
247
|
this.stopSources(this.activePlaybackSources);
|
|
243
248
|
}
|
|
244
249
|
this.clearPlaybackTick();
|
|
245
|
-
this.position =
|
|
250
|
+
this.position = this.roundPosition(this.playbackOffset);
|
|
246
251
|
this.playState = "paused";
|
|
247
252
|
}
|
|
248
253
|
/** Stops playback or recording. If recording, finalizes and merges the recorded audio. */
|
|
@@ -258,7 +263,7 @@ export class AudioEngine {
|
|
|
258
263
|
this.stopSources(this.activePlaybackSources);
|
|
259
264
|
}
|
|
260
265
|
this.clearPlaybackTick();
|
|
261
|
-
this.position =
|
|
266
|
+
this.position = this.roundPosition(this.playbackOffset);
|
|
262
267
|
}
|
|
263
268
|
this.playState = "stopped";
|
|
264
269
|
}
|
|
@@ -457,7 +462,7 @@ export class AudioEngine {
|
|
|
457
462
|
this.recordingTrackIndex = trackIndex;
|
|
458
463
|
this.recordingLatencySeconds = recordLatencySeconds;
|
|
459
464
|
this.punchInOffset = this.playbackOffset;
|
|
460
|
-
this.position =
|
|
465
|
+
this.position = this.roundPosition(this.punchInOffset);
|
|
461
466
|
this.playState = "recording";
|
|
462
467
|
// Play other tracks for overdub monitoring, start meters and position timer
|
|
463
468
|
this.playOtherTracksForMonitoring(trackIndex, this.punchInOffset);
|
|
@@ -573,12 +578,13 @@ export class AudioEngine {
|
|
|
573
578
|
// ─── Save / Load ────────────────────────────────────────────────────
|
|
574
579
|
/** Serializes all tracks and settings into a compressed .4trk binary blob. */
|
|
575
580
|
exportProject() {
|
|
576
|
-
return _exportProject(this.tracks, this.config, this.masterVolume);
|
|
581
|
+
return _exportProject(this.tracks, this.config, this.masterVolume, this.meta);
|
|
577
582
|
}
|
|
578
583
|
/** Loads a .4trk file, restoring all track buffers, mixer settings, and master volume. */
|
|
579
584
|
async importProject(file) {
|
|
580
|
-
const { masterVolume } = await _importProject(file, this.tracks, () => this.ensureContext());
|
|
585
|
+
const { masterVolume, meta } = await _importProject(file, this.tracks, () => this.ensureContext());
|
|
581
586
|
this.setMasterVolume(masterVolume);
|
|
587
|
+
this.meta = meta;
|
|
582
588
|
this.rewind();
|
|
583
589
|
}
|
|
584
590
|
// ─── Cleanup ────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Track } from './track.svelte.js';
|
|
2
|
-
import type { AudioEngineConfig } from '../types.js';
|
|
3
|
-
export declare function exportProject(tracks: Track[], config: AudioEngineConfig, masterVolume: number): Promise<Blob>;
|
|
2
|
+
import type { AudioEngineConfig, ProjectMeta } from '../types.js';
|
|
3
|
+
export declare function exportProject(tracks: Track[], config: AudioEngineConfig, masterVolume: number, meta?: ProjectMeta): Promise<Blob>;
|
|
4
4
|
export declare function importProject(file: File | Blob, tracks: Track[], ensureContext: () => AudioContext): Promise<{
|
|
5
5
|
masterVolume: number;
|
|
6
|
+
meta: ProjectMeta;
|
|
6
7
|
}>;
|
package/dist/audio/project-io.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Uses integer quantization from ./pcm.ts for compact storage.
|
|
4
4
|
import { Track } from './track.svelte.js';
|
|
5
5
|
import { quantizePCM, dequantizePCM } from './pcm.js';
|
|
6
|
-
export async function exportProject(tracks, config, masterVolume) {
|
|
6
|
+
export async function exportProject(tracks, config, masterVolume, meta = { artist: '', title: '', comment: '' }) {
|
|
7
7
|
const trackMeta = [];
|
|
8
8
|
const pcmParts = [];
|
|
9
9
|
for (const track of tracks) {
|
|
@@ -37,6 +37,12 @@ export async function exportProject(tracks, config, masterVolume) {
|
|
|
37
37
|
masterVolume,
|
|
38
38
|
tracks: trackMeta,
|
|
39
39
|
};
|
|
40
|
+
if (meta.artist)
|
|
41
|
+
metadata.artist = meta.artist.slice(0, 64);
|
|
42
|
+
if (meta.title)
|
|
43
|
+
metadata.title = meta.title.slice(0, 64);
|
|
44
|
+
if (meta.comment)
|
|
45
|
+
metadata.comment = meta.comment.slice(0, 256);
|
|
40
46
|
const encoder = new TextEncoder();
|
|
41
47
|
const metaBytes = encoder.encode(JSON.stringify(metadata));
|
|
42
48
|
const metaLength = new Uint32Array([metaBytes.length]);
|
|
@@ -84,5 +90,12 @@ export async function importProject(file, tracks, ensureContext) {
|
|
|
84
90
|
if (track.panNode)
|
|
85
91
|
track.panNode.pan.value = track.pan;
|
|
86
92
|
}
|
|
87
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
masterVolume: metadata.masterVolume ?? 1.0,
|
|
95
|
+
meta: {
|
|
96
|
+
artist: metadata.artist ?? '',
|
|
97
|
+
title: metadata.title ?? '',
|
|
98
|
+
comment: metadata.comment ?? '',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
88
101
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { AudioEngine } from "../audio/engine.svelte.js"
|
|
3
|
-
import type { HiddenTrackConfig, LoadStatus } from "../types.js"
|
|
3
|
+
import type { HiddenTrackConfig, LoadStatus, ProjectMeta } from "../types.js"
|
|
4
4
|
import casetteHissUrl from "../assets/casette_hiss_compressed.mp3"
|
|
5
5
|
import noiseImg from "../assets/noise_50.jpg"
|
|
6
6
|
import logoImg from "../assets/logo.svg?url"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
initialProject,
|
|
24
24
|
status = $bindable<LoadStatus>("idle"),
|
|
25
25
|
loadProgress = $bindable(0),
|
|
26
|
+
meta = $bindable<ProjectMeta>({ artist: "", title: "", comment: "" }),
|
|
26
27
|
}: {
|
|
27
28
|
hiddenTracks?: HiddenTrackConfig[]
|
|
28
29
|
onready?: (detail: { engine: AudioEngine }) => void
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
initialProject?: string | File
|
|
32
33
|
status?: LoadStatus
|
|
33
34
|
loadProgress?: number
|
|
35
|
+
meta?: ProjectMeta
|
|
34
36
|
} = $props()
|
|
35
37
|
|
|
36
38
|
let engine: AudioEngine | null = $state(null)
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
let recordEngaged = $state(false)
|
|
40
42
|
let resetTransport: (() => void) | undefined = $state()
|
|
41
43
|
|
|
44
|
+
|
|
42
45
|
async function fetchWithProgress(url: string): Promise<File> {
|
|
43
46
|
const response = await fetch(url)
|
|
44
47
|
const contentLength = response.headers.get("Content-Length")
|
|
@@ -76,7 +79,10 @@
|
|
|
76
79
|
engine = new AudioEngine({ hiddenTracks })
|
|
77
80
|
engine.initAudioContext()
|
|
78
81
|
|
|
79
|
-
save = () =>
|
|
82
|
+
save = () => {
|
|
83
|
+
engine!.meta = { ...meta }
|
|
84
|
+
return engine!.exportProject()
|
|
85
|
+
}
|
|
80
86
|
load = async (source: File | string) => {
|
|
81
87
|
status = "loading"
|
|
82
88
|
loadProgress = 0
|
|
@@ -84,6 +90,7 @@
|
|
|
84
90
|
const file =
|
|
85
91
|
typeof source === "string" ? await fetchWithProgress(source) : source
|
|
86
92
|
await engine!.importProject(file)
|
|
93
|
+
meta = { ...engine!.meta }
|
|
87
94
|
resetTransport?.()
|
|
88
95
|
status = "ready"
|
|
89
96
|
} catch (e) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AudioEngine } from "../audio/engine.svelte.js";
|
|
2
|
-
import type { HiddenTrackConfig, LoadStatus } from "../types.js";
|
|
2
|
+
import type { HiddenTrackConfig, LoadStatus, ProjectMeta } from "../types.js";
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
hiddenTracks?: HiddenTrackConfig[];
|
|
5
5
|
onready?: (detail: {
|
|
@@ -10,7 +10,8 @@ type $$ComponentProps = {
|
|
|
10
10
|
initialProject?: string | File;
|
|
11
11
|
status?: LoadStatus;
|
|
12
12
|
loadProgress?: number;
|
|
13
|
+
meta?: ProjectMeta;
|
|
13
14
|
};
|
|
14
|
-
declare const FourTrack: import("svelte").Component<$$ComponentProps, {}, "save" | "load" | "status" | "loadProgress">;
|
|
15
|
+
declare const FourTrack: import("svelte").Component<$$ComponentProps, {}, "meta" | "save" | "load" | "status" | "loadProgress">;
|
|
15
16
|
type FourTrack = ReturnType<typeof FourTrack>;
|
|
16
17
|
export default FourTrack;
|
|
@@ -43,8 +43,9 @@
|
|
|
43
43
|
$effect(() => {
|
|
44
44
|
let timer: ReturnType<typeof setInterval> | undefined
|
|
45
45
|
|
|
46
|
-
// Trigger stop when rewinding and reaching the
|
|
46
|
+
// Trigger stop when rewinding and reaching the beginning
|
|
47
47
|
if (speed < 0 && engine.position + speed / 50 <= 0) {
|
|
48
|
+
engine.seek(0)
|
|
48
49
|
clicky("stop")
|
|
49
50
|
}
|
|
50
51
|
if (engine.position + speed / 50 > engine.duration) {
|
|
@@ -1,48 +1,41 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
let {
|
|
2
|
+
let { timestamp = 0, index = 0 } = $props()
|
|
3
3
|
|
|
4
4
|
let roller: HTMLDivElement
|
|
5
|
-
let prev = -1
|
|
6
|
-
let wrapping = false
|
|
7
5
|
const step = 100 / 12
|
|
6
|
+
|
|
8
7
|
const pos = (d: number) => `translateY(${-step * (1 + d)}%)`
|
|
9
8
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
roller.offsetHeight
|
|
14
|
-
roller.style.transition = ""
|
|
9
|
+
function sigmoidEase(x: number, k: number, midpoint: number) {
|
|
10
|
+
const f = (v: number) => 1 / (1 + Math.exp(-k * (v - midpoint)))
|
|
11
|
+
return (f(x) - f(0)) / (f(1) - f(0))
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
$effect(() => {
|
|
18
|
-
const d = +digit
|
|
19
15
|
if (!roller) return
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
wrapping = true
|
|
25
|
-
roller.style.transform = `translateY(${-step * 11}%)`
|
|
26
|
-
} else if (prev === 0 && d === 9) {
|
|
27
|
-
wrapping = true
|
|
28
|
-
roller.style.transform = `translateY(0%)`
|
|
29
|
-
} else {
|
|
30
|
-
wrapping = false
|
|
31
|
-
roller.style.transform = pos(d)
|
|
32
|
-
}
|
|
17
|
+
// Get the value at this digit position (index 0=hundreds, 1=tens, 2=ones)
|
|
18
|
+
const divisor = Math.pow(10, 2 - index) //1, 10, 100
|
|
19
|
+
const value = (timestamp / divisor) % 10
|
|
33
20
|
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
// Split into whole digit and fractional part for smooth rolling
|
|
22
|
+
const digit = Math.floor(value)
|
|
23
|
+
const fraction = value - digit
|
|
36
24
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
const eased =
|
|
26
|
+
divisor == 1
|
|
27
|
+
? fraction
|
|
28
|
+
: sigmoidEase(
|
|
29
|
+
fraction,
|
|
30
|
+
divisor == 10 ? 12 : 50,
|
|
31
|
+
divisor == 10 ? 0.98 : 0.9995,
|
|
32
|
+
)
|
|
33
|
+
roller.style.transform = pos(digit + eased)
|
|
34
|
+
})
|
|
42
35
|
</script>
|
|
43
36
|
|
|
44
37
|
<div class="digits">
|
|
45
|
-
<div class="roller" bind:this={roller}
|
|
38
|
+
<div class="roller" bind:this={roller}>
|
|
46
39
|
{#each [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0] as n}
|
|
47
40
|
<div class="digit"><span>{n}</span></div>
|
|
48
41
|
{/each}
|
|
@@ -69,9 +62,9 @@
|
|
|
69
62
|
transform: translateY(-2cqw);
|
|
70
63
|
}
|
|
71
64
|
}
|
|
72
|
-
.roller {
|
|
65
|
+
/* .roller {
|
|
73
66
|
transition: 0.4s ease transform;
|
|
74
|
-
}
|
|
67
|
+
} */
|
|
75
68
|
.digit {
|
|
76
69
|
font-size: 35cqh;
|
|
77
70
|
/* letter-spacing: 7cqw; */
|
|
@@ -4,30 +4,21 @@
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
let { timestamp } = $props()
|
|
6
6
|
import { playFx } from "../../fx/soundfx"
|
|
7
|
+
import { Tween } from "svelte/motion"
|
|
7
8
|
import DigitRoller from "./DigitRoller.svelte"
|
|
8
|
-
import counterBgImg from
|
|
9
|
+
import counterBgImg from "../../assets/counter_bg.png"
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
function count_to_str(nr: number) {
|
|
12
|
-
var cor_nr = Math.floor(nr - correction)
|
|
13
|
-
if (cor_nr < 0) cor_nr += 1000
|
|
11
|
+
const correction = new Tween(0)
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return "00" + cor_nr.toString()
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function get_digit(nr, i) {
|
|
25
|
-
return count_to_str(nr).charAt(i)
|
|
26
|
-
}
|
|
13
|
+
let corrected = $derived.by(() => {
|
|
14
|
+
let v = timestamp - correction.current
|
|
15
|
+
if (v < 0) v += 1000
|
|
16
|
+
return v % 1000
|
|
17
|
+
})
|
|
27
18
|
|
|
28
19
|
function reset() {
|
|
29
20
|
playFx("counter")
|
|
30
|
-
correction
|
|
21
|
+
correction.set(timestamp, { duration: 400 })
|
|
31
22
|
}
|
|
32
23
|
</script>
|
|
33
24
|
|
|
@@ -37,9 +28,9 @@
|
|
|
37
28
|
<div class="counter">
|
|
38
29
|
<a onmousedown={() => reset()}> </a>
|
|
39
30
|
<div class="number-ticker">
|
|
40
|
-
<DigitRoller
|
|
41
|
-
<DigitRoller
|
|
42
|
-
<DigitRoller
|
|
31
|
+
<DigitRoller timestamp={corrected} index={0} />
|
|
32
|
+
<DigitRoller timestamp={corrected} index={1} />
|
|
33
|
+
<DigitRoller timestamp={corrected} index={2} />
|
|
43
34
|
</div>
|
|
44
35
|
</div>
|
|
45
36
|
</div>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { AudioEngine } from './audio/engine.svelte.js';
|
|
2
2
|
export { Track } from './audio/track.svelte.js';
|
|
3
3
|
export { default as FourTrack } from './components/FourTrack.svelte';
|
|
4
|
-
export type { AudioEngineConfig, HiddenTrackConfig, TrimFxConfig, ProjectMetadata, TrackMeta, PlayState, LoadStatus } from './types.js';
|
|
4
|
+
export type { AudioEngineConfig, HiddenTrackConfig, TrimFxConfig, ProjectMetadata, ProjectMeta, TrackMeta, PlayState, LoadStatus } from './types.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -34,6 +34,14 @@ export interface ProjectMetadata {
|
|
|
34
34
|
bitDepth: number;
|
|
35
35
|
masterVolume: number;
|
|
36
36
|
tracks: TrackMeta[];
|
|
37
|
+
artist?: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
comment?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ProjectMeta {
|
|
42
|
+
artist: string;
|
|
43
|
+
title: string;
|
|
44
|
+
comment: string;
|
|
37
45
|
}
|
|
38
46
|
export type PlayState = 'stopped' | 'playing' | 'paused' | 'recording';
|
|
39
47
|
export type MicStatus = 'unsupported' | 'prompt' | 'denied' | 'no-device' | 'inactive' | 'active' | 'error';
|