4track 0.1.10 → 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.
@@ -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,6 +8,7 @@ export declare class AudioEngine {
8
8
  latencyInfo: string;
9
9
  trimValue: number;
10
10
  recordingVolume: number;
11
+ meta: ProjectMeta;
11
12
  tracks: Track[];
12
13
  /** Rounds a time value to 2 decimal places for position display. */
13
14
  private roundPosition;
@@ -16,6 +16,7 @@ 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;
20
21
  /** Rounds a time value to 2 decimal places for position display. */
21
22
  roundPosition(seconds) {
@@ -577,12 +578,13 @@ export class AudioEngine {
577
578
  // ─── Save / Load ────────────────────────────────────────────────────
578
579
  /** Serializes all tracks and settings into a compressed .4trk binary blob. */
579
580
  exportProject() {
580
- return _exportProject(this.tracks, this.config, this.masterVolume);
581
+ return _exportProject(this.tracks, this.config, this.masterVolume, this.meta);
581
582
  }
582
583
  /** Loads a .4trk file, restoring all track buffers, mixer settings, and master volume. */
583
584
  async importProject(file) {
584
- const { masterVolume } = await _importProject(file, this.tracks, () => this.ensureContext());
585
+ const { masterVolume, meta } = await _importProject(file, this.tracks, () => this.ensureContext());
585
586
  this.setMasterVolume(masterVolume);
587
+ this.meta = meta;
586
588
  this.rewind();
587
589
  }
588
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
  }>;
@@ -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 { masterVolume: metadata.masterVolume ?? 1.0 };
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 = () => engine!.exportProject()
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;
@@ -22,7 +22,14 @@
22
22
  const digit = Math.floor(value)
23
23
  const fraction = value - digit
24
24
 
25
- const eased = divisor == 1 ? fraction : sigmoidEase(fraction, divisor == 10 ? 12 : 50, divisor == 10 ? 0.98 : 0.9995)
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
+ )
26
33
  roller.style.transform = pos(digit + eased)
27
34
  })
28
35
  </script>
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4track",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "A 4-track cassette recorder component for Svelte 5",
5
5
  "license": "GPL-3.0-only",
6
6
  "author": "Andre Boekhorst",
@@ -1,82 +0,0 @@
1
- <script lang="ts">
2
- let { digit = 0 } = $props()
3
-
4
- let roller: HTMLDivElement
5
- let prev = -1
6
- let wrapping = false
7
- const step = 100 / 12
8
- const pos = (d: number) => `translateY(${-step * (1 + d)}%)`
9
-
10
- function jumpTo(transform: string) {
11
- roller.style.transition = "none"
12
- roller.style.transform = transform
13
- roller.offsetHeight
14
- roller.style.transition = ""
15
- }
16
-
17
- $effect(() => {
18
- const d = +digit
19
- if (!roller) return
20
-
21
- if (prev === -1) {
22
- jumpTo(pos(d))
23
- } else if (prev === 9 && d === 0) {
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
- }
33
-
34
- prev = d
35
- })
36
-
37
- function onTransitionEnd() {
38
- if (!wrapping) return
39
- wrapping = false
40
- jumpTo(pos(+digit))
41
- }
42
- </script>
43
-
44
- <div class="digits">
45
- <div class="roller" bind:this={roller} ontransitionend={onTransitionEnd}>
46
- {#each [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0] as n}
47
- <div class="digit"><span>{n}</span></div>
48
- {/each}
49
- </div>
50
- </div>
51
-
52
- <style>
53
- .digits {
54
- overflow: hidden;
55
- height: 20cqw;
56
- width: 17cqw;
57
- text-align: center;
58
- color: #cfcdd3;
59
- background: linear-gradient(to bottom, #474748, #000000, #545454);
60
- border-right: 2px solid rgb(33, 33, 33);
61
- font-family: sans-serif;
62
- &:nth-child(1) {
63
- transform: translateY(-1.5cqw);
64
- }
65
- &:nth-child(2) {
66
- transform: translateY(-0.5cqw);
67
- }
68
- &:nth-child(3) {
69
- transform: translateY(-2cqw);
70
- }
71
- }
72
- .roller {
73
- transition: 0.4s ease transform;
74
- }
75
- .digit {
76
- font-size: 35cqh;
77
- /* letter-spacing: 7cqw; */
78
- }
79
- .span {
80
- padding-left: 1cqw;
81
- }
82
- </style>
@@ -1,5 +0,0 @@
1
- declare const DigitRollerFirst: import("svelte").Component<{
2
- digit?: number;
3
- }, {}, "">;
4
- type DigitRollerFirst = ReturnType<typeof DigitRollerFirst>;
5
- export default DigitRollerFirst;