@jjlmoya/utils-games-development 1.53.0 → 1.55.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 +6 -1
- package/src/index.ts +2 -0
- package/src/tests/contrast_legibility.test.ts +245 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/itchioGameTester/itchio-game-tester.css +3 -3
- package/src/tool/localizationSanitizer/localization-sanitizer.css +13 -13
- package/src/tool/retroSfxGenerator/bibliography.astro +6 -0
- package/src/tool/retroSfxGenerator/bibliography.ts +16 -0
- package/src/tool/retroSfxGenerator/client.ts +256 -0
- package/src/tool/retroSfxGenerator/component.astro +146 -0
- package/src/tool/retroSfxGenerator/entry.ts +27 -0
- package/src/tool/retroSfxGenerator/i18n/de.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/en.ts +224 -0
- package/src/tool/retroSfxGenerator/i18n/es.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/fr.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/id.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/it.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ja.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ko.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/nl.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/pl.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/pt.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/ru.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/sv.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/tr.ts +211 -0
- package/src/tool/retroSfxGenerator/i18n/zh.ts +211 -0
- package/src/tool/retroSfxGenerator/index.ts +11 -0
- package/src/tool/retroSfxGenerator/logic.test.ts +40 -0
- package/src/tool/retroSfxGenerator/logic.ts +214 -0
- package/src/tool/retroSfxGenerator/retro-sfx-generator.css +429 -0
- package/src/tool/retroSfxGenerator/seo.astro +15 -0
- package/src/tool/retroSfxGenerator/ui.ts +41 -0
- package/src/tool/saveFileEditor/game-save-file-editor.css +5 -5
- package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +5 -5
- package/src/tool/steamBbcodeTranslator/app-client.ts +125 -0
- package/src/tool/steamBbcodeTranslator/bbcode-parser.ts +110 -0
- package/src/tool/steamBbcodeTranslator/bibliography.astro +6 -0
- package/src/tool/steamBbcodeTranslator/bibliography.ts +14 -0
- package/src/tool/steamBbcodeTranslator/component.astro +83 -0
- package/src/tool/steamBbcodeTranslator/entry.ts +28 -0
- package/src/tool/steamBbcodeTranslator/html-parser.ts +42 -0
- package/src/tool/steamBbcodeTranslator/html-renderer.ts +71 -0
- package/src/tool/steamBbcodeTranslator/i18n/de.ts +207 -0
- package/src/tool/steamBbcodeTranslator/i18n/en.ts +236 -0
- package/src/tool/steamBbcodeTranslator/i18n/es.ts +207 -0
- package/src/tool/steamBbcodeTranslator/i18n/fr.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/id.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/it.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/ja.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/ko.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/nl.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/pl.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/pt.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/ru.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/sv.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/tr.ts +195 -0
- package/src/tool/steamBbcodeTranslator/i18n/zh.ts +195 -0
- package/src/tool/steamBbcodeTranslator/index.ts +12 -0
- package/src/tool/steamBbcodeTranslator/logic.test.ts +41 -0
- package/src/tool/steamBbcodeTranslator/logic.ts +96 -0
- package/src/tool/steamBbcodeTranslator/markdown-parser.ts +115 -0
- package/src/tool/steamBbcodeTranslator/markdown-renderer.ts +85 -0
- package/src/tool/steamBbcodeTranslator/seo.astro +15 -0
- package/src/tool/steamBbcodeTranslator/steam-bbcode-translator.css +392 -0
- package/src/tool/steamBbcodeTranslator/storage.ts +30 -0
- package/src/tool/steamBbcodeTranslator/types.ts +36 -0
- package/src/tool/steamBbcodeTranslator/ui.ts +18 -0
- package/src/tool/steamCapsuleGenerator/steam-capsule-generator.css +6 -6
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { RetroSfxGeneratorUI } from '../ui';
|
|
3
|
+
import { bibliographyEntries } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
export const content: ToolLocaleContent<RetroSfxGeneratorUI> = {
|
|
6
|
+
slug: 'retro-oyun-ses-efekti-olusturucu',
|
|
7
|
+
title: 'Oyunlar icin Retro Ses Efekti Olusturucu',
|
|
8
|
+
description: 'Tarayicinizda anlik hazir ayarlar, dalga sekli kontrolleri, canli osiloskop ve WAV dissa aktarim ile retro oyun ses efektleri olusturun.',
|
|
9
|
+
ui: {
|
|
10
|
+
waveformLabel: 'Dalga sekli',
|
|
11
|
+
waveformSquare: 'Kare',
|
|
12
|
+
waveformSawtooth: 'Testere disi',
|
|
13
|
+
waveformSine: 'Sinus',
|
|
14
|
+
waveformTriangle: 'Ucgen',
|
|
15
|
+
waveformNoise: 'Gurutu',
|
|
16
|
+
presetLabel: 'Hizli ses bankasi',
|
|
17
|
+
presetExplosion: 'Patlama',
|
|
18
|
+
presetLaser: 'Lazer',
|
|
19
|
+
presetJump: 'Ziplama',
|
|
20
|
+
presetCoin: 'Altin',
|
|
21
|
+
presetPowerUp: 'Guc alma',
|
|
22
|
+
frequencyLabel: 'Baslangic frekansi',
|
|
23
|
+
frequencyEndLabel: 'Bitis frekansi',
|
|
24
|
+
durationLabel: 'Uzunluk',
|
|
25
|
+
decayLabel: 'Sönümleme',
|
|
26
|
+
sweepLabel: 'Frekans kaymasi',
|
|
27
|
+
vibratoLabel: 'Vibrato',
|
|
28
|
+
lowpassLabel: 'Alcak geciren',
|
|
29
|
+
highpassLabel: 'Yuksek geciren',
|
|
30
|
+
noiseMixLabel: 'Gurultu karisimi',
|
|
31
|
+
toneSection: 'Ton',
|
|
32
|
+
dynamicsSection: 'Dinamik',
|
|
33
|
+
filterSection: 'Doku',
|
|
34
|
+
playButton: 'Sesi cal',
|
|
35
|
+
stopButton: 'Durdur',
|
|
36
|
+
downloadButton: 'WAV indir',
|
|
37
|
+
randomizeButton: 'Rastgele',
|
|
38
|
+
resetButton: 'Sifirla',
|
|
39
|
+
waveformPreviewLabel: 'Canli dalga sekli',
|
|
40
|
+
generatedLabel: 'Olusturuldu',
|
|
41
|
+
statusReady: 'Dinlemeye hazir',
|
|
42
|
+
statusPlaying: 'Tarayicida caliniyor',
|
|
43
|
+
statusStopped: 'Oynatma durduruldu',
|
|
44
|
+
statusDownloaded: 'WAV indirildi',
|
|
45
|
+
statusAudioBlocked: 'Oynatma icin sekmede ses izni gerekiyor',
|
|
46
|
+
statusGenerating: 'Ses isleniyor',
|
|
47
|
+
presetHint: 'Bir baslangic noktasi secin ve sinyali asagidaki kontrollerle sekillendirin.',
|
|
48
|
+
monoWavHint: '44.1 kHz · 16-bit mono WAV',
|
|
49
|
+
},
|
|
50
|
+
seo: [
|
|
51
|
+
{
|
|
52
|
+
type: 'title',
|
|
53
|
+
level: 2,
|
|
54
|
+
text: 'Game Jam Sirasinda Tarayicidan Ayrilmadan Ses Efektleri Uretin',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'paragraph',
|
|
58
|
+
html: 'Iyi bir oyun ses efekti oyuncuya eylemi aninda bildirmelidir. Yükselen bir frekans ziplama veya guc almayi, hizla dusen bir frekans ise lazer veya darbeyi hissettirir. Bu arac tarayici icinde ses sentezi yapar.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'stats',
|
|
62
|
+
columns: 4,
|
|
63
|
+
items: [
|
|
64
|
+
{ label: 'Ses hazir ayarlari', value: '5 baslangic sablonu' },
|
|
65
|
+
{ label: 'Osilator secenekleri', value: '5 dalga sekli' },
|
|
66
|
+
{ label: 'Aktarim formati', value: '16-bit PCM WAV' },
|
|
67
|
+
{ label: 'Islem', value: 'Tarayici tabanli' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'title',
|
|
72
|
+
level: 2,
|
|
73
|
+
text: 'Kontroller Neyi Degistirir',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'comparative',
|
|
77
|
+
columns: 2,
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
title: 'Ton ve Hareket',
|
|
81
|
+
description: 'Ton kontrolleri sesin temel kimligini belirler.',
|
|
82
|
+
points: [
|
|
83
|
+
'Baslangic frekansi ana ses yuksekligini belirler',
|
|
84
|
+
'Frekans kaymasi sesi bitis frekansina dogru surukler',
|
|
85
|
+
'Vibrato hafif periyodik ton dalgalanmasi ekler',
|
|
86
|
+
'Farkli dalga sekilleri farkli armonik dokular olusturur',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: 'Bicim ve Doku',
|
|
91
|
+
description: 'Dinamik ve filtre kontrolleri sesin sönümlenmesini ayarlar.',
|
|
92
|
+
points: [
|
|
93
|
+
'Uzunluk sesin toplam suresini belirler',
|
|
94
|
+
'Sönümleme genligin ne kadar hizli dusecegini ayarlar',
|
|
95
|
+
'Alcak geciren filtre sert yuksek frekanslari yumusatir',
|
|
96
|
+
'Yuksek geciren filtre bas frekanslari temizler',
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'title',
|
|
103
|
+
level: 2,
|
|
104
|
+
text: 'Oyun Etkinlikleri Icin Pratik Tarifler',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'table',
|
|
108
|
+
headers: ['Etkinlik', 'Kullanisli Baslangic Sablonu', 'Denenmesi Gereken Ilk Ayar'],
|
|
109
|
+
rows: [
|
|
110
|
+
['Patlama', 'Dusuk frekansli gurultu', 'Süreyi uzatin ve alcak geciren filtreyi dusurun'],
|
|
111
|
+
['Lazer', 'Dusen kaymali testere disi', 'Süreyi kisaltin ve yuksek geciren filtreyi yukseltin'],
|
|
112
|
+
['Ziplama', 'Yukselen kaymali kare', 'Yükselisin net duyulmasi icin sönümlemeyi azaltin'],
|
|
113
|
+
['Altin', 'Kisa yukselen kaymali kare', 'Parlak bir ses icin baslangic frekansini artirin'],
|
|
114
|
+
['Guc alma', 'Uzun yukselen kaymali ucgen', 'Daha hareketli bir ses icin biraz vibrato ekleyin'],
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'title',
|
|
119
|
+
level: 2,
|
|
120
|
+
text: 'WAV Neden Prototipler Icin Idealdir',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'paragraph',
|
|
124
|
+
html: 'WAV formati ekstra sıkıştırma kütüphanesine ihtiyaç duymadan tum popüler oyun motorlarinda doğrudan kullanilabilir.',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'tip',
|
|
128
|
+
title: 'Hedef Oyun Ses Seviyesinde Dinleyin',
|
|
129
|
+
html: 'Tek basina harika duyulan bir ses oyunda surekli tekrarlandiginda yorucu olabilir. Oyunda fon muzigiyle birlikte test edin.',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
faqTitle: 'Sikca Sorulan Sorular',
|
|
133
|
+
faq: [
|
|
134
|
+
{
|
|
135
|
+
question: 'Seslerim sunucuya yukleniyor mu?',
|
|
136
|
+
answer: 'Hayir. Sentezleme ve WAV kodlamasi tamamen tarayicinizda gerçekleşir.',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
question: 'Indirilen sesleri oyunumda kullanabilir miyim?',
|
|
140
|
+
answer: 'Evet. Uretilen sesler prototipler ve bagimsiz oyun projeleri icin uygundur.',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
question: 'Frekans kaymasi nasil calisir?',
|
|
144
|
+
answer: 'Kaydirici, baslangic frekansina orantili olarak bitis frekansini hesaplar.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
question: 'Ses gelmiyorsa ne yapmaliyim?',
|
|
148
|
+
answer: 'Tarayici sekmesinin ses iznine sahip oldugundan emin olun ve Sesi cal butonuna tekrar tiklayin.',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
howTo: [
|
|
152
|
+
{
|
|
153
|
+
name: 'Bir hazir ayar secin',
|
|
154
|
+
text: 'Patlama, Lazer, Ziplama, Altin veya Guc alma seceneklerinden birini secin.',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'Sinyali sekillendirin',
|
|
158
|
+
text: 'Dalga sekli, frekans, süre ve filtre ayarlarini yapin.',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'Sonucu dinleyin',
|
|
162
|
+
text: 'Canli dinlemek icin Sesi cal butonuna tiklayin.',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'WAV dosyasini indirin',
|
|
166
|
+
text: 'Dosyayi kaydetmek icin WAV indir butonuna tiklayin.',
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
schemas: [
|
|
170
|
+
{
|
|
171
|
+
'@context': 'https://schema.org',
|
|
172
|
+
'@type': 'SoftwareApplication',
|
|
173
|
+
name: 'Oyunlar icin Retro Ses Efekti Olusturucu',
|
|
174
|
+
applicationCategory: 'MultimediaApplication',
|
|
175
|
+
operatingSystem: 'Any',
|
|
176
|
+
offers: {
|
|
177
|
+
'@type': 'Offer',
|
|
178
|
+
price: '0',
|
|
179
|
+
priceCurrency: 'TRY',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
'@context': 'https://schema.org',
|
|
184
|
+
'@type': 'FAQPage',
|
|
185
|
+
mainEntity: [
|
|
186
|
+
{
|
|
187
|
+
'@type': 'Question',
|
|
188
|
+
name: 'Seslerim sunucuya yukleniyor mu?',
|
|
189
|
+
acceptedAnswer: {
|
|
190
|
+
'@type': 'Answer',
|
|
191
|
+
text: 'Hayir. Sentezleme tamamen tarayicinizda gerçekleşir.',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
'@context': 'https://schema.org',
|
|
198
|
+
'@type': 'HowTo',
|
|
199
|
+
name: 'Retro ses efekti olusturma adimlari',
|
|
200
|
+
step: [
|
|
201
|
+
{
|
|
202
|
+
'@type': 'HowToStep',
|
|
203
|
+
name: 'Bir hazir ayar secin',
|
|
204
|
+
text: 'Bir baslangic sablonu secin.',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
bibliographyTitle: 'Kaynaklar',
|
|
210
|
+
bibliography: bibliographyEntries,
|
|
211
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { RetroSfxGeneratorUI } from '../ui';
|
|
3
|
+
import { bibliographyEntries } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
export const content: ToolLocaleContent<RetroSfxGeneratorUI> = {
|
|
6
|
+
slug: 'retro-sfx-generator',
|
|
7
|
+
title: '复古游戏音效生成器',
|
|
8
|
+
description: '在浏览器中直接生成复古游戏音效,提供快速预设、波形调节、实时示波器与一键 WAV 导出。',
|
|
9
|
+
ui: {
|
|
10
|
+
waveformLabel: '波形',
|
|
11
|
+
waveformSquare: '方波',
|
|
12
|
+
waveformSawtooth: '锯齿波',
|
|
13
|
+
waveformSine: '正弦波',
|
|
14
|
+
waveformTriangle: '三角波',
|
|
15
|
+
waveformNoise: '噪声',
|
|
16
|
+
presetLabel: '预设音效库',
|
|
17
|
+
presetExplosion: '爆炸',
|
|
18
|
+
presetLaser: '激光',
|
|
19
|
+
presetJump: '跳跃',
|
|
20
|
+
presetCoin: '金币',
|
|
21
|
+
presetPowerUp: '道具强化',
|
|
22
|
+
frequencyLabel: '起始频率',
|
|
23
|
+
frequencyEndLabel: '终止频率',
|
|
24
|
+
durationLabel: '时长',
|
|
25
|
+
decayLabel: '衰减',
|
|
26
|
+
sweepLabel: '音调滑音',
|
|
27
|
+
vibratoLabel: '颤音',
|
|
28
|
+
lowpassLabel: '低通滤波',
|
|
29
|
+
highpassLabel: '高通滤波',
|
|
30
|
+
noiseMixLabel: '噪声混合',
|
|
31
|
+
toneSection: '音高',
|
|
32
|
+
dynamicsSection: '动态',
|
|
33
|
+
filterSection: '纹理',
|
|
34
|
+
playButton: '播放音效',
|
|
35
|
+
stopButton: '停止',
|
|
36
|
+
downloadButton: '下载 WAV',
|
|
37
|
+
randomizeButton: '随机生成',
|
|
38
|
+
resetButton: '重置',
|
|
39
|
+
waveformPreviewLabel: '实时波形预览',
|
|
40
|
+
generatedLabel: '已生成',
|
|
41
|
+
statusReady: '就绪,可随时试听',
|
|
42
|
+
statusPlaying: '正在浏览器中播放',
|
|
43
|
+
statusStopped: '播放已停止',
|
|
44
|
+
statusDownloaded: 'WAV 已下载',
|
|
45
|
+
statusAudioBlocked: '播放需要允许浏览器标签页的音频权限',
|
|
46
|
+
statusGenerating: '正在合成音效',
|
|
47
|
+
presetHint: '选择一个预设起点,然后使用下方滑块调整音频信号。',
|
|
48
|
+
monoWavHint: '44.1 kHz · 16-bit 单声道 WAV',
|
|
49
|
+
},
|
|
50
|
+
seo: [
|
|
51
|
+
{
|
|
52
|
+
type: 'title',
|
|
53
|
+
level: 2,
|
|
54
|
+
text: '在 Game Jam 期间无需离开浏览器即可制作游戏音效',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'paragraph',
|
|
58
|
+
html: '优秀的游戏音效能立即向玩家传达操作反馈。音高的快速上升可代表跳跃或获得道具,快速下降的滑音代表激光或击中,而带有衰减的噪声能完美表现爆炸。本工具完全在浏览器内部合成并导出音效。',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'stats',
|
|
62
|
+
columns: 4,
|
|
63
|
+
items: [
|
|
64
|
+
{ label: '预设模式', value: '5 种基础预设' },
|
|
65
|
+
{ label: '波形选择', value: '5 种振荡器波形' },
|
|
66
|
+
{ label: '导出格式', value: '16-bit PCM WAV' },
|
|
67
|
+
{ label: '处理方式', value: '浏览器本地合成' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'title',
|
|
72
|
+
level: 2,
|
|
73
|
+
text: '控制参数说明',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'comparative',
|
|
77
|
+
columns: 2,
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
title: '音调与变化',
|
|
81
|
+
description: '音高控制决定音效的基础音色。',
|
|
82
|
+
points: [
|
|
83
|
+
'起始频率设定振荡器的初始音高',
|
|
84
|
+
'音调滑音控制频率向终止值的滑动变化',
|
|
85
|
+
'颤音加入轻微的周期性音高波动',
|
|
86
|
+
'不同波形产生丰富的谐波纹理',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: '形态与纹理',
|
|
91
|
+
description: '动态与滤波控制决定音效的尾音和包络。',
|
|
92
|
+
points: [
|
|
93
|
+
'时长设定音效的总播放时间',
|
|
94
|
+
'衰减控制音量下降的快慢',
|
|
95
|
+
'低通滤波器使高频更加柔柔',
|
|
96
|
+
'高通滤波器滤除低频,配合噪声增加颗粒感',
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'title',
|
|
103
|
+
level: 2,
|
|
104
|
+
text: '常见游戏事件的实用配置参考',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'table',
|
|
108
|
+
headers: ['事件类型', '推荐起点预设', '建议优先尝试的调整'],
|
|
109
|
+
rows: [
|
|
110
|
+
['爆炸', '低频噪声', '增加时长并降低低通滤波,获得更浑厚的爆破感'],
|
|
111
|
+
['激光', '带下降滑音的锯齿波', '缩短时长并提高高通滤波,使攻击感更加锋利'],
|
|
112
|
+
['跳跃', '带上升滑音的方波', '减少衰减时间,使上升感更加清晰'],
|
|
113
|
+
['金币', '短促上升滑音的方波', '提高起始频率,打造清脆的拾取声'],
|
|
114
|
+
['道具强化', '带长上升滑音的三角波', '加入微量颤音,增加动态丰富度'],
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'title',
|
|
119
|
+
level: 2,
|
|
120
|
+
text: '为什么 WAV 是原型开发的理想导出格式',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'paragraph',
|
|
124
|
+
html: 'WAV 格式无需额外的解压库即可直接导入主流游戏引擎(如 Unity、Godot 等)。本工具在内存中直接生成小巧的单声道 WAV 文件并供一键下载。',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'tip',
|
|
128
|
+
title: '在游戏实际音量下进行测试',
|
|
129
|
+
html: '单独听起来出色的音效如果在游戏中高频触发可能会造成听觉疲劳。请务必在游戏实际场景中与背景音乐一起测试。',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
faqTitle: '常见问题',
|
|
133
|
+
faq: [
|
|
134
|
+
{
|
|
135
|
+
question: '生成的音效会上传到服务器吗?',
|
|
136
|
+
answer: '不会。所有合成与 WAV 编码完全在您的浏览器本地完成。',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
question: '我可以将下载的音效用于商业游戏吗?',
|
|
140
|
+
answer: '可以。生成的音频非常适合用于原型开发、Game Jam 以及独立游戏制作。',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
question: '音调滑音是如何计算的?',
|
|
144
|
+
answer: '滑块根据起始频率按比例计算终止频率,确保不同音高下的滑动效果自然流畅。',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
question: '如果没有声音怎么办?',
|
|
148
|
+
answer: '请确认浏览器标签页已开启音频播放权限,然后重新点击播放音效。',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
howTo: [
|
|
152
|
+
{
|
|
153
|
+
name: '选择音效预设',
|
|
154
|
+
text: '从爆炸、激光、跳跃、金币或道具强化中选择一个起点。',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: '调节音频参数',
|
|
158
|
+
text: '调整波质、音色、时长、衰减及过滤器滑块。',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: '试听生成效果',
|
|
162
|
+
text: '点击播放音效实时听取声音效果。',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: '下载 WAV 文件',
|
|
166
|
+
text: '点击下载 WAV 保存 44.1 kHz 16-bit 单声道音频文件。',
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
schemas: [
|
|
170
|
+
{
|
|
171
|
+
'@context': 'https://schema.org',
|
|
172
|
+
'@type': 'SoftwareApplication',
|
|
173
|
+
name: '复古游戏音效生成器',
|
|
174
|
+
applicationCategory: 'MultimediaApplication',
|
|
175
|
+
operatingSystem: 'Any',
|
|
176
|
+
offers: {
|
|
177
|
+
'@type': 'Offer',
|
|
178
|
+
price: '0',
|
|
179
|
+
priceCurrency: 'CNY',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
'@context': 'https://schema.org',
|
|
184
|
+
'@type': 'FAQPage',
|
|
185
|
+
mainEntity: [
|
|
186
|
+
{
|
|
187
|
+
'@type': 'Question',
|
|
188
|
+
name: '生成的音效会上传到服务器吗?',
|
|
189
|
+
acceptedAnswer: {
|
|
190
|
+
'@type': 'Answer',
|
|
191
|
+
text: '不会。所有合成与 WAV 编码完全在您的浏览器本地完成。',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
'@context': 'https://schema.org',
|
|
198
|
+
'@type': 'HowTo',
|
|
199
|
+
name: '如何制作复古游戏音效',
|
|
200
|
+
step: [
|
|
201
|
+
{
|
|
202
|
+
'@type': 'HowToStep',
|
|
203
|
+
name: '选择音效预设',
|
|
204
|
+
text: '选择预设起点。',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
bibliographyTitle: '参考文献',
|
|
210
|
+
bibliography: bibliographyEntries,
|
|
211
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { retroSfxGenerator } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const RETRO_SFX_GENERATOR_TOOL: ToolDefinition = {
|
|
7
|
+
entry: retroSfxGenerator,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
calculateEndFrequency,
|
|
4
|
+
encodeWav,
|
|
5
|
+
generateSfx,
|
|
6
|
+
getPresetParams,
|
|
7
|
+
updateSweep,
|
|
8
|
+
} from './logic';
|
|
9
|
+
|
|
10
|
+
describe('retroSfxGenerator logic', () => {
|
|
11
|
+
it('creates deterministic preset parameters when random is injected', () => {
|
|
12
|
+
const first = getPresetParams('coin', () => 0.5);
|
|
13
|
+
const second = getPresetParams('coin', () => 0.5);
|
|
14
|
+
expect(first).toEqual(second);
|
|
15
|
+
expect(first.frequencyEnd).toBe(calculateEndFrequency(first.frequency, first.sweep));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('maps sweep to a bounded end frequency', () => {
|
|
19
|
+
expect(calculateEndFrequency(440, -1)).toBeGreaterThanOrEqual(40);
|
|
20
|
+
expect(calculateEndFrequency(440, 1)).toBeLessThanOrEqual(12000);
|
|
21
|
+
expect(updateSweep(getPresetParams('laser', () => 0.5), 0.25).frequencyEnd).toBeGreaterThan(440);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('generates samples within the PCM range and expected duration', () => {
|
|
25
|
+
const params = getPresetParams('explosion', () => 0.5);
|
|
26
|
+
const generated = generateSfx(params, 8000);
|
|
27
|
+
expect(generated.samples.length).toBeGreaterThan(0);
|
|
28
|
+
expect(generated.duration).toBeCloseTo(params.duration, 2);
|
|
29
|
+
expect(Math.max(...generated.samples)).toBeLessThanOrEqual(1);
|
|
30
|
+
expect(Math.min(...generated.samples)).toBeGreaterThanOrEqual(-1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('encodes a mono 16-bit WAV with a valid RIFF header', () => {
|
|
34
|
+
const wav = encodeWav(new Float32Array([0, 0.5, -0.5, 0]), 44100);
|
|
35
|
+
expect(wav.length).toBe(52);
|
|
36
|
+
expect(String.fromCharCode(...wav.slice(0, 4))).toBe('RIFF');
|
|
37
|
+
expect(String.fromCharCode(...wav.slice(8, 12))).toBe('WAVE');
|
|
38
|
+
expect(new DataView(wav.buffer).getUint32(40, true)).toBe(8);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export type SfxWaveform = 'square' | 'sawtooth' | 'sine' | 'triangle' | 'noise';
|
|
2
|
+
|
|
3
|
+
export type SfxPreset = 'explosion' | 'laser' | 'jump' | 'coin' | 'powerUp';
|
|
4
|
+
|
|
5
|
+
export interface SfxParams {
|
|
6
|
+
waveform: SfxWaveform;
|
|
7
|
+
frequency: number;
|
|
8
|
+
frequencyEnd: number;
|
|
9
|
+
duration: number;
|
|
10
|
+
decay: number;
|
|
11
|
+
sweep: number;
|
|
12
|
+
vibrato: number;
|
|
13
|
+
lowpass: number;
|
|
14
|
+
highpass: number;
|
|
15
|
+
noiseMix: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GeneratedSfx {
|
|
19
|
+
samples: Float32Array;
|
|
20
|
+
sampleRate: number;
|
|
21
|
+
duration: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const MIN_SAMPLE_RATE = 8000;
|
|
25
|
+
export const DEFAULT_SAMPLE_RATE = 44100;
|
|
26
|
+
|
|
27
|
+
const PRESET_RANGES: Record<SfxPreset, Omit<SfxParams, 'frequencyEnd'>> = {
|
|
28
|
+
explosion: {
|
|
29
|
+
waveform: 'noise',
|
|
30
|
+
frequency: 180,
|
|
31
|
+
duration: 0.65,
|
|
32
|
+
decay: 0.84,
|
|
33
|
+
sweep: -0.55,
|
|
34
|
+
vibrato: 0.08,
|
|
35
|
+
lowpass: 3600,
|
|
36
|
+
highpass: 70,
|
|
37
|
+
noiseMix: 0.9,
|
|
38
|
+
},
|
|
39
|
+
laser: {
|
|
40
|
+
waveform: 'sawtooth',
|
|
41
|
+
frequency: 1200,
|
|
42
|
+
duration: 0.32,
|
|
43
|
+
decay: 0.56,
|
|
44
|
+
sweep: -0.7,
|
|
45
|
+
vibrato: 0.03,
|
|
46
|
+
lowpass: 9000,
|
|
47
|
+
highpass: 180,
|
|
48
|
+
noiseMix: 0.04,
|
|
49
|
+
},
|
|
50
|
+
jump: {
|
|
51
|
+
waveform: 'square',
|
|
52
|
+
frequency: 260,
|
|
53
|
+
duration: 0.42,
|
|
54
|
+
decay: 0.35,
|
|
55
|
+
sweep: 0.75,
|
|
56
|
+
vibrato: 0.06,
|
|
57
|
+
lowpass: 7600,
|
|
58
|
+
highpass: 100,
|
|
59
|
+
noiseMix: 0.02,
|
|
60
|
+
},
|
|
61
|
+
coin: {
|
|
62
|
+
waveform: 'square',
|
|
63
|
+
frequency: 880,
|
|
64
|
+
duration: 0.24,
|
|
65
|
+
decay: 0.44,
|
|
66
|
+
sweep: 0.28,
|
|
67
|
+
vibrato: 0.02,
|
|
68
|
+
lowpass: 11000,
|
|
69
|
+
highpass: 140,
|
|
70
|
+
noiseMix: 0,
|
|
71
|
+
},
|
|
72
|
+
powerUp: {
|
|
73
|
+
waveform: 'triangle',
|
|
74
|
+
frequency: 330,
|
|
75
|
+
duration: 0.78,
|
|
76
|
+
decay: 0.24,
|
|
77
|
+
sweep: 0.82,
|
|
78
|
+
vibrato: 0.11,
|
|
79
|
+
lowpass: 10000,
|
|
80
|
+
highpass: 90,
|
|
81
|
+
noiseMix: 0.01,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
86
|
+
return Math.min(max, Math.max(min, value));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createDefaultParams(): SfxParams {
|
|
90
|
+
return getPresetParams('laser', () => 0.5);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getPresetParams(preset: SfxPreset, random: () => number = Math.random): SfxParams {
|
|
94
|
+
const base = PRESET_RANGES[preset];
|
|
95
|
+
const frequencyJitter = 1 + (random() - 0.5) * 0.18;
|
|
96
|
+
const durationJitter = 1 + (random() - 0.5) * 0.12;
|
|
97
|
+
const sweepJitter = (random() - 0.5) * 0.16;
|
|
98
|
+
const frequency = clamp(base.frequency * frequencyJitter, 60, 8000);
|
|
99
|
+
const duration = clamp(base.duration * durationJitter, 0.08, 2.5);
|
|
100
|
+
const sweep = clamp(base.sweep + sweepJitter, -1, 1);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
frequency,
|
|
105
|
+
frequencyEnd: calculateEndFrequency(frequency, sweep),
|
|
106
|
+
duration,
|
|
107
|
+
sweep,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function calculateEndFrequency(frequency: number, sweep: number): number {
|
|
112
|
+
const normalizedSweep = clamp(sweep, -1, 1);
|
|
113
|
+
const ratio = normalizedSweep >= 0
|
|
114
|
+
? 1 + normalizedSweep * 11
|
|
115
|
+
: 1 / (1 + Math.abs(normalizedSweep) * 11);
|
|
116
|
+
return clamp(frequency * ratio, 40, 12000);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function updateSweep(params: SfxParams, sweep: number): SfxParams {
|
|
120
|
+
const safeSweep = clamp(sweep, -1, 1);
|
|
121
|
+
return { ...params, sweep: safeSweep, frequencyEnd: calculateEndFrequency(params.frequency, safeSweep) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function seededNoise(index: number): number {
|
|
125
|
+
const value = Math.sin((index + 1) * 12.9898) * 43758.5453;
|
|
126
|
+
return (value - Math.floor(value)) * 2 - 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function waveformSample(waveform: SfxWaveform, phase: number, index: number): number {
|
|
130
|
+
const cycle = phase - Math.floor(phase);
|
|
131
|
+
if (waveform === 'square') return cycle < 0.5 ? 1 : -1;
|
|
132
|
+
if (waveform === 'sawtooth') return cycle * 2 - 1;
|
|
133
|
+
if (waveform === 'triangle') return 1 - 4 * Math.abs(Math.round(cycle) - cycle);
|
|
134
|
+
if (waveform === 'noise') return seededNoise(index);
|
|
135
|
+
return Math.sin(cycle * Math.PI * 2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function filterAlpha(cutoff: number, sampleRate: number): number {
|
|
139
|
+
const safeCutoff = clamp(cutoff, 1, sampleRate * 0.45);
|
|
140
|
+
return 1 - Math.exp((-2 * Math.PI * safeCutoff) / sampleRate);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function generateSfx(params: SfxParams, sampleRate = DEFAULT_SAMPLE_RATE): GeneratedSfx {
|
|
144
|
+
const safeSampleRate = Math.max(MIN_SAMPLE_RATE, Math.round(sampleRate));
|
|
145
|
+
const duration = clamp(params.duration, 0.04, 4);
|
|
146
|
+
const frameCount = Math.max(1, Math.round(duration * safeSampleRate));
|
|
147
|
+
const samples = new Float32Array(frameCount);
|
|
148
|
+
const lowAlpha = filterAlpha(params.lowpass, safeSampleRate);
|
|
149
|
+
const highAlpha = filterAlpha(params.highpass, safeSampleRate);
|
|
150
|
+
const noiseMix = clamp(params.noiseMix, 0, 1);
|
|
151
|
+
const vibrato = clamp(params.vibrato, 0, 1);
|
|
152
|
+
let phase = 0;
|
|
153
|
+
let lowState = 0;
|
|
154
|
+
let highState = 0;
|
|
155
|
+
|
|
156
|
+
for (let index = 0; index < frameCount; index += 1) {
|
|
157
|
+
const progress = frameCount === 1 ? 1 : index / (frameCount - 1);
|
|
158
|
+
const frequency = Math.max(20, params.frequency + (params.frequencyEnd - params.frequency) * progress);
|
|
159
|
+
const vibratoFactor = 1 + Math.sin(progress * Math.PI * 2 * 7) * vibrato * 0.08;
|
|
160
|
+
phase += (frequency * vibratoFactor) / safeSampleRate;
|
|
161
|
+
const tone = waveformSample(params.waveform, phase, index);
|
|
162
|
+
const noise = seededNoise(index + 1987);
|
|
163
|
+
const mixed = tone * (1 - noiseMix) + noise * noiseMix;
|
|
164
|
+
lowState += lowAlpha * (mixed - lowState);
|
|
165
|
+
highState += highAlpha * (mixed - highState);
|
|
166
|
+
const filtered = lowState * 0.7 + (mixed - highState) * 0.3;
|
|
167
|
+
const attack = Math.min(1, progress / 0.025);
|
|
168
|
+
const decayPower = 0.35 + clamp(params.decay, 0, 1) * 4;
|
|
169
|
+
const envelope = attack * Math.pow(1 - progress, decayPower);
|
|
170
|
+
samples[index] = clamp(filtered * envelope * 0.92, -1, 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { samples, sampleRate: safeSampleRate, duration: frameCount / safeSampleRate };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function encodeWav(samples: Float32Array, sampleRate: number): Uint8Array {
|
|
177
|
+
const safeSampleRate = Math.max(MIN_SAMPLE_RATE, Math.round(sampleRate));
|
|
178
|
+
const dataSize = samples.length * 2;
|
|
179
|
+
const buffer = new Uint8Array(44 + dataSize);
|
|
180
|
+
const view = new DataView(buffer.buffer);
|
|
181
|
+
const writeText = (offset: number, value: string): void => {
|
|
182
|
+
for (let index = 0; index < value.length; index += 1) buffer[offset + index] = value.charCodeAt(index);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
writeText(0, 'RIFF');
|
|
186
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
187
|
+
writeText(8, 'WAVE');
|
|
188
|
+
writeText(12, 'fmt ');
|
|
189
|
+
view.setUint32(16, 16, true);
|
|
190
|
+
view.setUint16(20, 1, true);
|
|
191
|
+
view.setUint16(22, 1, true);
|
|
192
|
+
view.setUint32(24, safeSampleRate, true);
|
|
193
|
+
view.setUint32(28, safeSampleRate * 2, true);
|
|
194
|
+
view.setUint16(32, 2, true);
|
|
195
|
+
view.setUint16(34, 16, true);
|
|
196
|
+
writeText(36, 'data');
|
|
197
|
+
view.setUint32(40, dataSize, true);
|
|
198
|
+
|
|
199
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
200
|
+
const sample = clamp(samples[index] ?? 0, -1, 1);
|
|
201
|
+
view.setInt16(44 + index * 2, Math.round(sample < 0 ? sample * 0x8000 : sample * 0x7fff), true);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return buffer;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function formatFrequency(value: number): string {
|
|
208
|
+
if (value >= 1000) return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)} kHz`;
|
|
209
|
+
return `${Math.round(value)} Hz`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function formatDuration(value: number): string {
|
|
213
|
+
return `${value.toFixed(2)} s`;
|
|
214
|
+
}
|