@jjlmoya/utils-hardware 1.22.0 → 1.24.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/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/mouseScrollTest/mouse-scroll-test.css +23 -0
- package/src/tool/stereoAudioTest/bibliography.astro +14 -0
- package/src/tool/stereoAudioTest/bibliography.ts +16 -0
- package/src/tool/stereoAudioTest/component.astro +251 -0
- package/src/tool/stereoAudioTest/entry.ts +29 -0
- package/src/tool/stereoAudioTest/i18n/de.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/en.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/es.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/fr.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/id.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/it.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/ja.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/ko.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/nl.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/pl.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/pt.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/ru.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/sv.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/tr.ts +229 -0
- package/src/tool/stereoAudioTest/i18n/zh.ts +229 -0
- package/src/tool/stereoAudioTest/index.ts +11 -0
- package/src/tool/stereoAudioTest/logic.ts +22 -0
- package/src/tool/stereoAudioTest/seo.astro +15 -0
- package/src/tool/stereoAudioTest/stereo-audio-test.css +411 -0
- package/src/tool/stereoAudioTest/ui.ts +24 -0
- package/src/tools.ts +2 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { refreshRateDetector } from '../tool/refreshRateDetector/index';
|
|
|
12
12
|
import { monitorGhostingTest } from '../tool/monitorGhostingTest/index';
|
|
13
13
|
import { spectrumCanvas } from '../tool/colorAccuracyTest/index';
|
|
14
14
|
import { upsRuntimeCalculator } from '../tool/upsRuntimeCalculator/index';
|
|
15
|
+
import { stereoAudioTest } from '../tool/stereoAudioTest/index';
|
|
15
16
|
|
|
16
17
|
export const hardwareCategory: HardwareCategoryEntry = {
|
|
17
18
|
icon: 'mdi:memory',
|
|
18
|
-
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, mouseScrollTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator],
|
|
19
|
+
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, mouseScrollTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator, stereoAudioTest],
|
|
19
20
|
i18n: {
|
|
20
21
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
21
22
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -24,6 +24,8 @@ export { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
|
24
24
|
export type { SpectrumCanvasLocaleContent } from './tool/colorAccuracyTest/entry';
|
|
25
25
|
export { upsRuntimeCalculator } from './tool/upsRuntimeCalculator/entry';
|
|
26
26
|
export type { UpsRuntimeCalculatorLocaleContent } from './tool/upsRuntimeCalculator/entry';
|
|
27
|
+
export { stereoAudioTest } from './tool/stereoAudioTest/entry';
|
|
28
|
+
export type { StereoAudioTestLocaleContent } from './tool/stereoAudioTest/entry';
|
|
27
29
|
export { hardwareCategory } from './category';
|
|
28
30
|
import { estimadorSaludBateria } from './tool/batteryHealthEstimator/entry';
|
|
29
31
|
import { pixelesPantalla } from './tool/deadPixelTest/entry';
|
|
@@ -38,4 +40,5 @@ import { refreshRateDetector } from './tool/refreshRateDetector/entry';
|
|
|
38
40
|
import { monitorGhostingTest } from './tool/monitorGhostingTest/entry';
|
|
39
41
|
import { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
40
42
|
import { upsRuntimeCalculator } from './tool/upsRuntimeCalculator/entry';
|
|
41
|
-
|
|
43
|
+
import { stereoAudioTest } from './tool/stereoAudioTest/entry';
|
|
44
|
+
export const ALL_ENTRIES = [estimadorSaludBateria, pixelesPantalla, testMando, probadorVibracionMando, testTeclado, testRaton, mouseDoubleClickTest, mouseScrollTest, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator, stereoAudioTest];
|
package/src/index.ts
CHANGED
|
@@ -30,3 +30,4 @@ export { REFRESH_RATE_DETECTOR_TOOL } from './tool/refreshRateDetector/index';
|
|
|
30
30
|
export { MONITOR_GHOSTING_TEST_TOOL } from './tool/monitorGhostingTest/index';
|
|
31
31
|
export { SPECTRUM_CANVAS_TOOL } from './tool/colorAccuracyTest/index';
|
|
32
32
|
export { UPS_RUNTIME_CALCULATOR_TOOL } from './tool/upsRuntimeCalculator/index';
|
|
33
|
+
export { STEREO_AUDIO_TEST_TOOL } from './tool/stereoAudioTest/index';
|
|
@@ -4,8 +4,8 @@ import { hardwareCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 14 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(14);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('hardwareCategory should be defined', () => {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
.mst-root {
|
|
2
|
+
box-sizing: border-box;
|
|
2
3
|
width: 100%;
|
|
4
|
+
max-width: 100%;
|
|
5
|
+
min-width: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.mst-root *,
|
|
9
|
+
.mst-root *::before,
|
|
10
|
+
.mst-root *::after {
|
|
11
|
+
box-sizing: inherit;
|
|
3
12
|
}
|
|
4
13
|
|
|
5
14
|
.mst-card {
|
|
@@ -7,6 +16,8 @@
|
|
|
7
16
|
grid-template-columns: minmax(0, 1.05fr) minmax(280px, 0.95fr);
|
|
8
17
|
gap: 1rem;
|
|
9
18
|
width: 100%;
|
|
19
|
+
max-width: 100%;
|
|
20
|
+
min-width: 0;
|
|
10
21
|
padding: clamp(0.9rem, 3vw, 1.25rem);
|
|
11
22
|
border: 1px solid rgba(21, 94, 117, 0.22);
|
|
12
23
|
border-radius: 8px;
|
|
@@ -29,6 +40,8 @@
|
|
|
29
40
|
.mst-capture {
|
|
30
41
|
position: relative;
|
|
31
42
|
display: grid;
|
|
43
|
+
width: 100%;
|
|
44
|
+
min-width: 0;
|
|
32
45
|
min-height: 430px;
|
|
33
46
|
overflow: hidden;
|
|
34
47
|
padding: clamp(1rem, 4vw, 1.5rem);
|
|
@@ -89,6 +102,7 @@
|
|
|
89
102
|
display: inline-flex;
|
|
90
103
|
align-items: center;
|
|
91
104
|
gap: 0.45rem;
|
|
105
|
+
max-width: 100%;
|
|
92
106
|
padding: 0.5rem 0.7rem;
|
|
93
107
|
border-radius: 999px;
|
|
94
108
|
font-size: 0.82rem;
|
|
@@ -109,6 +123,7 @@
|
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
.mst-lock-text {
|
|
126
|
+
width: 100%;
|
|
112
127
|
max-width: 28rem;
|
|
113
128
|
margin: 0;
|
|
114
129
|
color: #fff;
|
|
@@ -196,6 +211,7 @@
|
|
|
196
211
|
display: grid;
|
|
197
212
|
gap: 0.45rem;
|
|
198
213
|
max-width: 30rem;
|
|
214
|
+
min-width: 0;
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
@keyframes mst-locked-pulse {
|
|
@@ -250,7 +266,9 @@
|
|
|
250
266
|
}
|
|
251
267
|
|
|
252
268
|
.mst-capture-copy small {
|
|
269
|
+
display: block;
|
|
253
270
|
max-width: 24rem;
|
|
271
|
+
overflow-wrap: anywhere;
|
|
254
272
|
color: #0f172a;
|
|
255
273
|
font-size: clamp(1.1rem, 3.4vw, 1.35rem);
|
|
256
274
|
font-weight: 900;
|
|
@@ -310,6 +328,7 @@
|
|
|
310
328
|
display: grid;
|
|
311
329
|
gap: 0.85rem;
|
|
312
330
|
align-content: start;
|
|
331
|
+
min-width: 0;
|
|
313
332
|
}
|
|
314
333
|
|
|
315
334
|
.mst-score,
|
|
@@ -349,6 +368,7 @@
|
|
|
349
368
|
font-size: 0.94rem;
|
|
350
369
|
font-weight: 750;
|
|
351
370
|
line-height: 1.35;
|
|
371
|
+
overflow-wrap: anywhere;
|
|
352
372
|
}
|
|
353
373
|
|
|
354
374
|
.theme-dark .mst-direction-note {
|
|
@@ -495,10 +515,12 @@
|
|
|
495
515
|
align-items: center;
|
|
496
516
|
justify-content: space-between;
|
|
497
517
|
gap: 0.75rem;
|
|
518
|
+
min-width: 0;
|
|
498
519
|
font-weight: 800;
|
|
499
520
|
}
|
|
500
521
|
|
|
501
522
|
.mst-log-head button {
|
|
523
|
+
flex: 0 0 auto;
|
|
502
524
|
border: 0;
|
|
503
525
|
border-radius: 999px;
|
|
504
526
|
background: #0f766e;
|
|
@@ -534,6 +556,7 @@
|
|
|
534
556
|
border-radius: 8px;
|
|
535
557
|
background: rgba(236, 253, 245, 0.82);
|
|
536
558
|
font-size: 0.86rem;
|
|
559
|
+
overflow-wrap: anywhere;
|
|
537
560
|
}
|
|
538
561
|
|
|
539
562
|
.mst-log-item.reversal {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
import { stereoAudioTest } from './index';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await stereoAudioTest.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && content.bibliography.length > 0 && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Microsoft Support - Fix sound or audio problems in Windows',
|
|
6
|
+
url: 'https://support.microsoft.com/windows/fix-sound-or-audio-problems-in-windows-73025246-b61c-40fb-671a-2535c7cd56c8',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Apple Support - Adjust audio balance and mono audio on Mac',
|
|
10
|
+
url: 'https://support.apple.com/guide/mac-help/change-sound-output-settings-mchlp2256/mac',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Crutchfield - Speaker placement for stereo listening',
|
|
14
|
+
url: 'https://www.crutchfield.com/learn/home-stereo-speaker-placement.html',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
import type { StereoAudioTestUI } from './ui';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
ui?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
const t = (ui ?? {}) as StereoAudioTestUI;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class="sat-root"
|
|
17
|
+
data-config={JSON.stringify({
|
|
18
|
+
left: t.left,
|
|
19
|
+
center: t.center,
|
|
20
|
+
right: t.right,
|
|
21
|
+
sweep: t.sweep,
|
|
22
|
+
stop: t.stop,
|
|
23
|
+
activeIdle: t.activeIdle,
|
|
24
|
+
activeLeft: t.activeLeft,
|
|
25
|
+
activeCenter: t.activeCenter,
|
|
26
|
+
activeRight: t.activeRight,
|
|
27
|
+
activeSweep: t.activeSweep,
|
|
28
|
+
secondsUnit: t.secondsUnit,
|
|
29
|
+
})}
|
|
30
|
+
>
|
|
31
|
+
<section class="sat-panel">
|
|
32
|
+
<div class="sat-stage" aria-live="polite">
|
|
33
|
+
<div class="sat-speaker sat-left">
|
|
34
|
+
<span></span>
|
|
35
|
+
<strong>{t.leftSpeaker}</strong>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="sat-orbit" aria-hidden="true">
|
|
38
|
+
<i data-dot></i>
|
|
39
|
+
<b>{t.centerLine}</b>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="sat-speaker sat-right">
|
|
42
|
+
<span></span>
|
|
43
|
+
<strong>{t.rightSpeaker}</strong>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="sat-controls">
|
|
48
|
+
<div class="sat-buttons">
|
|
49
|
+
<button type="button" data-play="left"><Icon name="mdi:play" />{t.left}</button>
|
|
50
|
+
<button type="button" data-play="center"><Icon name="mdi:play" />{t.center}</button>
|
|
51
|
+
<button type="button" data-play="right"><Icon name="mdi:play" />{t.right}</button>
|
|
52
|
+
<button type="button" data-play="sweep"><Icon name="mdi:play" />{t.sweep}</button>
|
|
53
|
+
<button type="button" data-stop>{t.stop}</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="sat-sliders">
|
|
57
|
+
<label class="sat-control-row">
|
|
58
|
+
<span>{t.volume}</span>
|
|
59
|
+
<strong data-volume-label>24%</strong>
|
|
60
|
+
<input data-volume type="range" min="1" max="80" value="24" />
|
|
61
|
+
</label>
|
|
62
|
+
<label class="sat-control-row" data-duration-control>
|
|
63
|
+
<span>{t.duration}</span>
|
|
64
|
+
<strong><output data-duration-label>3</output>{t.secondsUnit}</strong>
|
|
65
|
+
<input data-duration type="range" min="1" max="8" value="3" />
|
|
66
|
+
</label>
|
|
67
|
+
<label class="sat-control-row">
|
|
68
|
+
<span>{t.tone}</span>
|
|
69
|
+
<strong><output data-frequency-label>520</output>{t.hertzUnit}</strong>
|
|
70
|
+
<input data-frequency type="range" min="220" max="1200" step="10" value="520" />
|
|
71
|
+
</label>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<label class="sat-loop">
|
|
75
|
+
<input data-loop type="checkbox" />
|
|
76
|
+
<span>
|
|
77
|
+
<strong>{t.infiniteMode}</strong>
|
|
78
|
+
<em>{t.infiniteModeHint}</em>
|
|
79
|
+
</span>
|
|
80
|
+
</label>
|
|
81
|
+
|
|
82
|
+
<div class="sat-readout">
|
|
83
|
+
<span>{t.balance}</span>
|
|
84
|
+
<strong data-status>{t.activeIdle}</strong>
|
|
85
|
+
<meter min="-1" max="1" low="-0.65" high="0.65" optimum="0" value="0" data-meter></meter>
|
|
86
|
+
</div>
|
|
87
|
+
<p>{t.safety}</p>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<link rel="stylesheet" href="./stereo-audio-test.css" />
|
|
93
|
+
|
|
94
|
+
<script>
|
|
95
|
+
type Channel = 'left' | 'center' | 'right' | 'sweep';
|
|
96
|
+
interface Config {
|
|
97
|
+
left: string;
|
|
98
|
+
center: string;
|
|
99
|
+
right: string;
|
|
100
|
+
sweep: string;
|
|
101
|
+
stop: string;
|
|
102
|
+
activeIdle: string;
|
|
103
|
+
activeLeft: string;
|
|
104
|
+
activeCenter: string;
|
|
105
|
+
activeRight: string;
|
|
106
|
+
activeSweep: string;
|
|
107
|
+
secondsUnit: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const root = document.querySelector<HTMLElement>('.sat-root');
|
|
111
|
+
const config = JSON.parse(root?.dataset.config ?? '{}') as Config;
|
|
112
|
+
const audioState: {
|
|
113
|
+
context?: AudioContext;
|
|
114
|
+
oscillator?: OscillatorNode;
|
|
115
|
+
gain?: GainNode;
|
|
116
|
+
stopTimer?: number;
|
|
117
|
+
sweepTimer?: number;
|
|
118
|
+
} = {};
|
|
119
|
+
const els = {
|
|
120
|
+
dot: root?.querySelector<HTMLElement>('[data-dot]'),
|
|
121
|
+
meter: root?.querySelector<HTMLMeterElement>('[data-meter]'),
|
|
122
|
+
status: root?.querySelector<HTMLElement>('[data-status]'),
|
|
123
|
+
volume: root?.querySelector<HTMLInputElement>('[data-volume]'),
|
|
124
|
+
volumeLabel: root?.querySelector<HTMLElement>('[data-volume-label]'),
|
|
125
|
+
duration: root?.querySelector<HTMLInputElement>('[data-duration]'),
|
|
126
|
+
durationControl: root?.querySelector<HTMLElement>('[data-duration-control]'),
|
|
127
|
+
durationLabel: root?.querySelector<HTMLOutputElement>('[data-duration-label]'),
|
|
128
|
+
frequency: root?.querySelector<HTMLInputElement>('[data-frequency]'),
|
|
129
|
+
frequencyLabel: root?.querySelector<HTMLOutputElement>('[data-frequency-label]'),
|
|
130
|
+
loop: root?.querySelector<HTMLInputElement>('[data-loop]'),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function ensureContext() {
|
|
134
|
+
audioState.context ??= new AudioContext();
|
|
135
|
+
return audioState.context;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function play(channel: Channel) {
|
|
139
|
+
stop();
|
|
140
|
+
const context = ensureContext();
|
|
141
|
+
const oscillator = context.createOscillator();
|
|
142
|
+
const gain = context.createGain();
|
|
143
|
+
const panner = context.createStereoPanner();
|
|
144
|
+
const duration = readNumber(els.duration, 3);
|
|
145
|
+
const volume = readNumber(els.volume, 24) / 100;
|
|
146
|
+
const frequency = readNumber(els.frequency, 520);
|
|
147
|
+
|
|
148
|
+
oscillator.type = 'sine';
|
|
149
|
+
oscillator.frequency.value = frequency;
|
|
150
|
+
gain.gain.setValueAtTime(0.0001, context.currentTime);
|
|
151
|
+
gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, volume), context.currentTime + 0.03);
|
|
152
|
+
oscillator.connect(gain).connect(panner).connect(context.destination);
|
|
153
|
+
oscillator.start();
|
|
154
|
+
audioState.oscillator = oscillator;
|
|
155
|
+
audioState.gain = gain;
|
|
156
|
+
|
|
157
|
+
const shouldLoop = Boolean(els.loop?.checked);
|
|
158
|
+
if (channel === 'sweep') {
|
|
159
|
+
runSweep(panner, duration);
|
|
160
|
+
} else {
|
|
161
|
+
setPan(panner, getPan(channel));
|
|
162
|
+
}
|
|
163
|
+
if (!shouldLoop) {
|
|
164
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + duration);
|
|
165
|
+
oscillator.stop(context.currentTime + duration + 0.05);
|
|
166
|
+
audioState.stopTimer = window.setTimeout(stop, (duration + 0.08) * 1000);
|
|
167
|
+
}
|
|
168
|
+
setStatus(channel);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function runSweep(panner: StereoPannerNode, duration: number) {
|
|
172
|
+
const start = performance.now();
|
|
173
|
+
const tick = (now: number) => {
|
|
174
|
+
const progress = ((now - start) % (duration * 1000)) / (duration * 1000);
|
|
175
|
+
const pan = Math.sin(progress * Math.PI * 2);
|
|
176
|
+
setPan(panner, pan);
|
|
177
|
+
audioState.sweepTimer = requestAnimationFrame(tick);
|
|
178
|
+
};
|
|
179
|
+
audioState.sweepTimer = requestAnimationFrame(tick);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setPan(panner: StereoPannerNode, pan: number) {
|
|
183
|
+
panner.pan.value = pan;
|
|
184
|
+
root?.style.setProperty('--sat-pan', String(pan));
|
|
185
|
+
els.meter!.value = pan;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getPan(channel: Channel) {
|
|
189
|
+
if (channel === 'left') return -1;
|
|
190
|
+
if (channel === 'right') return 1;
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function stop() {
|
|
195
|
+
if (audioState.stopTimer) window.clearTimeout(audioState.stopTimer);
|
|
196
|
+
if (audioState.sweepTimer) cancelAnimationFrame(audioState.sweepTimer);
|
|
197
|
+
if (audioState.oscillator) {
|
|
198
|
+
try {
|
|
199
|
+
audioState.oscillator.stop();
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
audioState.oscillator = undefined;
|
|
203
|
+
}
|
|
204
|
+
audioState.gain = undefined;
|
|
205
|
+
root?.style.setProperty('--sat-pan', '0');
|
|
206
|
+
if (els.meter) els.meter.value = 0;
|
|
207
|
+
if (els.status) els.status.textContent = config.activeIdle;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function setStatus(channel: Channel) {
|
|
211
|
+
const labels = {
|
|
212
|
+
left: config.activeLeft,
|
|
213
|
+
center: config.activeCenter,
|
|
214
|
+
right: config.activeRight,
|
|
215
|
+
sweep: config.activeSweep,
|
|
216
|
+
};
|
|
217
|
+
if (els.status) els.status.textContent = labels[channel];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readNumber(input: HTMLInputElement | null | undefined, fallback: number) {
|
|
221
|
+
return input ? Number(input.value) : fallback;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function syncLabels() {
|
|
225
|
+
if (els.volumeLabel && els.volume) els.volumeLabel.textContent = `${els.volume.value}%`;
|
|
226
|
+
if (els.durationLabel && els.duration) els.durationLabel.textContent = els.duration.value;
|
|
227
|
+
if (els.frequencyLabel && els.frequency) els.frequencyLabel.textContent = els.frequency.value;
|
|
228
|
+
syncLoopState();
|
|
229
|
+
updateActiveVolume();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function syncLoopState() {
|
|
233
|
+
const isLooping = Boolean(els.loop?.checked);
|
|
234
|
+
if (els.duration) els.duration.disabled = isLooping;
|
|
235
|
+
els.durationControl?.classList.toggle('is-disabled', isLooping);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function updateActiveVolume() {
|
|
239
|
+
if (!audioState.context || !audioState.gain || !els.volume) return;
|
|
240
|
+
const volume = Math.max(0.0001, Number(els.volume.value) / 100);
|
|
241
|
+
audioState.gain.gain.setTargetAtTime(volume, audioState.context.currentTime, 0.015);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
root?.querySelectorAll<HTMLButtonElement>('[data-play]').forEach((button) => {
|
|
245
|
+
button.addEventListener('click', () => play(button.dataset.play as Channel));
|
|
246
|
+
});
|
|
247
|
+
root?.querySelector<HTMLButtonElement>('[data-stop]')?.addEventListener('click', stop);
|
|
248
|
+
[els.volume, els.duration, els.frequency].forEach((input) => input?.addEventListener('input', syncLabels));
|
|
249
|
+
els.loop?.addEventListener('change', syncLabels);
|
|
250
|
+
syncLabels();
|
|
251
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HardwareToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { StereoAudioTestUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type StereoAudioTestLocaleContent = ToolLocaleContent<StereoAudioTestUI>;
|
|
5
|
+
|
|
6
|
+
export const stereoAudioTest: HardwareToolEntry<StereoAudioTestUI> = {
|
|
7
|
+
id: 'stereo-audio-test',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:speaker-multiple',
|
|
10
|
+
fg: 'mdi:surround-sound',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
14
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
15
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
16
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
17
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
18
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
19
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
20
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
21
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
22
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
23
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
24
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
25
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
26
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
27
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
28
|
+
},
|
|
29
|
+
};
|