@jjlmoya/utils-hardware 1.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/package.json +60 -0
- package/src/category/i18n/en.ts +107 -0
- package/src/category/i18n/es.ts +107 -0
- package/src/category/i18n/fr.ts +107 -0
- package/src/category/index.ts +18 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +26 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +23 -0
- package/src/tests/title_quality.test.ts +55 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/batteryHealthEstimator/bibliography.astro +14 -0
- package/src/tool/batteryHealthEstimator/component.astro +606 -0
- package/src/tool/batteryHealthEstimator/i18n/en.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/es.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/fr.ts +158 -0
- package/src/tool/batteryHealthEstimator/index.ts +34 -0
- package/src/tool/batteryHealthEstimator/seo.astro +14 -0
- package/src/tool/batteryHealthEstimator/ui.ts +31 -0
- package/src/tool/deadPixelTest/bibliography.astro +31 -0
- package/src/tool/deadPixelTest/component.astro +717 -0
- package/src/tool/deadPixelTest/i18n/en.ts +218 -0
- package/src/tool/deadPixelTest/i18n/es.ts +218 -0
- package/src/tool/deadPixelTest/i18n/fr.ts +218 -0
- package/src/tool/deadPixelTest/index.ts +34 -0
- package/src/tool/deadPixelTest/seo.astro +14 -0
- package/src/tool/deadPixelTest/ui.ts +20 -0
- package/src/tool/gamepadTest/bibliography.astro +30 -0
- package/src/tool/gamepadTest/component.astro +1419 -0
- package/src/tool/gamepadTest/i18n/en.ts +186 -0
- package/src/tool/gamepadTest/i18n/es.ts +186 -0
- package/src/tool/gamepadTest/i18n/fr.ts +186 -0
- package/src/tool/gamepadTest/index.ts +34 -0
- package/src/tool/gamepadTest/seo.astro +14 -0
- package/src/tool/gamepadTest/ui.ts +57 -0
- package/src/tool/gamepadVibrationTester/bibliography.astro +14 -0
- package/src/tool/gamepadVibrationTester/component.astro +694 -0
- package/src/tool/gamepadVibrationTester/i18n/en.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/es.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/fr.ts +150 -0
- package/src/tool/gamepadVibrationTester/index.ts +34 -0
- package/src/tool/gamepadVibrationTester/seo.astro +14 -0
- package/src/tool/gamepadVibrationTester/ui.ts +28 -0
- package/src/tool/keyboardTest/bibliography.astro +30 -0
- package/src/tool/keyboardTest/component.astro +601 -0
- package/src/tool/keyboardTest/i18n/en.ts +146 -0
- package/src/tool/keyboardTest/i18n/es.ts +146 -0
- package/src/tool/keyboardTest/i18n/fr.ts +146 -0
- package/src/tool/keyboardTest/index.ts +34 -0
- package/src/tool/keyboardTest/seo.astro +14 -0
- package/src/tool/keyboardTest/ui.ts +8 -0
- package/src/tool/mousePollingTest/bibliography.astro +14 -0
- package/src/tool/mousePollingTest/component.astro +237 -0
- package/src/tool/mousePollingTest/i18n/en.ts +135 -0
- package/src/tool/mousePollingTest/i18n/es.ts +135 -0
- package/src/tool/mousePollingTest/i18n/fr.ts +135 -0
- package/src/tool/mousePollingTest/index.ts +34 -0
- package/src/tool/mousePollingTest/logic/MovingAverage.ts +20 -0
- package/src/tool/mousePollingTest/logic/RatonManager.ts +97 -0
- package/src/tool/mousePollingTest/logic/StatisticalFilter.ts +48 -0
- package/src/tool/mousePollingTest/seo.astro +14 -0
- package/src/tool/mousePollingTest/ui.ts +8 -0
- package/src/tool/mousePollingTest/worker/mouseWorker.ts +50 -0
- package/src/tool/toneGenerator/bibliography.astro +14 -0
- package/src/tool/toneGenerator/component.astro +571 -0
- package/src/tool/toneGenerator/i18n/en.ts +146 -0
- package/src/tool/toneGenerator/i18n/es.ts +146 -0
- package/src/tool/toneGenerator/i18n/fr.ts +146 -0
- package/src/tool/toneGenerator/index.ts +34 -0
- package/src/tool/toneGenerator/seo.astro +14 -0
- package/src/tool/toneGenerator/ui.ts +19 -0
- package/src/tools.ts +10 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale } from '../../types';
|
|
3
|
+
import type { ToneGeneratorUI } from './ui';
|
|
4
|
+
import { Icon } from 'astro-icon/components';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
ui?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
const t = (ui ?? {}) as ToneGeneratorUI;
|
|
13
|
+
|
|
14
|
+
const S = JSON.stringify({ bp: t.btnPlay, bs: t.btnStop });
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div id="tg-root" class="tg-wrapper" data-s={S}>
|
|
18
|
+
|
|
19
|
+
<style is:global>
|
|
20
|
+
.tg-wrapper {
|
|
21
|
+
--tg-accent: #e11d48;
|
|
22
|
+
--tg-accent-glow: rgba(225, 29, 72, 0.3);
|
|
23
|
+
--tg-canvas-line: #f43f5e;
|
|
24
|
+
--tg-btn-bg: #e11d48;
|
|
25
|
+
--tg-btn-stop-bg: #1e293b;
|
|
26
|
+
--tg-surface: #fff;
|
|
27
|
+
--tg-surface-alt: #f8fafc;
|
|
28
|
+
--tg-border: #e2e8f0;
|
|
29
|
+
--tg-text: #0f172a;
|
|
30
|
+
--tg-text-muted: #64748b;
|
|
31
|
+
--tg-text-faint: #94a3b8;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.theme-dark .tg-wrapper {
|
|
35
|
+
--tg-btn-stop-bg: #27272a;
|
|
36
|
+
--tg-surface: #09090b;
|
|
37
|
+
--tg-surface-alt: #18181b;
|
|
38
|
+
--tg-border: #27272a;
|
|
39
|
+
--tg-text: #fafafa;
|
|
40
|
+
--tg-text-muted: #a1a1aa;
|
|
41
|
+
--tg-text-faint: #52525b;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.tg-wave-btn[data-active='true'] {
|
|
45
|
+
background: var(--tg-accent);
|
|
46
|
+
color: #fff;
|
|
47
|
+
box-shadow: 0 4px 14px var(--tg-accent-glow);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#tg-toggle[data-playing='true'] {
|
|
51
|
+
background: var(--tg-btn-stop-bg);
|
|
52
|
+
box-shadow: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#tg-play-icon {
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#tg-stop-icon {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#tg-toggle[data-playing='true'] #tg-play-icon {
|
|
64
|
+
display: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#tg-toggle[data-playing='true'] #tg-stop-icon {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@keyframes tg-pulse {
|
|
72
|
+
0%, 100% { opacity: 1; }
|
|
73
|
+
50% { opacity: 0.4; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
body.is-widget .tg-wrapper {
|
|
77
|
+
padding: 0;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
|
|
81
|
+
<div class="tg-card">
|
|
82
|
+
|
|
83
|
+
<div class="tg-header">
|
|
84
|
+
<div class="tg-icon-wrap">
|
|
85
|
+
<Icon name="mdi:sine-wave" class="tg-icon" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="tg-header-text">
|
|
88
|
+
<span class="tg-badge">{t.badge}</span>
|
|
89
|
+
<h2 class="tg-title">{t.title}</h2>
|
|
90
|
+
<p class="tg-desc">{t.description}</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="tg-canvas-wrap">
|
|
95
|
+
<canvas id="tg-canvas" class="tg-canvas"></canvas>
|
|
96
|
+
<div id="tg-hz-display" class="tg-hz-display">440 Hz</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="tg-controls">
|
|
100
|
+
|
|
101
|
+
<div class="tg-section">
|
|
102
|
+
<div class="tg-section-header">
|
|
103
|
+
<label class="tg-label">{t.freqLabel}</label>
|
|
104
|
+
<div class="tg-presets">
|
|
105
|
+
<button class="tg-preset" data-val="60">{t.preset60}</button>
|
|
106
|
+
<button class="tg-preset" data-val="440">{t.preset440}</button>
|
|
107
|
+
<button class="tg-preset" data-val="1000">{t.preset1k}</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<input type="range" id="tg-freq" class="tg-slider" min="20" max="20000" value="440" step="1" />
|
|
111
|
+
<div class="tg-range-hints">
|
|
112
|
+
<span>{t.rangeMin}</span>
|
|
113
|
+
<span>{t.rangeMax}</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="tg-two-col">
|
|
118
|
+
<div class="tg-section">
|
|
119
|
+
<label class="tg-label">{t.waveLabel}</label>
|
|
120
|
+
<div class="tg-wave-grid">
|
|
121
|
+
<button class="tg-wave-btn" data-type="sine" data-active="true" title={t.waveSine}>
|
|
122
|
+
<Icon name="mdi:sine-wave" class="tg-wave-icon" />
|
|
123
|
+
</button>
|
|
124
|
+
<button class="tg-wave-btn" data-type="square" title={t.waveSquare}>
|
|
125
|
+
<Icon name="mdi:square-wave" class="tg-wave-icon" />
|
|
126
|
+
</button>
|
|
127
|
+
<button class="tg-wave-btn" data-type="sawtooth" title={t.waveSawtooth}>
|
|
128
|
+
<Icon name="mdi:sawtooth-wave" class="tg-wave-icon" />
|
|
129
|
+
</button>
|
|
130
|
+
<button class="tg-wave-btn" data-type="triangle" title={t.waveTriangle}>
|
|
131
|
+
<Icon name="mdi:triangle-wave" class="tg-wave-icon" />
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="tg-section">
|
|
137
|
+
<label class="tg-label">{t.volLabel}</label>
|
|
138
|
+
<div class="tg-vol-row">
|
|
139
|
+
<Icon name="mdi:volume-high" class="tg-vol-icon" />
|
|
140
|
+
<input type="range" id="tg-vol" class="tg-slider tg-vol-slider" min="0" max="1" value="0.5" step="0.01" />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<button id="tg-toggle" class="tg-toggle-btn" data-playing="false">
|
|
146
|
+
<span id="tg-play-icon" class="tg-btn-icon">
|
|
147
|
+
<Icon name="mdi:play" class="tg-toggle-icon" />
|
|
148
|
+
</span>
|
|
149
|
+
<span id="tg-stop-icon" class="tg-btn-icon">
|
|
150
|
+
<Icon name="mdi:stop" class="tg-toggle-icon" />
|
|
151
|
+
</span>
|
|
152
|
+
<span id="tg-btn-text">{t.btnPlay}</span>
|
|
153
|
+
</button>
|
|
154
|
+
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<style>
|
|
159
|
+
.tg-wrapper {
|
|
160
|
+
width: 100%;
|
|
161
|
+
color: var(--tg-text);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.tg-card {
|
|
165
|
+
background: var(--tg-surface);
|
|
166
|
+
border: 1px solid var(--tg-border);
|
|
167
|
+
border-radius: 1.5rem;
|
|
168
|
+
padding: 2rem;
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
gap: 2rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.tg-header {
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 1rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.tg-icon-wrap {
|
|
181
|
+
flex-shrink: 0;
|
|
182
|
+
padding: 0.75rem;
|
|
183
|
+
background: rgba(225, 29, 72, 0.1);
|
|
184
|
+
color: var(--tg-accent);
|
|
185
|
+
border-radius: 1rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.tg-icon {
|
|
189
|
+
width: 2rem;
|
|
190
|
+
height: 2rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.tg-header-text {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
gap: 0.125rem;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.tg-badge {
|
|
200
|
+
display: inline-block;
|
|
201
|
+
font-size: 0.625rem;
|
|
202
|
+
font-weight: 900;
|
|
203
|
+
text-transform: uppercase;
|
|
204
|
+
letter-spacing: 0.12em;
|
|
205
|
+
color: var(--tg-accent);
|
|
206
|
+
background: rgba(225, 29, 72, 0.08);
|
|
207
|
+
border: 1px solid rgba(225, 29, 72, 0.2);
|
|
208
|
+
padding: 0.2rem 0.6rem;
|
|
209
|
+
border-radius: 9999px;
|
|
210
|
+
width: fit-content;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.tg-title {
|
|
214
|
+
font-size: 1.75rem;
|
|
215
|
+
font-weight: 900;
|
|
216
|
+
letter-spacing: -0.02em;
|
|
217
|
+
margin: 0;
|
|
218
|
+
color: var(--tg-text);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.tg-desc {
|
|
222
|
+
font-size: 0.875rem;
|
|
223
|
+
color: var(--tg-text-muted);
|
|
224
|
+
margin: 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.tg-canvas-wrap {
|
|
228
|
+
position: relative;
|
|
229
|
+
width: 100%;
|
|
230
|
+
height: 12rem;
|
|
231
|
+
background: #000;
|
|
232
|
+
border-radius: 1rem;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
border: 1px solid #111;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.tg-canvas {
|
|
238
|
+
width: 100%;
|
|
239
|
+
height: 100%;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.tg-hz-display {
|
|
243
|
+
position: absolute;
|
|
244
|
+
top: 1rem;
|
|
245
|
+
right: 1rem;
|
|
246
|
+
font-size: 0.75rem;
|
|
247
|
+
color: #f43f5e;
|
|
248
|
+
letter-spacing: 0.05em;
|
|
249
|
+
animation: tg-pulse 2s ease-in-out infinite;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.tg-controls {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
gap: 2rem;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.tg-section {
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
gap: 0.5rem;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.tg-section-header {
|
|
265
|
+
display: flex;
|
|
266
|
+
justify-content: space-between;
|
|
267
|
+
align-items: center;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.tg-label {
|
|
271
|
+
font-size: 0.7rem;
|
|
272
|
+
font-weight: 800;
|
|
273
|
+
text-transform: uppercase;
|
|
274
|
+
letter-spacing: 0.1em;
|
|
275
|
+
color: var(--tg-text-muted);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.tg-presets {
|
|
279
|
+
display: flex;
|
|
280
|
+
gap: 0.375rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.tg-preset {
|
|
284
|
+
padding: 0.25rem 0.5rem;
|
|
285
|
+
background: var(--tg-surface-alt);
|
|
286
|
+
border: 1px solid var(--tg-border);
|
|
287
|
+
border-radius: 0.375rem;
|
|
288
|
+
font-size: 0.75rem;
|
|
289
|
+
color: var(--tg-text-muted);
|
|
290
|
+
cursor: pointer;
|
|
291
|
+
transition: background 150ms, color 150ms;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.tg-preset:hover {
|
|
295
|
+
background: var(--tg-border);
|
|
296
|
+
color: var(--tg-text);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.tg-slider {
|
|
300
|
+
-webkit-appearance: none;
|
|
301
|
+
appearance: none;
|
|
302
|
+
width: 100%;
|
|
303
|
+
height: 6px;
|
|
304
|
+
background: var(--tg-border);
|
|
305
|
+
border-radius: 3px;
|
|
306
|
+
outline: none;
|
|
307
|
+
accent-color: var(--tg-accent);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.tg-slider::-webkit-slider-thumb {
|
|
311
|
+
-webkit-appearance: none;
|
|
312
|
+
appearance: none;
|
|
313
|
+
width: 18px;
|
|
314
|
+
height: 18px;
|
|
315
|
+
background: var(--tg-accent);
|
|
316
|
+
border-radius: 50%;
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
border: 4px solid var(--tg-surface);
|
|
319
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.tg-range-hints {
|
|
323
|
+
display: flex;
|
|
324
|
+
justify-content: space-between;
|
|
325
|
+
font-size: 0.625rem;
|
|
326
|
+
color: var(--tg-text-faint);
|
|
327
|
+
letter-spacing: 0.02em;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.tg-two-col {
|
|
331
|
+
display: grid;
|
|
332
|
+
grid-template-columns: 1fr 1fr;
|
|
333
|
+
gap: 2rem;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@media (max-width: 640px) {
|
|
337
|
+
.tg-two-col {
|
|
338
|
+
grid-template-columns: 1fr;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.tg-wave-grid {
|
|
343
|
+
display: grid;
|
|
344
|
+
grid-template-columns: repeat(4, 1fr);
|
|
345
|
+
gap: 0.5rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.tg-wave-btn {
|
|
349
|
+
padding: 0.75rem;
|
|
350
|
+
border-radius: 0.75rem;
|
|
351
|
+
background: var(--tg-surface-alt);
|
|
352
|
+
border: 1px solid var(--tg-border);
|
|
353
|
+
color: var(--tg-text-muted);
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
transition: background 150ms, color 150ms, box-shadow 150ms;
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
justify-content: center;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.tg-wave-btn:hover {
|
|
362
|
+
background: var(--tg-border);
|
|
363
|
+
color: var(--tg-text);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.tg-wave-icon {
|
|
367
|
+
width: 1.5rem;
|
|
368
|
+
height: 1.5rem;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.tg-vol-row {
|
|
372
|
+
display: flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
gap: 0.75rem;
|
|
375
|
+
background: var(--tg-surface-alt);
|
|
376
|
+
border: 1px solid var(--tg-border);
|
|
377
|
+
border-radius: 0.75rem;
|
|
378
|
+
padding: 0.75rem;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.tg-vol-icon {
|
|
382
|
+
width: 1.25rem;
|
|
383
|
+
height: 1.25rem;
|
|
384
|
+
color: var(--tg-text-faint);
|
|
385
|
+
flex-shrink: 0;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.tg-vol-slider {
|
|
389
|
+
height: 4px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.tg-toggle-btn {
|
|
393
|
+
width: 100%;
|
|
394
|
+
padding: 1.5rem;
|
|
395
|
+
border-radius: 1rem;
|
|
396
|
+
background: var(--tg-btn-bg);
|
|
397
|
+
border: none;
|
|
398
|
+
color: #fff;
|
|
399
|
+
font-size: 1.125rem;
|
|
400
|
+
font-weight: 900;
|
|
401
|
+
letter-spacing: 0.05em;
|
|
402
|
+
cursor: pointer;
|
|
403
|
+
display: flex;
|
|
404
|
+
align-items: center;
|
|
405
|
+
justify-content: center;
|
|
406
|
+
gap: 0.75rem;
|
|
407
|
+
box-shadow: 0 8px 24px var(--tg-accent-glow);
|
|
408
|
+
transition: background 200ms, box-shadow 200ms, transform 100ms;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.tg-toggle-btn:active {
|
|
412
|
+
transform: scale(0.98);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.tg-btn-icon {
|
|
416
|
+
display: inherit;
|
|
417
|
+
align-items: center;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.tg-toggle-icon {
|
|
421
|
+
width: 1.75rem;
|
|
422
|
+
height: 1.75rem;
|
|
423
|
+
}
|
|
424
|
+
</style>
|
|
425
|
+
|
|
426
|
+
<script>
|
|
427
|
+
const root = document.getElementById('tg-root');
|
|
428
|
+
const S: Record<string, string> = JSON.parse(root?.dataset?.s ?? '{}');
|
|
429
|
+
const BP = S.bp ?? 'PLAY TONE';
|
|
430
|
+
const BS = S.bs ?? 'STOP';
|
|
431
|
+
|
|
432
|
+
const canvas = document.getElementById('tg-canvas') as HTMLCanvasElement | null;
|
|
433
|
+
const canvasCtx = canvas?.getContext('2d') ?? null;
|
|
434
|
+
|
|
435
|
+
const freqSlider = document.getElementById('tg-freq') as HTMLInputElement | null;
|
|
436
|
+
const volSlider = document.getElementById('tg-vol') as HTMLInputElement | null;
|
|
437
|
+
const toggleBtn = document.getElementById('tg-toggle') as HTMLButtonElement | null;
|
|
438
|
+
const btnText = document.getElementById('tg-btn-text');
|
|
439
|
+
const hzDisplay = document.getElementById('tg-hz-display');
|
|
440
|
+
const waveBtns = document.querySelectorAll<HTMLButtonElement>('.tg-wave-btn');
|
|
441
|
+
const presets = document.querySelectorAll<HTMLButtonElement>('.tg-preset');
|
|
442
|
+
|
|
443
|
+
let audioCtx: AudioContext | null = null;
|
|
444
|
+
let oscillator: OscillatorNode | null = null;
|
|
445
|
+
let gainNode: GainNode | null = null;
|
|
446
|
+
let analyser: AnalyserNode | null = null;
|
|
447
|
+
let isPlaying = false;
|
|
448
|
+
let waveform: OscillatorType = 'sine';
|
|
449
|
+
|
|
450
|
+
function initAudio(): boolean {
|
|
451
|
+
if (audioCtx) return true;
|
|
452
|
+
const W = window as typeof window & { webkitAudioContext?: typeof AudioContext };
|
|
453
|
+
const Ctx = W.AudioContext ?? W.webkitAudioContext;
|
|
454
|
+
if (!Ctx) return false;
|
|
455
|
+
const ctx = new Ctx();
|
|
456
|
+
audioCtx = ctx;
|
|
457
|
+
analyser = ctx.createAnalyser();
|
|
458
|
+
analyser.fftSize = 2048;
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function updateUI(playing: boolean): void {
|
|
463
|
+
if (toggleBtn) toggleBtn.dataset.playing = playing ? 'true' : 'false';
|
|
464
|
+
if (btnText) btnText.textContent = playing ? BS : BP;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function resizeCanvas(): void {
|
|
468
|
+
if (!canvas) return;
|
|
469
|
+
const parent = canvas.parentElement;
|
|
470
|
+
if (!parent) return;
|
|
471
|
+
const rect = parent.getBoundingClientRect();
|
|
472
|
+
if (canvas.width !== rect.width) canvas.width = rect.width;
|
|
473
|
+
if (canvas.height !== rect.height) canvas.height = rect.height;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function drawWave(w: number, h: number, data: Uint8Array): void {
|
|
477
|
+
if (!canvasCtx) return;
|
|
478
|
+
canvasCtx.fillStyle = 'rgb(0,0,0)';
|
|
479
|
+
canvasCtx.fillRect(0, 0, w, h);
|
|
480
|
+
canvasCtx.lineWidth = 2;
|
|
481
|
+
canvasCtx.strokeStyle = '#f43f5e';
|
|
482
|
+
canvasCtx.beginPath();
|
|
483
|
+
const sliceWidth = w / data.length;
|
|
484
|
+
let x = 0;
|
|
485
|
+
for (let i = 0; i < data.length; i++) {
|
|
486
|
+
const v = (data[i] ?? 128) / 128.0;
|
|
487
|
+
const y = (v * h) / 2;
|
|
488
|
+
if (i === 0) canvasCtx.moveTo(x, y);
|
|
489
|
+
else canvasCtx.lineTo(x, y);
|
|
490
|
+
x += sliceWidth;
|
|
491
|
+
}
|
|
492
|
+
canvasCtx.lineTo(w, h / 2);
|
|
493
|
+
canvasCtx.stroke();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function drawVisualizer(): void {
|
|
497
|
+
if (!isPlaying || !analyser || !canvas) return;
|
|
498
|
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
499
|
+
analyser.getByteTimeDomainData(dataArray);
|
|
500
|
+
resizeCanvas();
|
|
501
|
+
drawWave(canvas.width, canvas.height, dataArray);
|
|
502
|
+
requestAnimationFrame(drawVisualizer);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function updateParams(): void {
|
|
506
|
+
if (oscillator && freqSlider) oscillator.frequency.value = parseInt(freqSlider.value, 10);
|
|
507
|
+
if (gainNode && volSlider) gainNode.gain.value = parseFloat(volSlider.value);
|
|
508
|
+
if (oscillator) oscillator.type = waveform;
|
|
509
|
+
if (hzDisplay && freqSlider) hzDisplay.textContent = `${freqSlider.value} Hz`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function start(): void {
|
|
513
|
+
if (!audioCtx || !analyser || !freqSlider || !volSlider) return;
|
|
514
|
+
oscillator = audioCtx.createOscillator();
|
|
515
|
+
gainNode = audioCtx.createGain();
|
|
516
|
+
oscillator.type = waveform;
|
|
517
|
+
oscillator.frequency.value = parseInt(freqSlider.value, 10);
|
|
518
|
+
gainNode.gain.value = parseFloat(volSlider.value);
|
|
519
|
+
oscillator.connect(gainNode);
|
|
520
|
+
gainNode.connect(analyser);
|
|
521
|
+
analyser.connect(audioCtx.destination);
|
|
522
|
+
oscillator.start();
|
|
523
|
+
isPlaying = true;
|
|
524
|
+
updateUI(true);
|
|
525
|
+
drawVisualizer();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function stop(): void {
|
|
529
|
+
if (oscillator) {
|
|
530
|
+
oscillator.stop();
|
|
531
|
+
oscillator.disconnect();
|
|
532
|
+
oscillator = null;
|
|
533
|
+
}
|
|
534
|
+
isPlaying = false;
|
|
535
|
+
updateUI(false);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function toggle(): void {
|
|
539
|
+
if (!initAudio()) return;
|
|
540
|
+
if (audioCtx?.state === 'suspended') audioCtx.resume();
|
|
541
|
+
if (isPlaying) stop();
|
|
542
|
+
else start();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
toggleBtn?.addEventListener('click', toggle);
|
|
546
|
+
freqSlider?.addEventListener('input', updateParams);
|
|
547
|
+
volSlider?.addEventListener('input', updateParams);
|
|
548
|
+
|
|
549
|
+
waveBtns.forEach((btn) => {
|
|
550
|
+
btn.addEventListener('click', () => {
|
|
551
|
+
const t = btn.getAttribute('data-type');
|
|
552
|
+
if (!t) return;
|
|
553
|
+
waveform = t as OscillatorType;
|
|
554
|
+
updateParams();
|
|
555
|
+
waveBtns.forEach((b) => { b.dataset.active = 'false'; });
|
|
556
|
+
btn.dataset.active = 'true';
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
presets.forEach((p) => {
|
|
561
|
+
p.addEventListener('click', () => {
|
|
562
|
+
const val = p.getAttribute('data-val');
|
|
563
|
+
if (val && freqSlider) {
|
|
564
|
+
freqSlider.value = val;
|
|
565
|
+
updateParams();
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
</script>
|
|
570
|
+
|
|
571
|
+
</div>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { ToneGeneratorUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const slug = 'tone-frequency-generator';
|
|
6
|
+
const title = 'Online Tone and Frequency Generator';
|
|
7
|
+
const description =
|
|
8
|
+
'Generate sine, square, triangle and sawtooth waves. Test your speakers, headphones or subwoofer with frequencies from 20Hz to 20kHz.';
|
|
9
|
+
|
|
10
|
+
const faqData = [
|
|
11
|
+
{
|
|
12
|
+
question: 'What is a frequency generator used for?',
|
|
13
|
+
answer:
|
|
14
|
+
'It is used to test the frequency response of speakers and headphones, detect unwanted resonances in furniture, find gaps in your hearing range, or even help calm tinnitus through notch therapy.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: 'What is a sine wave versus a square wave?',
|
|
18
|
+
answer:
|
|
19
|
+
'A sine wave is a pure tone with no harmonics (smooth and round). A square wave is rich in odd harmonics and sounds much more aggressive or digital. The triangle wave sits in between, useful for audio synthesis.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
question: 'Can I damage my speakers with this tool?',
|
|
23
|
+
answer:
|
|
24
|
+
'Yes, if you use very high volumes at extreme frequencies (especially bass below 30Hz or treble above 15kHz). Always start with a low system volume and increase gradually.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
question: 'What is the human hearing range?',
|
|
28
|
+
answer:
|
|
29
|
+
'Theoretically 20Hz to 20,000Hz (20kHz). However, with age it is normal to lose the ability to hear above 15kHz. This test can help you verify your personal upper limit.',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const howToData = [
|
|
34
|
+
{
|
|
35
|
+
name: 'Select the waveform type',
|
|
36
|
+
text: 'Choose between Sine (pure), Square, Triangle or Sawtooth depending on the type of test you want to perform.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Adjust the frequency',
|
|
40
|
+
text: 'Move the slider to navigate the audible spectrum. Use the 60Hz, 440Hz or 1kHz shortcuts to quickly access reference frequencies.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Control the volume',
|
|
44
|
+
text: 'Make sure your speaker volume is low before pressing Play. You can adjust the gain directly from the tool.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Analyse the response',
|
|
48
|
+
text: 'Listen for possible distortions or moments when the sound disappears. This will indicate the physical limits of your audio hardware.',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
53
|
+
'@context': 'https://schema.org',
|
|
54
|
+
'@type': 'FAQPage',
|
|
55
|
+
mainEntity: faqData.map((item) => ({
|
|
56
|
+
'@type': 'Question',
|
|
57
|
+
name: item.question,
|
|
58
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const howToSchema: WithContext<HowTo> = {
|
|
63
|
+
'@context': 'https://schema.org',
|
|
64
|
+
'@type': 'HowTo',
|
|
65
|
+
name: title,
|
|
66
|
+
description,
|
|
67
|
+
step: howToData.map((step, i) => ({
|
|
68
|
+
'@type': 'HowToStep',
|
|
69
|
+
position: i + 1,
|
|
70
|
+
name: step.name,
|
|
71
|
+
text: step.text,
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
76
|
+
'@context': 'https://schema.org',
|
|
77
|
+
'@type': 'SoftwareApplication',
|
|
78
|
+
name: title,
|
|
79
|
+
description,
|
|
80
|
+
applicationCategory: 'UtilityApplication',
|
|
81
|
+
operatingSystem: 'All',
|
|
82
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
83
|
+
inLanguage: 'en',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const content: ToolLocaleContent<ToneGeneratorUI> = {
|
|
87
|
+
slug,
|
|
88
|
+
title,
|
|
89
|
+
description,
|
|
90
|
+
faqTitle: 'Frequently Asked Questions',
|
|
91
|
+
faq: faqData,
|
|
92
|
+
bibliographyTitle: 'References',
|
|
93
|
+
bibliography: [
|
|
94
|
+
{
|
|
95
|
+
name: 'MDN Web Docs — Web Audio API',
|
|
96
|
+
url: 'https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'ISO 226:2023 — Equal-loudness contours',
|
|
100
|
+
url: 'https://www.iso.org/standard/83117.html',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
howTo: howToData,
|
|
104
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
105
|
+
seo: [
|
|
106
|
+
{ type: 'title', text: 'Everything About Frequencies and Sound Waves', level: 2 },
|
|
107
|
+
{
|
|
108
|
+
type: 'paragraph',
|
|
109
|
+
html: 'Sound is pure physics in motion. This tool lets you manipulate sound waves in real time, from the deepest bass you can feel in your chest to the ultrasonic highs only animals can perceive.',
|
|
110
|
+
},
|
|
111
|
+
{ type: 'title', text: 'Human Hearing Range and Presbycusis', level: 3 },
|
|
112
|
+
{
|
|
113
|
+
type: 'paragraph',
|
|
114
|
+
html: 'A healthy human ear perceives sounds between <strong>20Hz and 20kHz</strong>. With age, the upper limit drops: most adults over 50 cannot hear above 12kHz. The 17.4kHz tone, known as the "mosquito tone", is a classic test that only teenagers can typically pass.',
|
|
115
|
+
},
|
|
116
|
+
{ type: 'title', text: 'Wave Types and Their Uses', level: 3 },
|
|
117
|
+
{
|
|
118
|
+
type: 'paragraph',
|
|
119
|
+
html: '<strong>Sine:</strong> pure tone with no harmonics, used in medical hearing tests and instrument calibration. <strong>Square:</strong> rich in odd harmonics, sounds like retro 8-bit consoles. <strong>Sawtooth:</strong> contains all harmonics, the basis of electronic music synthesisers. <strong>Triangle:</strong> midpoint between sine and square — smoother than square but with more harmonic content than sine.',
|
|
120
|
+
},
|
|
121
|
+
{ type: 'title', text: 'Speaker Testing and Vibration Cleaning', level: 3 },
|
|
122
|
+
{
|
|
123
|
+
type: 'paragraph',
|
|
124
|
+
html: 'A low-frequency wave (typically <strong>165Hz</strong>) at maximum volume with a square or sawtooth shape forces the speaker diaphragm to vibrate violently, mechanically expelling water droplets trapped in the grille. Do not play extremely high frequencies at maximum volume for long periods: even if you cannot hear them, the energy can damage your equipment\'s tweeters.',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
ui: {
|
|
128
|
+
badge: 'Audio Generator',
|
|
129
|
+
title: 'Tone Generator',
|
|
130
|
+
description: 'Generate pure frequencies for audio testing.',
|
|
131
|
+
freqLabel: 'Frequency',
|
|
132
|
+
preset60: '60Hz',
|
|
133
|
+
preset440: '440Hz',
|
|
134
|
+
preset1k: '1kHz',
|
|
135
|
+
rangeMin: '20Hz (Sub-bass)',
|
|
136
|
+
rangeMax: '20kHz (Ultrasound)',
|
|
137
|
+
waveLabel: 'Waveform',
|
|
138
|
+
waveSine: 'Sine',
|
|
139
|
+
waveSquare: 'Square',
|
|
140
|
+
waveSawtooth: 'Sawtooth',
|
|
141
|
+
waveTriangle: 'Triangle',
|
|
142
|
+
volLabel: 'Volume',
|
|
143
|
+
btnPlay: 'PLAY TONE',
|
|
144
|
+
btnStop: 'STOP',
|
|
145
|
+
},
|
|
146
|
+
};
|