11const SLIDER_LIMITS = Object . freeze ( {
22 attackMs : Object . freeze ( { min : 0 , max : 250 , step : 5 , defaultValue : 5 } ) ,
33 durationMs : Object . freeze ( { min : 60 , max : 2000 , step : 5 , defaultValue : 180 } ) ,
4- frequencyHz : Object . freeze ( { min : 80 , max : 1800 , step : 1 , defaultValue : 880 } ) ,
4+ frequencyHz : Object . freeze ( { min : 20 , max : 20000 , step : 1 , defaultValue : 880 } ) ,
55 noiseAmount : Object . freeze ( { min : 0 , max : 1 , step : 0.01 , defaultValue : 0.65 } ) ,
66 noiseDecayMs : Object . freeze ( { min : 20 , max : 600 , step : 5 , defaultValue : 95 } ) ,
77 noiseFilterHz : Object . freeze ( { min : 400 , max : 9000 , step : 50 , defaultValue : 5200 } ) ,
@@ -22,6 +22,108 @@ const SLIDER_INPUTS = Object.freeze([
2222 Object . freeze ( { soundKey : "volume" , inputProperty : "volumeInput" } )
2323] ) ;
2424
25+ const STYLE_CLAMPS = Object . freeze ( {
26+ "pure-tone" : Object . freeze ( {
27+ attackMs : Object . freeze ( { min : 0 , max : 120 } ) ,
28+ durationMs : Object . freeze ( { min : 80 , max : 2000 } ) ,
29+ frequencyHz : Object . freeze ( { min : 20 , max : 20000 } ) ,
30+ noiseAmount : Object . freeze ( { min : 0 , max : 0.25 } ) ,
31+ noiseDecayMs : Object . freeze ( { min : 20 , max : 240 } ) ,
32+ noiseFilterHz : Object . freeze ( { min : 1200 , max : 9000 } ) ,
33+ pitchSweepCents : Object . freeze ( { min : - 1200 , max : 1200 } ) ,
34+ releaseMs : Object . freeze ( { min : 20 , max : 700 } ) ,
35+ volume : Object . freeze ( { min : 0 , max : 1 } )
36+ } ) ,
37+ "noise-only" : Object . freeze ( {
38+ attackMs : Object . freeze ( { min : 0 , max : 80 } ) ,
39+ durationMs : Object . freeze ( { min : 60 , max : 1200 } ) ,
40+ frequencyHz : Object . freeze ( { min : 80 , max : 4000 } ) ,
41+ noiseAmount : Object . freeze ( { min : 0.2 , max : 1 } ) ,
42+ noiseDecayMs : Object . freeze ( { min : 20 , max : 600 } ) ,
43+ noiseFilterHz : Object . freeze ( { min : 400 , max : 9000 } ) ,
44+ pitchSweepCents : Object . freeze ( { min : - 400 , max : 400 } ) ,
45+ releaseMs : Object . freeze ( { min : 20 , max : 500 } ) ,
46+ volume : Object . freeze ( { min : 0 , max : 1 } )
47+ } ) ,
48+ "atari-style" : Object . freeze ( {
49+ attackMs : Object . freeze ( { min : 0 , max : 80 } ) ,
50+ durationMs : Object . freeze ( { min : 60 , max : 900 } ) ,
51+ frequencyHz : Object . freeze ( { min : 40 , max : 4000 } ) ,
52+ noiseAmount : Object . freeze ( { min : 0 , max : 1 } ) ,
53+ noiseDecayMs : Object . freeze ( { min : 20 , max : 450 } ) ,
54+ noiseFilterHz : Object . freeze ( { min : 400 , max : 7000 } ) ,
55+ pitchSweepCents : Object . freeze ( { min : - 1200 , max : 1200 } ) ,
56+ releaseMs : Object . freeze ( { min : 20 , max : 400 } ) ,
57+ volume : Object . freeze ( { min : 0 , max : 1 } )
58+ } ) ,
59+ "classic-arcade" : Object . freeze ( {
60+ attackMs : Object . freeze ( { min : 0 , max : 100 } ) ,
61+ durationMs : Object . freeze ( { min : 60 , max : 1400 } ) ,
62+ frequencyHz : Object . freeze ( { min : 60 , max : 5000 } ) ,
63+ noiseAmount : Object . freeze ( { min : 0 , max : 1 } ) ,
64+ noiseDecayMs : Object . freeze ( { min : 20 , max : 520 } ) ,
65+ noiseFilterHz : Object . freeze ( { min : 500 , max : 8500 } ) ,
66+ pitchSweepCents : Object . freeze ( { min : - 1200 , max : 1200 } ) ,
67+ releaseMs : Object . freeze ( { min : 20 , max : 500 } ) ,
68+ volume : Object . freeze ( { min : 0 , max : 1 } )
69+ } ) ,
70+ "early-analog" : Object . freeze ( {
71+ attackMs : Object . freeze ( { min : 0 , max : 180 } ) ,
72+ durationMs : Object . freeze ( { min : 100 , max : 2000 } ) ,
73+ frequencyHz : Object . freeze ( { min : 30 , max : 8000 } ) ,
74+ noiseAmount : Object . freeze ( { min : 0 , max : 0.6 } ) ,
75+ noiseDecayMs : Object . freeze ( { min : 20 , max : 600 } ) ,
76+ noiseFilterHz : Object . freeze ( { min : 400 , max : 7000 } ) ,
77+ pitchSweepCents : Object . freeze ( { min : - 1200 , max : 1200 } ) ,
78+ releaseMs : Object . freeze ( { min : 40 , max : 700 } ) ,
79+ volume : Object . freeze ( { min : 0 , max : 1 } )
80+ } ) ,
81+ "namco-style" : Object . freeze ( {
82+ attackMs : Object . freeze ( { min : 0 , max : 90 } ) ,
83+ durationMs : Object . freeze ( { min : 60 , max : 900 } ) ,
84+ frequencyHz : Object . freeze ( { min : 120 , max : 6500 } ) ,
85+ noiseAmount : Object . freeze ( { min : 0 , max : 0.5 } ) ,
86+ noiseDecayMs : Object . freeze ( { min : 20 , max : 360 } ) ,
87+ noiseFilterHz : Object . freeze ( { min : 1200 , max : 9000 } ) ,
88+ pitchSweepCents : Object . freeze ( { min : - 900 , max : 1200 } ) ,
89+ releaseMs : Object . freeze ( { min : 20 , max : 420 } ) ,
90+ volume : Object . freeze ( { min : 0 , max : 1 } )
91+ } ) ,
92+ "nintendo-style" : Object . freeze ( {
93+ attackMs : Object . freeze ( { min : 0 , max : 80 } ) ,
94+ durationMs : Object . freeze ( { min : 60 , max : 1100 } ) ,
95+ frequencyHz : Object . freeze ( { min : 80 , max : 5000 } ) ,
96+ noiseAmount : Object . freeze ( { min : 0 , max : 0.9 } ) ,
97+ noiseDecayMs : Object . freeze ( { min : 20 , max : 450 } ) ,
98+ noiseFilterHz : Object . freeze ( { min : 800 , max : 9000 } ) ,
99+ pitchSweepCents : Object . freeze ( { min : - 900 , max : 1200 } ) ,
100+ releaseMs : Object . freeze ( { min : 20 , max : 450 } ) ,
101+ volume : Object . freeze ( { min : 0 , max : 1 } )
102+ } ) ,
103+ "ttl-arcade" : Object . freeze ( {
104+ attackMs : Object . freeze ( { min : 0 , max : 60 } ) ,
105+ durationMs : Object . freeze ( { min : 60 , max : 700 } ) ,
106+ frequencyHz : Object . freeze ( { min : 80 , max : 2500 } ) ,
107+ noiseAmount : Object . freeze ( { min : 0 , max : 1 } ) ,
108+ noiseDecayMs : Object . freeze ( { min : 20 , max : 300 } ) ,
109+ noiseFilterHz : Object . freeze ( { min : 700 , max : 9000 } ) ,
110+ pitchSweepCents : Object . freeze ( { min : - 800 , max : 800 } ) ,
111+ releaseMs : Object . freeze ( { min : 20 , max : 260 } ) ,
112+ volume : Object . freeze ( { min : 0 , max : 1 } )
113+ } ) ,
114+ "vector-arcade" : Object . freeze ( {
115+ attackMs : Object . freeze ( { min : 0 , max : 140 } ) ,
116+ durationMs : Object . freeze ( { min : 80 , max : 1600 } ) ,
117+ frequencyHz : Object . freeze ( { min : 100 , max : 6000 } ) ,
118+ noiseAmount : Object . freeze ( { min : 0 , max : 0.6 } ) ,
119+ noiseDecayMs : Object . freeze ( { min : 20 , max : 500 } ) ,
120+ noiseFilterHz : Object . freeze ( { min : 500 , max : 8500 } ) ,
121+ pitchSweepCents : Object . freeze ( { min : - 1200 , max : 1200 } ) ,
122+ releaseMs : Object . freeze ( { min : 20 , max : 600 } ) ,
123+ volume : Object . freeze ( { min : 0 , max : 1 } )
124+ } )
125+ } ) ;
126+
25127const DEFAULT_SOUND = Object . freeze ( {
26128 attackMs : SLIDER_LIMITS . attackMs . defaultValue ,
27129 durationMs : SLIDER_LIMITS . durationMs . defaultValue ,
@@ -199,6 +301,7 @@ export class SfxControlPanel {
199301 volumeValue,
200302 waveformSelect
201303 } ) {
304+ this . activeSliderLimits = SLIDER_LIMITS ;
202305 this . addButton = addButton ;
203306 this . attackInput = attackInput ;
204307 this . attackValue = attackValue ;
@@ -271,16 +374,41 @@ export class SfxControlPanel {
271374 return SLIDER_INPUTS . map ( ( item ) => this [ item . inputProperty ] ) ;
272375 }
273376
274- applySliderLimits ( ) {
377+ applySliderLimits ( styleKey = "custom" , shouldClampValues = false ) {
378+ this . activeSliderLimits = styleKey === "custom" ? SLIDER_LIMITS : this . sliderLimitsForStyle ( styleKey ) ;
275379 SLIDER_INPUTS . forEach ( ( item ) => {
276380 const input = this [ item . inputProperty ] ;
277- const limits = SLIDER_LIMITS [ item . soundKey ] ;
381+ const limits = this . activeSliderLimits [ item . soundKey ] ;
278382 input . min = String ( limits . min ) ;
279383 input . max = String ( limits . max ) ;
280- input . step = String ( limits . step ) ;
384+ input . step = String ( SLIDER_LIMITS [ item . soundKey ] . step ) ;
385+ if ( shouldClampValues ) {
386+ input . value = String ( this . clampSliderValue ( input , limits ) ) ;
387+ }
281388 } ) ;
282389 }
283390
391+ sliderLimitsForStyle ( styleKey ) {
392+ const clampProfile = STYLE_CLAMPS [ styleKey ] || { } ;
393+ return Object . freeze ( Object . fromEntries ( SLIDER_INPUTS . map ( ( item ) => {
394+ const baseLimits = SLIDER_LIMITS [ item . soundKey ] ;
395+ const clampLimits = clampProfile [ item . soundKey ] || baseLimits ;
396+ return [ item . soundKey , Object . freeze ( {
397+ max : clampLimits . max ,
398+ min : clampLimits . min ,
399+ step : baseLimits . step
400+ } ) ] ;
401+ } ) ) ) ;
402+ }
403+
404+ clampSliderValue ( input , limits ) {
405+ const value = toNumber ( input ) ;
406+ if ( ! Number . isFinite ( value ) ) {
407+ return limits . min ;
408+ }
409+ return Math . min ( limits . max , Math . max ( limits . min , value ) ) ;
410+ }
411+
284412 focusSlider ( input ) {
285413 if ( typeof input . focus === "function" ) {
286414 input . focus ( { preventScroll : true } ) ;
@@ -289,6 +417,7 @@ export class SfxControlPanel {
289417
290418 loadSound ( sound ) {
291419 this . styleProfileSelect . value = "custom" ;
420+ this . applySliderLimits ( "custom" ) ;
292421 this . attackInput . value = String ( sound . attackMs ) ;
293422 this . durationInput . value = String ( sound . durationMs ) ;
294423 this . frequencyInput . value = String ( sound . frequencyHz ) ;
@@ -305,11 +434,19 @@ export class SfxControlPanel {
305434 }
306435
307436 applyStyleProfile ( ) {
308- const profile = STYLE_PROFILES [ this . styleProfileSelect . value ] ;
437+ const styleKey = this . styleProfileSelect . value ;
438+ if ( styleKey === "custom" ) {
439+ this . applySliderLimits ( "custom" , true ) ;
440+ this . syncOutputs ( ) ;
441+ return true ;
442+ }
443+ const profile = STYLE_PROFILES [ styleKey ] ;
309444 if ( ! profile ) {
310445 this . styleProfileSelect . value = "custom" ;
446+ this . applySliderLimits ( "custom" , true ) ;
311447 return false ;
312448 }
449+ this . applySliderLimits ( styleKey , true ) ;
313450 this . attackInput . value = String ( DEFAULT_SOUND . attackMs ) ;
314451 this . durationInput . value = String ( profile . durationMs ) ;
315452 this . frequencyInput . value = String ( profile . frequencyHz ) ;
@@ -346,15 +483,15 @@ export class SfxControlPanel {
346483 if ( ! ALLOWED_WAVEFORMS . has ( this . waveformSelect . value ) ) {
347484 return { valid : false , message : `Unsupported waveform: ${ this . waveformSelect . value } .` } ;
348485 }
349- const frequency = readRange ( this . frequencyInput , SLIDER_LIMITS . frequencyHz ) ;
350- const duration = readRange ( this . durationInput , SLIDER_LIMITS . durationMs ) ;
351- const attack = readRange ( this . attackInput , SLIDER_LIMITS . attackMs ) ;
352- const release = readRange ( this . releaseInput , SLIDER_LIMITS . releaseMs ) ;
353- const volume = readRange ( this . volumeInput , SLIDER_LIMITS . volume ) ;
354- const pitchSweep = readRange ( this . pitchSweepInput , SLIDER_LIMITS . pitchSweepCents ) ;
355- const noiseAmount = readRange ( this . noiseAmountInput , SLIDER_LIMITS . noiseAmount ) ;
356- const noiseDecay = readRange ( this . noiseDecayInput , SLIDER_LIMITS . noiseDecayMs ) ;
357- const noiseFilter = readRange ( this . noiseFilterInput , SLIDER_LIMITS . noiseFilterHz ) ;
486+ const frequency = readRange ( this . frequencyInput , this . activeSliderLimits . frequencyHz ) ;
487+ const duration = readRange ( this . durationInput , this . activeSliderLimits . durationMs ) ;
488+ const attack = readRange ( this . attackInput , this . activeSliderLimits . attackMs ) ;
489+ const release = readRange ( this . releaseInput , this . activeSliderLimits . releaseMs ) ;
490+ const volume = readRange ( this . volumeInput , this . activeSliderLimits . volume ) ;
491+ const pitchSweep = readRange ( this . pitchSweepInput , this . activeSliderLimits . pitchSweepCents ) ;
492+ const noiseAmount = readRange ( this . noiseAmountInput , this . activeSliderLimits . noiseAmount ) ;
493+ const noiseDecay = readRange ( this . noiseDecayInput , this . activeSliderLimits . noiseDecayMs ) ;
494+ const noiseFilter = readRange ( this . noiseFilterInput , this . activeSliderLimits . noiseFilterHz ) ;
358495 const failed = [ frequency , duration , attack , release , volume , pitchSweep , noiseAmount , noiseDecay , noiseFilter ] . find ( ( result ) => ! result . ok ) ;
359496 if ( failed ) {
360497 return { valid : false , message : failed . message } ;
0 commit comments