4track 0.1.8 → 0.1.10

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
1
  import type { AudioEngineConfig } from "../types.js";
2
- export declare const PLAYBACK_TICK_MS = 50;
2
+ export declare const PLAYBACK_TICK_MS = 32;
3
3
  export declare const DEFAULT_CONFIG: AudioEngineConfig;
4
4
  export declare const AUDIO_CONSTRAINTS: MediaTrackConstraints;
@@ -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 = 50;
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)
@@ -9,6 +9,8 @@ export declare class AudioEngine {
9
9
  trimValue: number;
10
10
  recordingVolume: number;
11
11
  tracks: Track[];
12
+ /** Rounds a time value to 2 decimal places for position display. */
13
+ private roundPosition;
12
14
  private config;
13
15
  private audioContext;
14
16
  private masterGainNode;
@@ -17,6 +17,10 @@ export class AudioEngine {
17
17
  trimValue = $state(-1);
18
18
  recordingVolume = $state(0.75);
19
19
  tracks;
20
+ /** Rounds a time value to 2 decimal places for position display. */
21
+ roundPosition(seconds) {
22
+ return Math.round(seconds * 100) / 100;
23
+ }
20
24
  // ─── Private state ──────────────────────────────────────────────────
21
25
  config;
22
26
  // Web Audio graph
@@ -172,7 +176,7 @@ export class AudioEngine {
172
176
  const max = this.getMaxDuration();
173
177
  const clamped = Math.max(0, Math.min(seconds, max));
174
178
  this.playbackOffset = clamped;
175
- this.position = Math.round(clamped * 10) / 10;
179
+ this.position = this.roundPosition(clamped);
176
180
  if (this.playState === "playing") {
177
181
  this.stopSources(this.activePlaybackSources);
178
182
  this.clearPlaybackTick();
@@ -219,14 +223,14 @@ export class AudioEngine {
219
223
  }
220
224
  this.playbackStartTime = startTime;
221
225
  this.playbackOffset = offsetSeconds;
222
- this.position = Math.round(offsetSeconds * 10) / 10;
226
+ this.position = this.roundPosition(offsetSeconds);
223
227
  this.playState = "playing";
224
228
  this.playbackTickId = window.setInterval(() => {
225
229
  const elapsed = ctx.currentTime - this.playbackStartTime;
226
- this.position = Math.round((this.playbackOffset + elapsed) * 10) / 10;
230
+ this.position = this.roundPosition(this.playbackOffset + elapsed);
227
231
  if (elapsed >= effectiveDuration) {
228
232
  this.playbackOffset = maxDuration;
229
- this.position = Math.round(maxDuration * 10) / 10;
233
+ this.position = this.roundPosition(maxDuration);
230
234
  this.clearPlaybackTick();
231
235
  this.playState = "stopped";
232
236
  }
@@ -242,7 +246,7 @@ export class AudioEngine {
242
246
  this.stopSources(this.activePlaybackSources);
243
247
  }
244
248
  this.clearPlaybackTick();
245
- this.position = Math.round(this.playbackOffset * 10) / 10;
249
+ this.position = this.roundPosition(this.playbackOffset);
246
250
  this.playState = "paused";
247
251
  }
248
252
  /** Stops playback or recording. If recording, finalizes and merges the recorded audio. */
@@ -258,7 +262,7 @@ export class AudioEngine {
258
262
  this.stopSources(this.activePlaybackSources);
259
263
  }
260
264
  this.clearPlaybackTick();
261
- this.position = Math.round(this.playbackOffset * 10) / 10;
265
+ this.position = this.roundPosition(this.playbackOffset);
262
266
  }
263
267
  this.playState = "stopped";
264
268
  }
@@ -457,7 +461,7 @@ export class AudioEngine {
457
461
  this.recordingTrackIndex = trackIndex;
458
462
  this.recordingLatencySeconds = recordLatencySeconds;
459
463
  this.punchInOffset = this.playbackOffset;
460
- this.position = Math.round(this.punchInOffset * 10) / 10;
464
+ this.position = this.roundPosition(this.punchInOffset);
461
465
  this.playState = "recording";
462
466
  // Play other tracks for overdub monitoring, start meters and position timer
463
467
  this.playOtherTracksForMonitoring(trackIndex, this.punchInOffset);
@@ -60,11 +60,11 @@
60
60
  <div class="rotaters">
61
61
  <div
62
62
  class="rotater rot1"
63
- style:transform={"rotate(" + (time * 270) / 10 + "deg)"}
63
+ style:transform={"rotate(" + (time * 270 * -1) / 10 + "deg)"}
64
64
  ></div>
65
65
  <div
66
66
  class="rotater rot2"
67
- style:transform={"rotate(" + (time * 180) / 10 + "deg)"}
67
+ style:transform={"rotate(" + (time * 140 * -1) / 10 + "deg)"}
68
68
  ></div>
69
69
  </div>
70
70
  </div>
@@ -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 end
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,34 @@
1
1
  <script lang="ts">
2
- let { digit = 0 } = $props()
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 jumpTo(transform: string) {
11
- roller.style.transition = "none"
12
- roller.style.transform = transform
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
- 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
- }
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
- prev = d
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
- function onTransitionEnd() {
38
- if (!wrapping) return
39
- wrapping = false
40
- jumpTo(pos(+digit))
41
- }
25
+ const eased = divisor == 1 ? fraction : sigmoidEase(fraction, divisor == 10 ? 12 : 50, divisor == 10 ? 0.98 : 0.9995)
26
+ roller.style.transform = pos(digit + eased)
27
+ })
42
28
  </script>
43
29
 
44
30
  <div class="digits">
45
- <div class="roller" bind:this={roller} ontransitionend={onTransitionEnd}>
31
+ <div class="roller" bind:this={roller}>
46
32
  {#each [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0] as n}
47
33
  <div class="digit"><span>{n}</span></div>
48
34
  {/each}
@@ -69,9 +55,9 @@
69
55
  transform: translateY(-2cqw);
70
56
  }
71
57
  }
72
- .roller {
58
+ /* .roller {
73
59
  transition: 0.4s ease transform;
74
- }
60
+ } */
75
61
  .digit {
76
62
  font-size: 35cqh;
77
63
  /* letter-spacing: 7cqw; */
@@ -1,5 +1,6 @@
1
1
  declare const DigitRoller: import("svelte").Component<{
2
- digit?: number;
2
+ timestamp?: number;
3
+ index?: number;
3
4
  }, {}, "">;
4
5
  type DigitRoller = ReturnType<typeof DigitRoller>;
5
6
  export default DigitRoller;
@@ -0,0 +1,82 @@
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>
@@ -0,0 +1,5 @@
1
+ declare const DigitRollerFirst: import("svelte").Component<{
2
+ digit?: number;
3
+ }, {}, "">;
4
+ type DigitRollerFirst = ReturnType<typeof DigitRollerFirst>;
5
+ export default DigitRollerFirst;
@@ -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 '../../assets/counter_bg.png'
9
+ import counterBgImg from "../../assets/counter_bg.png"
9
10
 
10
- let correction = $state(0)
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
- if (cor_nr > 99) {
16
- return cor_nr.toString()
17
- } else if (cor_nr > 9) {
18
- return "0" + cor_nr.toString()
19
- } else {
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 = timestamp
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()}>&nbsp;</a>
39
30
  <div class="number-ticker">
40
- <DigitRoller digit={get_digit(timestamp, 0)} />
41
- <DigitRoller digit={get_digit(timestamp, 1)} />
42
- <DigitRoller digit={get_digit(timestamp, 2)} />
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4track",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "A 4-track cassette recorder component for Svelte 5",
5
5
  "license": "GPL-3.0-only",
6
6
  "author": "Andre Boekhorst",