99 < script src ="https://unpkg.com/wavesurfer.js@7/dist/plugins/spectrogram.min.js "> </ script >
1010 < style >
1111 body { font-family : sans-serif; margin : 20px ; }
12- # controls { margin-top : 10px ; margin-bottom : 10px ; display : none; padding-left : 0px ; }
12+ # controls { margin-top : 10px ; margin-bottom : 10px ; display : none; padding-left : 0px ; }
1313 # zipInput { margin-right : 10px ; }
14- # waveform { width : 100% ; height : 150px ; margin-top : 10px ; margin-bottom : 10px ;}
15- # spectrogram { width : 100% ; height : 150px ; margin-top : 10px ; margin-bottom : 15px ;}
16- # track-info { margin-top : 10px ; font-style : italic; color : # 444 ; font-size : 0.9em ;}
17- # currentTime { margin-top : 5px ; font-family : monospace; font-size : 2em ;}
14+ # waveform , # spectrogram { width : 100% ; height : 150px ; margin-top : 10px ; margin-bottom : 10px ; }
15+ # track-info { margin-top : 10px ; font-style : italic; color : # 444 ; font-size : 0.9em ; }
16+ # currentTime { margin-top : 5px ; font-family : monospace; font-size : 2em ; }
1817 .radio-group { margin-top : 10px ; margin-bottom : 10px ; }
19- .load-control { margin-top : 5px ; margin-bottom : 15px ;}
18+ .load-control { margin-top : 5px ; margin-bottom : 15px ; }
19+ # mobile-controls { display : none; margin-top : 10px ; gap : 10px ; justify-content : start; }
20+ @media (max-width : 768px ) { # mobile-controls { display : flex; } }
2021 </ style >
2122</ head >
2223< body >
2324< h2 > BlastViz</ h2 >
2425< h4 > Blastbeat Detection Visualizer</ h4 >
2526< div id ="explainer ">
2627 < hr >
27- < p > < i > < strong > Warning:</ strong > This is a work in progress. This visualizer expects a ZIP file containing an audio
28- file
29- (MP3 or WAV), a JSON file with the identified blast-beat sections, and optionally a drum-only audio track.</ i >
28+ < p > < i > < strong > Warning:</ strong > This is a work in progress. The visualizer expects a ZIP file containing an audio
29+ file (MP3 or WAV), a JSON file with the identified blast-beat sections, and optionally a drum-only audio track.</ i >
3030 </ p >
3131 < p > Expected JSON structure:</ p >
3232 < pre >
@@ -39,8 +39,7 @@ <h4>Blastbeat Detection Visualizer</h4>
3939 </ pre >
4040 < p > You can download some examples from < a
4141 href ="https://drive.google.com/drive/folders/1YFoxrsrBo8hl0cOYkdxCsfW_bf3CX7Al?usp=drive_link "
42- target ="_blank "> this Google
43- Drive folder</ a > </ p >
42+ target ="_blank "> this Google Drive folder</ a > </ p >
4443</ div >
4544< div class ="load-control ">
4645 < input type ="file " id ="zipInput " accept =".zip ">
@@ -56,8 +55,13 @@ <h4>Blastbeat Detection Visualizer</h4>
5655</ div >
5756< div id ="controls ">
5857 < div id ="currentTime "> 0:00:000</ div >
58+ < div id ="mobile-controls ">
59+ < button id ="playPauseBtn "> Play/Pause</ button >
60+ < button id ="zoomInBtn "> Zoom In</ button >
61+ < button id ="zoomOutBtn "> Zoom Out</ button >
62+ </ div >
5963</ div >
60- < div id ="waveform "> </ div >
64+ < div id ="waveform " title =" Space bar: play/pause -- Mouse wheel: zoom in/out " > </ div >
6165< div id ="spectrogram "> </ div >
6266< script >
6367 let wavesurfer = null
@@ -66,115 +70,127 @@ <h4>Blastbeat Detection Visualizer</h4>
6670 let zoomLevel = 0
6771
6872 function applyZoom ( ) {
69- if ( wavesurfer ) wavesurfer . zoom ( zoomLevel )
73+ if ( wavesurfer ) wavesurfer . zoom ( zoomLevel )
7074 }
7175
7276 function handleWheelZoom ( e ) {
73- if ( ! wavesurfer ) return
74- e . preventDefault ( )
75- zoomLevel += e . deltaY < 0 ? 10 : - 10
76- zoomLevel = Math . max ( 0 , Math . min ( zoomLevel , 200 ) )
77- applyZoom ( )
77+ if ( ! wavesurfer ) return
78+ e . preventDefault ( )
79+ zoomLevel += e . deltaY < 0 ? 10 : - 10
80+ zoomLevel = Math . max ( 0 , Math . min ( zoomLevel , 200 ) )
81+ applyZoom ( )
7882 }
7983
8084 function initWaveSurfer ( audioFile , intervals ) {
81- if ( wavesurfer ) {
82- wavesurfer . destroy ( )
83- document . getElementById ( 'waveform' ) . innerHTML = ""
84- document . getElementById ( 'spectrogram' ) . innerHTML = ""
85- document . getElementById ( 'currentTime' ) . textContent = "0:00:000"
86- }
87- const regions = WaveSurfer . Regions . create ( )
88- const spectrogram = WaveSurfer . Spectrogram . create ( {
89- container : '#spectrogram' ,
90- labels : true ,
91- frequencyMax : 500 ,
92- scale : "mel" ,
93- rangeDB : 60 ,
94- gainDB : 20 ,
95- windowFunc : 'hann' ,
96- colorMap : 'roseus' ,
97- fftSamples : 256
98- } )
99- wavesurfer = WaveSurfer . create ( {
100- container : '#waveform' ,
101- waveColor : '#000' ,
102- progressColor : '#888' ,
103- cursorColor : '#f00' ,
104- height : 150 ,
105- plugins : [ regions , spectrogram ]
106- } )
107- wavesurfer . load ( audioFile )
108- wavesurfer . on ( 'ready' , ( ) => {
109- document . getElementById ( 'explainer' ) . style . display = 'none'
110- audioReady = true
111- document . getElementById ( 'controls' ) . style . display = 'block'
112- intervals . forEach ( ( { start_time, end_time } ) => {
113- regions . addRegion ( {
114- start : start_time ,
115- end : end_time ,
116- color : 'rgba(2,247,7,0.3)' ,
117- drag : false ,
118- resize : false
119- } )
85+ if ( wavesurfer ) {
86+ wavesurfer . destroy ( )
87+ document . getElementById ( 'waveform' ) . innerHTML = ""
88+ document . getElementById ( 'spectrogram' ) . innerHTML = ""
89+ document . getElementById ( 'currentTime' ) . textContent = "0:00:000"
90+ }
91+ const regions = WaveSurfer . Regions . create ( )
92+ const spectrogram = WaveSurfer . Spectrogram . create ( {
93+ container : '#spectrogram' ,
94+ labels : true ,
95+ frequencyMax : 500 ,
96+ scale : "mel" ,
97+ rangeDB : 60 ,
98+ gainDB : 20 ,
99+ windowFunc : 'hann' ,
100+ colorMap : 'roseus' ,
101+ fftSamples : 256
102+ } )
103+ wavesurfer = WaveSurfer . create ( {
104+ container : '#waveform' ,
105+ waveColor : '#000' ,
106+ progressColor : '#888' ,
107+ cursorColor : '#f00' ,
108+ height : 150 ,
109+ plugins : [ regions , spectrogram ]
110+ } )
111+ wavesurfer . load ( audioFile )
112+ wavesurfer . on ( 'ready' , ( ) => {
113+ document . getElementById ( 'explainer' ) . style . display = 'none'
114+ audioReady = true
115+ document . getElementById ( 'controls' ) . style . display = 'block'
116+ intervals . forEach ( ( { start_time, end_time } ) => {
117+ regions . addRegion ( {
118+ start : start_time ,
119+ end : end_time ,
120+ color : 'rgba(2,247,7,0.3)' ,
121+ drag : false ,
122+ resize : false
123+ } )
124+ } )
120125 } )
121- } )
122- wavesurfer . on ( 'timeupdate' , t => {
123- const m = Math . floor ( t / 60 )
124- const s = Math . floor ( t % 60 )
125- const ms = Math . floor ( ( t % 1 ) * 1000 )
126- document . getElementById ( 'currentTime' ) . textContent = `${ m } :${ s . toString ( ) . padStart ( 2 , '0' ) } :${ ms . toString ( ) . padStart ( 3 , '0' ) } `
127- } )
128- document . getElementById ( 'waveform' ) . addEventListener ( 'wheel' , handleWheelZoom , { passive : false } )
129- document . getElementById ( 'spectrogram' ) . addEventListener ( 'wheel' , handleWheelZoom , { passive : false } )
126+ wavesurfer . on ( 'timeupdate' , t => {
127+ const m = Math . floor ( t / 60 )
128+ const s = Math . floor ( t % 60 )
129+ const ms = Math . floor ( ( t % 1 ) * 1000 )
130+ document . getElementById ( 'currentTime' ) . textContent = `${ m } :${ s . toString ( ) . padStart ( 2 , '0' ) } :${ ms . toString ( ) . padStart ( 3 , '0' ) } `
131+ } )
132+ document . getElementById ( 'waveform' ) . addEventListener ( 'wheel' , handleWheelZoom , { passive : false } )
133+ document . getElementById ( 'spectrogram' ) . addEventListener ( 'wheel' , handleWheelZoom , { passive : false } )
130134 }
131135
132136 async function loadZip ( zipFile ) {
133- const zip = await JSZip . loadAsync ( zipFile )
134- const jsonFileName = Object . keys ( zip . files ) . find ( n => n . endsWith ( '.json' ) )
135- if ( ! jsonFileName ) return alert ( "No JSON found in zip" )
136- const jsonData = await zip . files [ jsonFileName ] . async ( 'string' )
137- const config = JSON . parse ( jsonData )
138- const intervals = config . blast_beats || [ ]
139- const trackType = document . querySelector ( 'input[name="trackType"]:checked' ) . value
140- let audioFileName = null
141- if ( trackType === "drums" ) {
142- audioFileName = Object . keys ( zip . files ) . find ( n => / _ d r u m s \. ( m p 3 | w a v ) $ / i. test ( n ) )
143- if ( ! audioFileName ) return alert ( "Drums-only file (_drums) not found in zip" )
137+ const zip = await JSZip . loadAsync ( zipFile )
138+ const jsonFileName = Object . keys ( zip . files ) . find ( n => n . endsWith ( '.json' ) )
139+ if ( ! jsonFileName ) return alert ( "No JSON found in zip" )
140+ const jsonData = await zip . files [ jsonFileName ] . async ( 'string' )
141+ const config = JSON . parse ( jsonData )
142+ const intervals = config . blast_beats || [ ]
143+ const trackType = document . querySelector ( 'input[name="trackType"]:checked' ) . value
144+ let audioFileName = null
145+ if ( trackType === "drums" ) {
146+ audioFileName = Object . keys ( zip . files ) . find ( n => / _ d r u m s \. ( m p 3 | w a v ) $ / i. test ( n ) )
147+ if ( ! audioFileName ) return alert ( "Drums-only file (_drums) not found in zip" )
144148 } else {
145- audioFileName = Object . keys ( zip . files ) . find ( n => / \. ( m p 3 | w a v ) $ / i. test ( n ) && ! / _ d r u m s \. / i. test ( n ) )
149+ audioFileName = Object . keys ( zip . files ) . find ( n => / \. ( m p 3 | w a v ) $ / i. test ( n ) && ! / _ d r u m s \. / i. test ( n ) )
146150 }
147- console . log ( trackType )
148- console . log ( "Selected audio file:" , audioFileName )
149- if ( ! audioFileName ) return alert ( "No audio file found in zip" )
150- const audioBlob = await zip . files [ audioFileName ] . async ( 'blob' )
151- const audioURL = URL . createObjectURL ( audioBlob )
152- document . getElementById ( 'audioPath' ) . textContent = "Track: " + audioFileName
153- if ( config . snare_frequency !== undefined )
154- document . getElementById ( 'snareFreq' ) . textContent = "| Snare drum freq: " + config . snare_frequency . toFixed ( 2 ) + " Hz"
155- if ( config . bass_drum_frequency !== undefined )
156- document . getElementById ( 'bassdrumFreq' ) . textContent = "| Bass drum freq: " + config . bass_drum_frequency . toFixed ( 2 ) + " Hz"
157- initWaveSurfer ( audioURL , intervals )
151+ if ( ! audioFileName ) return alert ( "No audio file found in zip" )
152+ const audioBlob = await zip . files [ audioFileName ] . async ( 'blob' )
153+ const audioURL = URL . createObjectURL ( audioBlob )
154+ document . getElementById ( 'audioPath' ) . textContent = "Track: " + audioFileName
155+ if ( config . snare_frequency !== undefined )
156+ document . getElementById ( 'snareFreq' ) . textContent = "| Snare drum freq: " + config . snare_frequency . toFixed ( 2 ) + " Hz"
157+ if ( config . bass_drum_frequency !== undefined )
158+ document . getElementById ( 'bassdrumFreq' ) . textContent = "| Bass drum freq: " + config . bass_drum_frequency . toFixed ( 2 ) + " Hz"
159+ initWaveSurfer ( audioURL , intervals )
158160 }
159161
160162 document . getElementById ( 'zipInput' ) . addEventListener ( 'change' , e => {
161- const file = e . target . files [ 0 ]
162- if ( ! file ) return
163- currentZipFile = file
164- loadZip ( file )
163+ const file = e . target . files [ 0 ]
164+ if ( ! file ) return
165+ currentZipFile = file
166+ loadZip ( file )
165167 } )
166168
167169 document . querySelectorAll ( 'input[name="trackType"]' ) . forEach ( r => {
168- r . addEventListener ( 'change' , ( ) => {
169- if ( currentZipFile ) loadZip ( currentZipFile )
170- } )
170+ r . addEventListener ( 'change' , ( ) => {
171+ if ( currentZipFile ) loadZip ( currentZipFile )
172+ } )
171173 } )
172174
173175 document . addEventListener ( 'keydown' , e => {
174- if ( e . code === 'Space' && audioReady ) {
175- e . preventDefault ( )
176- if ( wavesurfer ) wavesurfer . playPause ( )
177- }
176+ if ( e . code === 'Space' && audioReady ) {
177+ e . preventDefault ( )
178+ if ( wavesurfer ) wavesurfer . playPause ( )
179+ }
180+ } )
181+
182+ document . getElementById ( 'playPauseBtn' ) . addEventListener ( 'click' , ( ) => {
183+ if ( audioReady && wavesurfer ) wavesurfer . playPause ( )
184+ } )
185+
186+ document . getElementById ( 'zoomInBtn' ) . addEventListener ( 'click' , ( ) => {
187+ zoomLevel = Math . min ( 200 , zoomLevel + 10 )
188+ applyZoom ( )
189+ } )
190+
191+ document . getElementById ( 'zoomOutBtn' ) . addEventListener ( 'click' , ( ) => {
192+ zoomLevel = Math . max ( 0 , zoomLevel - 10 )
193+ applyZoom ( )
178194 } )
179195</ script >
180196</ body >
0 commit comments