@jjlmoya/utils-hardware 1.2.0 → 1.5.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 +2 -1
- package/src/tests/faq_count.test.ts +1 -1
- package/src/tests/i18n_coverage.test.ts +36 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/slug_uniqueness.test.ts +81 -0
- package/src/tests/title_quality.test.ts +1 -1
- package/src/tool/batteryHealthEstimator/i18n/de.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/id.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/it.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/ja.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/ko.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/nl.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/pl.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/pt.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/ru.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/sv.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/tr.ts +158 -0
- package/src/tool/batteryHealthEstimator/i18n/zh.ts +158 -0
- package/src/tool/batteryHealthEstimator/index.ts +15 -7
- package/src/tool/deadPixelTest/i18n/de.ts +218 -0
- package/src/tool/deadPixelTest/i18n/id.ts +218 -0
- package/src/tool/deadPixelTest/i18n/it.ts +218 -0
- package/src/tool/deadPixelTest/i18n/ja.ts +218 -0
- package/src/tool/deadPixelTest/i18n/ko.ts +218 -0
- package/src/tool/deadPixelTest/i18n/nl.ts +218 -0
- package/src/tool/deadPixelTest/i18n/pl.ts +218 -0
- package/src/tool/deadPixelTest/i18n/pt.ts +218 -0
- package/src/tool/deadPixelTest/i18n/ru.ts +218 -0
- package/src/tool/deadPixelTest/i18n/sv.ts +218 -0
- package/src/tool/deadPixelTest/i18n/tr.ts +218 -0
- package/src/tool/deadPixelTest/i18n/zh.ts +217 -0
- package/src/tool/deadPixelTest/index.ts +15 -7
- package/src/tool/gamepadTest/i18n/de.ts +186 -0
- package/src/tool/gamepadTest/i18n/id.ts +186 -0
- package/src/tool/gamepadTest/i18n/it.ts +186 -0
- package/src/tool/gamepadTest/i18n/ja.ts +186 -0
- package/src/tool/gamepadTest/i18n/ko.ts +186 -0
- package/src/tool/gamepadTest/i18n/nl.ts +186 -0
- package/src/tool/gamepadTest/i18n/pl.ts +186 -0
- package/src/tool/gamepadTest/i18n/pt.ts +186 -0
- package/src/tool/gamepadTest/i18n/ru.ts +186 -0
- package/src/tool/gamepadTest/i18n/sv.ts +186 -0
- package/src/tool/gamepadTest/i18n/tr.ts +186 -0
- package/src/tool/gamepadTest/i18n/zh.ts +186 -0
- package/src/tool/gamepadTest/index.ts +15 -7
- package/src/tool/gamepadVibrationTester/i18n/de.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/id.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/it.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/ja.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/ko.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/nl.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/pl.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/pt.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/ru.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/sv.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/tr.ts +155 -0
- package/src/tool/gamepadVibrationTester/i18n/zh.ts +155 -0
- package/src/tool/gamepadVibrationTester/index.ts +15 -7
- package/src/tool/keyboardTest/i18n/de.ts +146 -0
- package/src/tool/keyboardTest/i18n/id.ts +146 -0
- package/src/tool/keyboardTest/i18n/it.ts +146 -0
- package/src/tool/keyboardTest/i18n/ja.ts +146 -0
- package/src/tool/keyboardTest/i18n/ko.ts +146 -0
- package/src/tool/keyboardTest/i18n/nl.ts +146 -0
- package/src/tool/keyboardTest/i18n/pl.ts +146 -0
- package/src/tool/keyboardTest/i18n/pt.ts +146 -0
- package/src/tool/keyboardTest/i18n/ru.ts +146 -0
- package/src/tool/keyboardTest/i18n/sv.ts +146 -0
- package/src/tool/keyboardTest/i18n/tr.ts +146 -0
- package/src/tool/keyboardTest/i18n/zh.ts +146 -0
- package/src/tool/keyboardTest/index.ts +15 -7
- package/src/tool/mousePollingTest/component.astro +1 -1
- package/src/tool/mousePollingTest/i18n/de.ts +135 -0
- package/src/tool/mousePollingTest/i18n/id.ts +135 -0
- package/src/tool/mousePollingTest/i18n/it.ts +135 -0
- package/src/tool/mousePollingTest/i18n/ja.ts +135 -0
- package/src/tool/mousePollingTest/i18n/ko.ts +135 -0
- package/src/tool/mousePollingTest/i18n/nl.ts +135 -0
- package/src/tool/mousePollingTest/i18n/pl.ts +135 -0
- package/src/tool/mousePollingTest/i18n/pt.ts +135 -0
- package/src/tool/mousePollingTest/i18n/ru.ts +135 -0
- package/src/tool/mousePollingTest/i18n/sv.ts +135 -0
- package/src/tool/mousePollingTest/i18n/tr.ts +135 -0
- package/src/tool/mousePollingTest/i18n/zh.ts +135 -0
- package/src/tool/mousePollingTest/index.ts +15 -7
- package/src/tool/mousePollingTest/logic/RatonManager.ts +90 -51
- package/src/tool/mousePollingTest/logic/StatisticalFilter.ts +17 -25
- package/src/tool/toneGenerator/i18n/de.ts +146 -0
- package/src/tool/toneGenerator/i18n/id.ts +146 -0
- package/src/tool/toneGenerator/i18n/it.ts +146 -0
- package/src/tool/toneGenerator/i18n/ja.ts +146 -0
- package/src/tool/toneGenerator/i18n/ko.ts +146 -0
- package/src/tool/toneGenerator/i18n/nl.ts +146 -0
- package/src/tool/toneGenerator/i18n/pl.ts +146 -0
- package/src/tool/toneGenerator/i18n/pt.ts +146 -0
- package/src/tool/toneGenerator/i18n/ru.ts +146 -0
- package/src/tool/toneGenerator/i18n/sv.ts +146 -0
- package/src/tool/toneGenerator/i18n/tr.ts +146 -0
- package/src/tool/toneGenerator/i18n/zh.ts +146 -0
- package/src/tool/toneGenerator/index.ts +15 -7
- package/src/types.ts +1 -1
- package/src/tool/mousePollingTest/worker/mouseWorker.ts +0 -50
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { TestRatonUI } from '../ui';
|
|
4
|
+
|
|
5
|
+
const slug = 'mouse-polling-rate-test';
|
|
6
|
+
const title = '在线鼠标回报率测试';
|
|
7
|
+
const description =
|
|
8
|
+
'检查鼠标的真实 Hz。使用我们的专业工具验证您的游戏鼠标是否在 1000Hz 或更高频率下工作。';
|
|
9
|
+
|
|
10
|
+
const faqData = [
|
|
11
|
+
{
|
|
12
|
+
question: '什么是鼠标回报率 (Polling Rate)?',
|
|
13
|
+
answer:
|
|
14
|
+
'它是鼠标向电脑报告其位置的频率,以 Hz 为单位衡量。1000Hz 的回报率意味着鼠标每 1 毫秒发送一次数据,从而提供更平滑的移动。',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: '为什么我的测试不能持续达到 1000Hz?',
|
|
18
|
+
answer:
|
|
19
|
+
'浏览器只有在鼠标移动时才能测量回报率。如果您移动缓慢或 CPU 非常繁忙,测量值可能会波动。请快速转圈移动鼠标以获得真实的最高峰值。',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
question: '回报率是否越高越好?',
|
|
23
|
+
answer:
|
|
24
|
+
'对于游戏(1000Hz 或更高)来说,通常是的,因为它可以减少延迟。然而,极高的速率(4000Hz 或 8000Hz)会消耗大量 CPU 资源,并非所有游戏都针对这些速率进行了优化。',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
question: '显示器刷新率如何影响鼠标?',
|
|
28
|
+
answer:
|
|
29
|
+
'如果您使用 144Hz 或 240Hz 显示器,较低的回报率(125Hz)会使指针看起来有顿挫感。对于高刷新率显示器,至少使用 500Hz 或 1000Hz 是至关重要的。',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const howToData = [
|
|
34
|
+
{
|
|
35
|
+
name: '准备测试区域',
|
|
36
|
+
text: '将光标置于工具的采集区域内。',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: '快速移动鼠标',
|
|
40
|
+
text: '进行快速、大范围的圆周运动。该工具将计算硬件发送的每个 mousemove 事件之间的时间间隔。',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: '观察稳定性图表',
|
|
44
|
+
text: '检查 Hz 曲线是否平稳或是否存在突然下降,这可能表明无线鼠标受到干扰或 CPU 存在问题。',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: '分析响应时间',
|
|
48
|
+
text: '检查以毫秒为单位的平均延迟(Delay)。一只好的游戏鼠标平均延迟应保持在 1ms 左右。',
|
|
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: 'zh',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const content: ToolLocaleContent<TestRatonUI> = {
|
|
87
|
+
slug,
|
|
88
|
+
title,
|
|
89
|
+
description,
|
|
90
|
+
faqTitle: '常见问题',
|
|
91
|
+
faq: faqData,
|
|
92
|
+
bibliographyTitle: '参考资料',
|
|
93
|
+
bibliography: [
|
|
94
|
+
{
|
|
95
|
+
name: 'Gamepad Polling Rate — Logitech',
|
|
96
|
+
url: 'https://www.logitechg.com/en-us/innovation/delta-zero.html',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'USB HID Polling Rate — USB Implementers Forum',
|
|
100
|
+
url: 'https://www.usb.org/hid',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
howTo: howToData,
|
|
104
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
105
|
+
seo: [
|
|
106
|
+
{ type: 'title', text: '鼠标回报率权威指南', level: 2 },
|
|
107
|
+
{
|
|
108
|
+
type: 'paragraph',
|
|
109
|
+
html: '当您在鼠标垫上物理移动鼠标时,这种模拟运动必须转换为电脑理解的数字信号。<strong>回报率 (Polling Rate)</strong> 是鼠标向电脑“报告”其位置的频率。它以赫兹 (Hz) 为单位。',
|
|
110
|
+
},
|
|
111
|
+
{ type: 'title', text: '回报率等级及其用途', level: 3 },
|
|
112
|
+
{
|
|
113
|
+
type: 'paragraph',
|
|
114
|
+
html: '<strong>125 Hz</strong> — 鼠标每 8 毫秒报告一次。适用于办公,但在 144Hz 显示器上会感到卡顿。 <strong>1000 Hz</strong> — 游戏黄金标准:每 1 毫秒报告一次。平滑移动,无感知延迟。 <strong>8000 Hz</strong> — 尖端技术(Razer, Logitech)。每 0.125 毫秒报告一次,但需要强劲的 CPU。',
|
|
115
|
+
},
|
|
116
|
+
{ type: 'title', text: '为什么我的 Hz 值会波动?', level: 3 },
|
|
117
|
+
{
|
|
118
|
+
type: 'paragraph',
|
|
119
|
+
html: '完全正常。大多数现代鼠标都有动态回报率以节省电量。如果您移动鼠标缓慢,它发送的报告就会减少,因为新数据较少。只有快速、持续的移动才能使传感器达到其真实的最高峰值。',
|
|
120
|
+
},
|
|
121
|
+
{ type: 'title', text: '回报率 vs DPI:核心区别', level: 3 },
|
|
122
|
+
{
|
|
123
|
+
type: 'paragraph',
|
|
124
|
+
html: '<strong>DPI (Dots Per Inch)</strong> 是灵敏度:即鼠标每移动一英寸,光标移动多少像素。 <strong>Hz (回报率)</strong> 是更新频率:即该移动报告的平滑度和及时性。这两个参数是相互独立且互补的。',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
ui: {
|
|
128
|
+
badge: '鼠标测试',
|
|
129
|
+
title: '回报率检测器',
|
|
130
|
+
description: '快速圆周移动鼠标以测量 Hz。',
|
|
131
|
+
labelAvg: '平均值',
|
|
132
|
+
labelPeak: '峰值',
|
|
133
|
+
placeholder: '在此移动鼠标',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -7,10 +7,6 @@ import TestRatonBibliography from './bibliography.astro';
|
|
|
7
7
|
import type { TestRatonUI } from './ui';
|
|
8
8
|
export type TestRatonLocaleContent = ToolLocaleContent<TestRatonUI>;
|
|
9
9
|
|
|
10
|
-
import { content as es } from './i18n/es';
|
|
11
|
-
import { content as en } from './i18n/en';
|
|
12
|
-
import { content as fr } from './i18n/fr';
|
|
13
|
-
|
|
14
10
|
export const testRaton: HardwareToolEntry<TestRatonUI> = {
|
|
15
11
|
id: 'test-raton',
|
|
16
12
|
icons: {
|
|
@@ -18,9 +14,21 @@ export const testRaton: HardwareToolEntry<TestRatonUI> = {
|
|
|
18
14
|
fg: 'mdi:mouse',
|
|
19
15
|
},
|
|
20
16
|
i18n: {
|
|
21
|
-
|
|
22
|
-
en:
|
|
23
|
-
|
|
17
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
18
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
19
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
20
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
21
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
22
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
23
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
24
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
25
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
26
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
27
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
28
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
29
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
30
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
31
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
24
32
|
},
|
|
25
33
|
};
|
|
26
34
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { MovingAverage } from './MovingAverage';
|
|
2
|
+
import { StatisticalFilter } from './StatisticalFilter';
|
|
3
|
+
|
|
1
4
|
export interface UIToolElements {
|
|
2
5
|
avg: HTMLElement | null;
|
|
3
6
|
max: HTMLElement | null;
|
|
@@ -10,88 +13,124 @@ export interface UIToolElements {
|
|
|
10
13
|
export class RatonManager {
|
|
11
14
|
private el: UIToolElements;
|
|
12
15
|
private ctx: CanvasRenderingContext2D | null;
|
|
13
|
-
private worker: Worker;
|
|
14
16
|
private history: number[] = [];
|
|
15
|
-
private readonly MAX_HISTORY = 100;
|
|
17
|
+
private readonly MAX_HISTORY: number = 100;
|
|
16
18
|
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
19
|
private lastMouseEvent: MouseEvent | null = null;
|
|
20
|
+
private lastTime: number = 0;
|
|
21
|
+
private maxHz: number = 0;
|
|
22
|
+
private totalHistory: number[] = [];
|
|
23
|
+
private filter: StatisticalFilter = new StatisticalFilter();
|
|
24
|
+
private smoother: MovingAverage = new MovingAverage(20);
|
|
18
25
|
|
|
19
|
-
constructor(el: UIToolElements
|
|
26
|
+
constructor(el: UIToolElements) {
|
|
20
27
|
this.el = el;
|
|
21
28
|
this.ctx = el.canvas?.getContext('2d') ?? null;
|
|
22
|
-
this.worker = new Worker(workerUrl, { type: 'module' });
|
|
23
29
|
this.init();
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
private init(): void {
|
|
27
33
|
window.addEventListener('resize', () => this.resize());
|
|
28
34
|
this.resize();
|
|
29
|
-
|
|
30
|
-
this.worker.onmessage = (event) => this.handleWorkerMessage(event.data);
|
|
31
|
-
|
|
32
|
-
this.el.area?.addEventListener('mousemove', (e) => this.processMove(e));
|
|
35
|
+
this.el.area?.addEventListener('mousemove', (e: MouseEvent) => this.processMove(e));
|
|
33
36
|
this.el.area?.addEventListener('mouseleave', () => this.handleMouseLeave());
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
private resize(): void {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
const { area, canvas } = this.el;
|
|
41
|
+
if (!area || !canvas) return;
|
|
42
|
+
const rect = area.getBoundingClientRect();
|
|
43
|
+
canvas.width = rect.width;
|
|
44
|
+
canvas.height = rect.height;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
+
private processMove(e: MouseEvent): void {
|
|
48
|
+
this.lastMouseEvent = e;
|
|
49
|
+
const now: number = performance.now();
|
|
50
|
+
this.calculateHz(now);
|
|
51
|
+
this.lastTime = now;
|
|
52
|
+
this.refreshIdleTimer();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private calculateHz(now: number): void {
|
|
56
|
+
if (this.lastTime <= 0) return;
|
|
57
|
+
const delta: number = now - this.lastTime;
|
|
58
|
+
if (delta <= 0.5 || delta >= 200) return;
|
|
59
|
+
|
|
60
|
+
const hz: number = 1000 / delta;
|
|
61
|
+
if (this.filter.isOutlier(hz)) return;
|
|
62
|
+
|
|
63
|
+
this.processValidMove(hz);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private processValidMove(hz: number): void {
|
|
67
|
+
const smoothed: number = this.smoother.next(hz);
|
|
68
|
+
this.history.push(smoothed);
|
|
47
69
|
if (this.history.length > this.MAX_HISTORY) this.history.shift();
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
if (this.
|
|
71
|
+
this.totalHistory.push(hz);
|
|
72
|
+
if (this.totalHistory.length > 200) this.totalHistory.shift();
|
|
73
|
+
|
|
74
|
+
if (hz > this.maxHz) this.maxHz = hz;
|
|
75
|
+
const avg: number = this.totalHistory.reduce((acc: number, val: number) => acc + val, 0) / this.totalHistory.length;
|
|
51
76
|
|
|
77
|
+
this.updateUI(hz, avg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private refreshIdleTimer(): void {
|
|
81
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
82
|
+
this.idleTimer = setTimeout(() => this.reset(), 100);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private updateUI(hz: number, avg: number): void {
|
|
86
|
+
const { max, avg: avgEl, placeholder } = this.el;
|
|
87
|
+
if (max) max.textContent = Math.round(this.maxHz).toString();
|
|
88
|
+
if (avgEl) avgEl.textContent = Math.round(avg).toString();
|
|
89
|
+
if (placeholder) placeholder.style.opacity = '0';
|
|
90
|
+
this.updateFollower(hz);
|
|
52
91
|
this.updateGraph();
|
|
92
|
+
}
|
|
53
93
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
private updateFollower(hz: number): void {
|
|
95
|
+
const { follower, area } = this.el;
|
|
96
|
+
if (!follower || !this.lastMouseEvent || !area) return;
|
|
97
|
+
follower.textContent = `${Math.round(hz)} Hz`;
|
|
98
|
+
const rect = area.getBoundingClientRect();
|
|
99
|
+
const x: number = this.lastMouseEvent.clientX - rect.left + 15;
|
|
100
|
+
const y: number = this.lastMouseEvent.clientY - rect.top + 15;
|
|
101
|
+
follower.style.transform = `translate(${x}px, ${y}px)`;
|
|
102
|
+
follower.style.opacity = '1';
|
|
62
103
|
}
|
|
63
104
|
|
|
64
105
|
private updateGraph(): void {
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
const ctx = this.ctx;
|
|
107
|
+
const canvas = this.el.canvas;
|
|
108
|
+
if (!ctx || !canvas) return;
|
|
109
|
+
const { width: w, height: h } = canvas;
|
|
110
|
+
ctx.clearRect(0, 0, w, h);
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
ctx.strokeStyle = '#10b981';
|
|
113
|
+
ctx.lineWidth = 2;
|
|
114
|
+
const currentMax: number = Math.max(1000, ...this.history);
|
|
115
|
+
const stepX: number = w / this.MAX_HISTORY;
|
|
116
|
+
this.history.forEach((hz: number, i: number) => {
|
|
117
|
+
const x: number = i * stepX;
|
|
118
|
+
const y: number = h - (hz / currentMax) * (h * 0.8);
|
|
119
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
120
|
+
else ctx.lineTo(x, y);
|
|
78
121
|
});
|
|
79
|
-
|
|
122
|
+
ctx.stroke();
|
|
80
123
|
}
|
|
81
124
|
|
|
82
|
-
private
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
if (this.el.follower) this.el.follower.style.opacity = '0';
|
|
89
|
-
this.worker.postMessage('reset');
|
|
90
|
-
}, 100);
|
|
125
|
+
private reset(): void {
|
|
126
|
+
this.lastTime = 0;
|
|
127
|
+
this.filter.reset();
|
|
128
|
+
this.smoother.reset();
|
|
129
|
+
this.totalHistory = [];
|
|
130
|
+
if (this.el.follower) this.el.follower.style.opacity = '0';
|
|
91
131
|
}
|
|
92
132
|
|
|
93
133
|
private handleMouseLeave(): void {
|
|
94
|
-
|
|
95
|
-
this.worker.postMessage('reset');
|
|
134
|
+
this.reset();
|
|
96
135
|
}
|
|
97
136
|
}
|
|
@@ -1,38 +1,30 @@
|
|
|
1
|
-
export interface FilterConfig {
|
|
2
|
-
maxThreshold?: number;
|
|
3
|
-
stdDevMultiplier?: number;
|
|
4
|
-
minSamples?: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
1
|
export class StatisticalFilter {
|
|
8
2
|
private samples: number[] = [];
|
|
9
|
-
private readonly
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.config = {
|
|
13
|
-
maxThreshold: 1100,
|
|
14
|
-
stdDevMultiplier: 2,
|
|
15
|
-
minSamples: 20,
|
|
16
|
-
...config,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
3
|
+
private readonly MAX_THRESHOLD: number = 10000;
|
|
4
|
+
private readonly STD_DEV_MULTIPLIER: number = 4;
|
|
5
|
+
private readonly MIN_SAMPLES: number = 20;
|
|
19
6
|
|
|
20
7
|
public isOutlier(value: number): boolean {
|
|
21
|
-
if (
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
8
|
+
if (value > this.MAX_THRESHOLD) return true;
|
|
24
9
|
|
|
25
|
-
if (this.samples.length <
|
|
10
|
+
if (this.samples.length < this.MIN_SAMPLES) {
|
|
26
11
|
this.samples.push(value);
|
|
27
12
|
return false;
|
|
28
13
|
}
|
|
29
14
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
)
|
|
15
|
+
const total: number = this.samples.reduce((acc: number, val: number) => acc + val, 0);
|
|
16
|
+
const avg: number = total / this.samples.length;
|
|
17
|
+
|
|
18
|
+
const squareDiffs: number = this.samples.reduce((acc: number, val: number) => {
|
|
19
|
+
const diff: number = val - avg;
|
|
20
|
+
return acc + (diff * diff);
|
|
21
|
+
}, 0);
|
|
22
|
+
|
|
23
|
+
const stdDev: number = Math.sqrt(squareDiffs / this.samples.length);
|
|
24
|
+
|
|
25
|
+
if (stdDev === 0) return false;
|
|
34
26
|
|
|
35
|
-
const isFiltered = Math.abs(value - avg) > stdDev *
|
|
27
|
+
const isFiltered: boolean = Math.abs(value - avg) > stdDev * this.STD_DEV_MULTIPLIER;
|
|
36
28
|
|
|
37
29
|
if (!isFiltered) {
|
|
38
30
|
this.samples.push(value);
|
|
@@ -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 = 'ton-frequenz-generator-online';
|
|
6
|
+
const title = 'Online Ton und Frequenzgenerator';
|
|
7
|
+
const description =
|
|
8
|
+
'Erzeugen Sie Sinus-, Rechteck-, Dreieck- und Sägezahnwellen. Testen Sie Ihre Lautsprecher, Kopfhörer oder Subwoofer mit Frequenzen von 20Hz bis 20kHz.';
|
|
9
|
+
|
|
10
|
+
const faqData = [
|
|
11
|
+
{
|
|
12
|
+
question: 'Wofür wird ein Frequenzgenerator verwendet?',
|
|
13
|
+
answer:
|
|
14
|
+
'Er wird verwendet, um den Frequenzgang von Lautsprechern und Kopfhörern zu testen, unerwünschte Resonanzen in Möbeln zu erkennen, Lücken in Ihrem Hörbereich zu finden oder sogar bei Tinnitus durch Notch-Therapie zu helfen.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: 'Was ist eine Sinuswelle im Vergleich zu einer Rechteckwelle?',
|
|
18
|
+
answer:
|
|
19
|
+
'Eine Sinuswelle ist ein reiner Ton ohne Oberwellen (glatt und rund). Eine Rechteckwelle ist reich an ungeraden Oberwellen und klingt viel aggressiver oder digitaler. Die Dreieckwelle liegt dazwischen und ist nützlich für die Audiosynthese.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
question: 'Kann ich meine Lautsprecher mit diesem Tool beschädigen?',
|
|
23
|
+
answer:
|
|
24
|
+
'Ja, wenn Sie sehr hohe Lautstärken bei extremen Frequenzen verwenden (insbesondere Bässe unter 30Hz oder Höhen über 15kHz). Beginnen Sie immer mit einer niedrigen Systemlautstärke und erhöhen Sie diese schrittweise.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
question: 'Wie hoch ist der menschliche Hörbereich?',
|
|
28
|
+
answer:
|
|
29
|
+
'Theoretisch 20Hz bis 20.000Hz (20kHz). Mit zunehmendem Alter ist es jedoch normal, die Fähigkeit zu verlieren, Töne über 15kHz zu hören. Dieser Test kann Ihnen helfen, Ihre persönliche Obergrenze zu überprüfen.',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const howToData = [
|
|
34
|
+
{
|
|
35
|
+
name: 'Wellenformtyp auswählen',
|
|
36
|
+
text: 'Wählen Sie zwischen Sinus (rein), Rechteck, Dreieck oder Sägezahn, je nachdem, welche Art von Test Sie durchführen möchten.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Frequenz einstellen',
|
|
40
|
+
text: 'Bewegen Sie den Schieberegler, um durch das hörbare Spektrum zu navigieren. Verwenden Sie die Shortcuts für 60Hz, 440Hz oder 1kHz, um schnell auf Referenzfrequenzen zuzugreifen.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Lautstärke kontrollieren',
|
|
44
|
+
text: 'Stellen Sie sicher, dass die Lautstärke Ihrer Lautsprecher niedrig ist, bevor Sie auf Play drücken. Sie können die Verstärkung direkt über das Tool anpassen.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Reaktion analysieren',
|
|
48
|
+
text: 'Achten Sie auf mögliche Verzerrungen oder Momente, in denen der Ton verschwindet. Dies deutet auf die physikalischen Grenzen Ihrer Audio-Hardware hin.',
|
|
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: 'de',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const content: ToolLocaleContent<ToneGeneratorUI> = {
|
|
87
|
+
slug,
|
|
88
|
+
title,
|
|
89
|
+
description,
|
|
90
|
+
faqTitle: 'Häufig gestellte Fragen',
|
|
91
|
+
faq: faqData,
|
|
92
|
+
bibliographyTitle: 'Referenzen',
|
|
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: 'Alles über Frequenzen und Schallwellen', level: 2 },
|
|
107
|
+
{
|
|
108
|
+
type: 'paragraph',
|
|
109
|
+
html: 'Schall ist reine Physik in Bewegung. Mit diesem Tool können Sie Schallwellen in Echtzeit manipulieren, von den tiefsten Bässen, die Sie in Ihrer Brust spüren können, bis hin zu den Ultraschall-Höhen, die nur Tiere wahrnehmen können.',
|
|
110
|
+
},
|
|
111
|
+
{ type: 'title', text: 'Menschlicher Hörbereich und Presbyakusis', level: 3 },
|
|
112
|
+
{
|
|
113
|
+
type: 'paragraph',
|
|
114
|
+
html: 'Ein gesundes menschliches Ohr nimmt Geräusche zwischen <strong>20Hz und 20kHz</strong> wahr. Mit zunehmendem Alter sinkt die Obergrenze: Die meisten Erwachsenen über 50 können Töne über 12kHz nicht mehr hören. Der 17,4kHz-Ton, bekannt als "Mückenton", ist ein klassischer Test, den typischerweise nur Teenager bestehen können.',
|
|
115
|
+
},
|
|
116
|
+
{ type: 'title', text: 'Wellenformen und ihre Verwendung', level: 3 },
|
|
117
|
+
{
|
|
118
|
+
type: 'paragraph',
|
|
119
|
+
html: '<strong>Sinus:</strong> reiner Ton ohne Oberwellen, wird in medizinischen Hörtests und zur Instrumentenkalibrierung verwendet. <strong>Rechteck:</strong> reich an ungeraden Oberwellen, klingt wie Retro-8-Bit-Konsolen. <strong>Sägezahn:</strong> enthält alle Oberwellen, die Basis für Synthesizer in der elektronischen Musik. <strong>Dreieck:</strong> Mittelpunkt zwischen Sinus und Rechteck — glatter als Rechteck, aber mit mehr harmonischem Gehalt als Sinus.',
|
|
120
|
+
},
|
|
121
|
+
{ type: 'title', text: 'Lautsprechertest und Reinigung durch Vibration', level: 3 },
|
|
122
|
+
{
|
|
123
|
+
type: 'paragraph',
|
|
124
|
+
html: 'Eine niederfrequente Welle (typischerweise <strong>165Hz</strong>) bei maximaler Lautstärke mit Rechteck- oder Sägezahnform zwingt die Lautsprechermembran zu heftigen Vibrationen, wodurch Wassertropfen, die im Gitter gefangen sind, mechanisch ausgestoßen werden. Spielen Sie extrem hohe Frequenzen nicht über längere Zeit bei maximaler Lautstärke ab: Auch wenn Sie sie nicht hören können, kann die Energie die Hochtöner Ihrer Ausrüstung beschädigen.',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
ui: {
|
|
128
|
+
badge: 'Audio-Generator',
|
|
129
|
+
title: 'Frequenzgenerator',
|
|
130
|
+
description: 'Erzeugen Sie reine Frequenzen für Audiotests.',
|
|
131
|
+
freqLabel: 'Frequenz',
|
|
132
|
+
preset60: '60Hz',
|
|
133
|
+
preset440: '440Hz',
|
|
134
|
+
preset1k: '1kHz',
|
|
135
|
+
rangeMin: '20Hz (Sub-Bass)',
|
|
136
|
+
rangeMax: '20kHz (Ultraschall)',
|
|
137
|
+
waveLabel: 'Wellenform',
|
|
138
|
+
waveSine: 'Sinus',
|
|
139
|
+
waveSquare: 'Rechteck',
|
|
140
|
+
waveSawtooth: 'Sägezahn',
|
|
141
|
+
waveTriangle: 'Dreieck',
|
|
142
|
+
volLabel: 'Lautstärke',
|
|
143
|
+
btnPlay: 'TON ABSPIELEN',
|
|
144
|
+
btnStop: 'STOPP',
|
|
145
|
+
},
|
|
146
|
+
};
|