@@ -7,6 +7,7 @@ <h1>Yamaha OP* Web Gateway</h1>
7
7
< p > This webpage lets you submit commands to a real Yamaha synthesizer and listen to the output. The synthesizer is a < b > shared resource</ b > (and isn't very fast, although it does not have to run in real time), and everyone who submits a file is in the same queue, so you might have to wait until it's your turn.</ p >
8
8
< p > Supported chips: {{chips}}.</ p >
9
9
< p > Play a < a href ="https://vgmrips.net/packs/chips "> VGM/VGZ file</ a > : < input type ="file " id ="file " accept =".vgm,.vgz "> < input type ="button " id ="play " value ="Play "> < input type ="button " id ="replay " value ="Replay " disabled > < input type ="checkbox " id ="loop "> < label for ="loop "> Loop</ label > </ p >
10
+ < p > Export PCM: < input type ="button " id ="exportFull " value ="Export full "> < input type ="button " id ="exportLoop " value ="Export loop "> </ p >
10
11
< p > Status: < span id ="chipStatus "> no chip</ span > , < span id ="netStatus "> idle</ span > , < span id ="playStatus "> stopped</ span > .</ p >
11
12
< p id ="errorPane " style ="color:red; display:none "> Error: < span id ="error "> </ span > </ p >
12
13
@@ -27,6 +28,7 @@ <h1>Yamaha OP* Web Gateway</h1>
27
28
28
29
this . _context = new ( window . AudioContext || window . webkitAudioContext ) ;
29
30
this . _buffers = [ ] ;
31
+ this . _buffersRaw = [ ] ;
30
32
31
33
this . _queueStart = 0 ;
32
34
this . _queueEnd = 0 ;
@@ -43,12 +45,34 @@ <h1>Yamaha OP* Web Gateway</h1>
43
45
dataView . byteLength / 2 , sampleRate ) ;
44
46
var f32Buffer = audioBuffer . getChannelData ( 0 ) ;
45
47
for ( var i = 0 ; i < f32Buffer . length ; i ++ )
46
- f32Buffer [ i ] = ( dataView . getUint16 ( i * 2 , /*littleEndian=*/ true ) - 32768 ) / 32768 ;
48
+ f32Buffer [ i ] = dataView . getInt16 ( i * 2 , /*littleEndian=*/ true ) / 32768 ;
47
49
48
50
this . _buffers . push ( audioBuffer ) ;
51
+ this . _buffersRaw . push ( dataView . buffer ) ;
49
52
this . _updateStatus ( ) ;
50
53
}
51
54
55
+ this . getAllSamples = function ( ) {
56
+ return this . _buffersRaw ;
57
+ }
58
+
59
+ this . getLoopSamples = function ( ) {
60
+ var buffers = [ ] ;
61
+ var skipTo = this . loopSkipTo * 2 ;
62
+ var sourcePos = 0 ;
63
+ for ( var i = 0 ; i < this . _buffersRaw . length ; i ++ ) {
64
+ if ( skipTo >= sourcePos + this . _buffersRaw [ i ] . byteLength ) {
65
+ // skip
66
+ } else if ( skipTo >= sourcePos ) {
67
+ buffers . push ( this . _buffersRaw [ i ] . slice ( skipTo - sourcePos ) ) ;
68
+ } else {
69
+ buffers . push ( this . _buffersRaw [ i ] ) ;
70
+ }
71
+ sourcePos += this . _buffersRaw [ i ] . byteLength ;
72
+ }
73
+ return buffers ;
74
+ }
75
+
52
76
this . scheduleAtLeast = function ( atLeast ) {
53
77
if ( this . _buffers . length - this . _queueEnd < atLeast ) {
54
78
console . log ( "need at least" , atLeast , "buffers;" ,
@@ -143,8 +167,85 @@ <h1>Yamaha OP* Web Gateway</h1>
143
167
}
144
168
}
145
169
170
+ function makeWAVFile ( fileName , numOfChannels , sampleRate , bytesPerSample , sampleBuffers ) {
171
+ var totalSampleBytes = 0 ;
172
+ for ( var i = 0 ; i < sampleBuffers . length ; i ++ )
173
+ totalSampleBytes += sampleBuffers [ i ] . byteLength ;
174
+
175
+ var id_length = 8 ,
176
+ fmt_subchunk_length = id_length + 16 ,
177
+ data_subchunk_length = id_length + totalSampleBytes ,
178
+ RIFF_header_length = id_length + 4 ,
179
+ RIFF_chunk_length = RIFF_header_length + fmt_subchunk_length + data_subchunk_length ;
180
+
181
+ var header = new ArrayBuffer ( RIFF_header_length + fmt_subchunk_length + id_length ) ;
182
+ var headerView = new DataView ( header , 0 , header . length ) ;
183
+
184
+ // "RIFF" chunk
185
+ // ChunkID
186
+ headerView . setUint32 ( 0 , /*"RIFF"*/ 0x52494646 ,
187
+ /*littleEndian=*/ false ) ;
188
+ // ChunkSize
189
+ headerView . setUint32 ( 4 , RIFF_chunk_length - id_length ,
190
+ /*littleEndian=*/ true ) ;
191
+ // Format
192
+ headerView . setUint32 ( 8 , /*"WAVE"*/ 0x57415645 ,
193
+ /*littleEndian=*/ false ) ;
194
+
195
+ // "fmt " subchunk
196
+ // Subchunk1ID
197
+ headerView . setUint32 ( 12 , /*"fmt "*/ 0x666d7420 ,
198
+ /*littleEndian=*/ false ) ;
199
+ // Subchunk1Size
200
+ headerView . setUint32 ( 16 , fmt_subchunk_length - id_length ,
201
+ /*littleEndian=*/ true ) ;
202
+ // AudioFormat
203
+ headerView . setUint16 ( 20 , /*PCM*/ 1 ,
204
+ /*littleEndian=*/ true ) ;
205
+ // NumChannels
206
+ headerView . setUint16 ( 22 , numOfChannels ,
207
+ /*littleEndian=*/ true ) ;
208
+ // SampleRate
209
+ headerView . setUint32 ( 24 , sampleRate ,
210
+ /*littleEndian=*/ true ) ;
211
+ // ByteRate
212
+ headerView . setUint32 ( 28 , sampleRate * numOfChannels * bytesPerSample ,
213
+ /*littleEndian=*/ true ) ;
214
+ // BlockAlign
215
+ headerView . setUint16 ( 32 , numOfChannels * bytesPerSample ,
216
+ /*littleEndian=*/ true ) ;
217
+ // BitsPerSample
218
+ headerView . setUint16 ( 34 , bytesPerSample * 8 ,
219
+ /*littleEndian=*/ true ) ;
220
+
221
+ // "data" subchunk
222
+ // Subchunk1ID
223
+ headerView . setUint32 ( 36 , /*"data"*/ 0x64617461 ,
224
+ /*littleEndian=*/ false ) ;
225
+ // Subchunk1Size
226
+ headerView . setUint32 ( 40 , totalSampleBytes ,
227
+ /*littleEndian=*/ true ) ;
228
+
229
+ return new File ( [ header ] . concat ( sampleBuffers ) , fileName , { type : "audio/wav" } ) ;
230
+ }
231
+
232
+ function downloadFile ( file ) {
233
+ if ( window . fileUrl )
234
+ URL . revokeObjectURL ( window . fileUrl ) ;
235
+ window . fileUrl = URL . createObjectURL ( file ) ;
236
+
237
+ var a = window . document . createElement ( 'a' ) ;
238
+ a . href = window . fileUrl ;
239
+ a . download = file . name ;
240
+ document . body . appendChild ( a ) ;
241
+ a . click ( ) ;
242
+ document . body . removeChild ( a ) ;
243
+ }
244
+
146
245
var playButton = document . getElementById ( "play" ) ;
147
246
var replayButton = document . getElementById ( "replay" ) ;
247
+ var exportFullButton = document . getElementById ( "exportFull" ) ;
248
+ var exportLoopButton = document . getElementById ( "exportLoop" ) ;
148
249
var loopCheckbox = document . getElementById ( "loop" ) ;
149
250
var fileInput = document . getElementById ( "file" ) ;
150
251
var netStatusSpan = document . getElementById ( "netStatus" ) ;
@@ -155,6 +256,8 @@ <h1>Yamaha OP* Web Gateway</h1>
155
256
playButton . onclick = function ( event ) {
156
257
playButton . disabled = true ;
157
258
replayButton . disabled = true ;
259
+ exportFullButton . disabled = true ;
260
+ exportLoopButton . disabled = true ;
158
261
159
262
var player = new PCMPlayer ( ) ;
160
263
window . player = player ;
@@ -189,6 +292,7 @@ <h1>Yamaha OP* Web Gateway</h1>
189
292
var xhr = new XMLHttpRequest ( ) ;
190
293
xhr . open ( "POST" , "vgm" , /*async=*/ true ) ;
191
294
xhr . setRequestHeader ( "X-Preferred-Sample-Rate" , player . preferredSampleRate ( ) ) ;
295
+ var sampleRate = 0 ;
192
296
var seenBytes = 0 ;
193
297
var totalSamples = 0 ;
194
298
var seenSamples = 0 ;
@@ -203,6 +307,7 @@ <h1>Yamaha OP* Web Gateway</h1>
203
307
errorPane . style . display = "none" ;
204
308
205
309
if ( totalSamples == 0 ) {
310
+ sampleRate = parseInt ( xhr . getResponseHeader ( "X-Sample-Rate" ) ) ;
206
311
totalSamples = parseInt ( xhr . getResponseHeader ( "X-Total-Samples" ) ) ;
207
312
player . loopSkipTo = parseInt ( xhr . getResponseHeader ( "X-Loop-Skip-To" ) ) ;
208
313
chipStatusSpan . innerText = "chip " + xhr . getResponseHeader ( "X-Chip" ) ;
@@ -217,14 +322,17 @@ <h1>Yamaha OP* Web Gateway</h1>
217
322
viewLength = bytes . buffer . byteLength ;
218
323
var view = new DataView ( bytes . buffer , 0 , viewLength ) ;
219
324
seenSamples += view . byteLength / 2 ;
220
- player . addSamples ( view , parseInt ( xhr . getResponseHeader ( "X-Sample-Rate" ) ) ) ;
325
+ player . addSamples ( view , sampleRate ) ;
221
326
if ( ! player . _playing )
222
327
player . scheduleAtLeast ( BUFFER_AT_LEAST ) ;
223
328
seenBytes += CHUNK_SIZE ;
224
329
}
225
330
226
331
if ( xhr . readyState == 4 ) {
227
332
player . complete = true ;
333
+ exportFullButton . disabled = false ;
334
+ if ( player . loopSkipTo > 0 )
335
+ exportLoopButton . disabled = false ;
228
336
}
229
337
}
230
338
@@ -239,6 +347,18 @@ <h1>Yamaha OP* Web Gateway</h1>
239
347
}
240
348
netStatusSpan . innerText = "waiting" ;
241
349
xhr . send ( fileInput . files [ 0 ] ) ;
350
+
351
+ exportFullButton . onclick = function ( event ) {
352
+ var fileName = fileInput . files [ 0 ] . name . replace ( / \. v g [ m z ] $ / i, "" ) + ".wav" ;
353
+ downloadFile ( makeWAVFile ( fileName , /*numOfChannels=*/ 1 , sampleRate , /*bytesPerSample=*/ 2 ,
354
+ player . getAllSamples ( ) ) ) ;
355
+ }
356
+
357
+ exportLoopButton . onclick = function ( event ) {
358
+ var fileName = fileInput . files [ 0 ] . name . replace ( / \. v g [ m z ] $ / i, "" ) + " (loop).wav" ;
359
+ downloadFile ( makeWAVFile ( fileName , /*numOfChannels=*/ 1 , sampleRate , /*bytesPerSample=*/ 2 ,
360
+ player . getLoopSamples ( ) ) ) ;
361
+ }
242
362
} ;
243
363
</ script >
244
364
< script type ="text/javascript ">
0 commit comments