-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplaymsu.kts
executable file
·201 lines (154 loc) · 5.3 KB
/
playmsu.kts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env kotlinc/bin/kotlin
import java.io.File
import java.io.RandomAccessFile
import javax.sound.sampled.AudioFormat
import javax.sound.sampled.AudioSystem
import javax.sound.sampled.SourceDataLine
val BITS_PER_BYTE = 8
val CHANNELS = 2
val LOOP_POINT_SIZE = 4
val MSU_MAGIC = "MSU1"
val MSU_MAGIC_SIZE = MSU_MAGIC.toByteArray().size
val SAMPLE_RATE = 44100
val SAMPLE_SIZE = 2
/**
* Loads from the given [file][filename] the audio surrounding its loop point and returns it.
*/
fun loadLoopAudio(filename: String): ByteArray {
val buffer = ByteArray(SAMPLE_RATE * 2)
val (file, loopPoint) = openFile(filename)
// Read the audio before the loop point
file.seek(file.length() - SAMPLE_RATE)
file.read(buffer, 0, SAMPLE_RATE)
// Read the audio after the loop point
seekSample(file, loopPoint)
file.read(buffer, SAMPLE_RATE, SAMPLE_RATE)
file.close()
return buffer
}
/**
* Opens an [audio output line][SourceDataLine] using a format that matches the MSU PCM standard:
*
* - 44100 samples per second
* - 16 bits per sample
* - two channels
* - signed
* - little-endian
*/
fun openAudio(): SourceDataLine {
val audioFormat = AudioFormat(SAMPLE_RATE.toFloat(), SAMPLE_SIZE * BITS_PER_BYTE, CHANNELS, true, false)
val sourceDataLine = AudioSystem.getSourceDataLine(audioFormat)
sourceDataLine.open()
sourceDataLine.start()
return sourceDataLine
}
/**
* Opens the file with the given [filename] and returns it along with its loop point.
*/
fun openFile(filename: String): Pair<RandomAccessFile, UInt> {
val file = RandomAccessFile(filename, "r")
readMagicNumber(file)
val loopPoint = readLoopPoint(file)
println("Loop point: $loopPoint")
return file to loopPoint
}
/**
* Loads the given [file][filename] and plays it indefinitely.
*/
fun playFile(filename: String) {
val audio = openAudio()
val buffer = ByteArray(audio.bufferSize)
val (file, loopPoint) = openFile(filename)
while (true) {
val bytesRead = file.read(buffer)
if (bytesRead == -1) {
// This shouldn't happen, because we always seek back to the loop point when we reach the end of the file,
// but just in case.
break
}
audio.write(buffer, 0, bytesRead)
if (file.filePointer >= file.length()) {
println("Looped!")
seekSample(file, loopPoint)
}
}
audio.drain()
}
/**
* Loads the given [file][filename] and indefinitely plays the bit of audio surrounding its loop point so the user can
* listen for pops or other discontinuities.
*/
fun playLoop(filename: String) {
val audio = openAudio()
val buffer = loadLoopAudio(filename)
val silence = ByteArray(SAMPLE_RATE)
while (true) {
audio.write(buffer, 0, buffer.size)
audio.write(silence, 0, silence.size)
}
}
fun printUsage() {
println(
"""
Usage:
playmsu.kts
Show this usage statement
playmsu.kts (filename.pcm)
Play the MSU PCM file with the given name, looping indefinitely
playmsu.kts (filename.pcm) -loop
Play the point of the MSU PCM file with the given name where the loop happens
playmsu.kts (filename.pcm) -writeloop (output filename)
Writes the point of the MSU PCM file with the given name where the loop happens to the given output file
Press Ctrl-C to quit.
""".trimIndent()
)
}
/**
* Reads and returns the four-byte, little-endian loop point from the given [file].
*/
fun readLoopPoint(file: RandomAccessFile): UInt {
val bytes = ByteArray(LOOP_POINT_SIZE)
file.readFully(bytes)
return (bytes[3].toUByte().toUInt() shl 24) or
(bytes[2].toUByte().toUInt() shl 16) or
(bytes[1].toUByte().toUInt() shl 8) or
bytes[0].toUByte().toUInt()
}
/**
* Reads the [magic number][MSU_MAGIC] from the given [file] and throws an exception if it isn't found.
*/
fun readMagicNumber(file: RandomAccessFile) {
val msuMagicBytes = ByteArray(MSU_MAGIC_SIZE)
file.readFully(msuMagicBytes)
val msuMagic = String(msuMagicBytes)
if (msuMagic != MSU_MAGIC) {
throw Exception("Specified file is missing the \"$MSU_MAGIC\" magic number: $msuMagic")
}
}
/**
* Seeks the given [file] to the given [sample], accounting for the size of each sample, the number of channels, and the
* MSU PCM header.
*/
fun seekSample(file: RandomAccessFile, sample: UInt) {
file.seek(MSU_MAGIC_SIZE + LOOP_POINT_SIZE + (sample.toLong() * SAMPLE_SIZE * CHANNELS))
}
/**
* Loads the given [file][filename] and writes a small bit of audio from before and after its loop point to the given
* [outputFilename]. The resulting file can be loaded in an audio editing tool and visually inspected for a
* discontinuity between the two samples at the exact middle.
*/
fun writeLoop(filename: String, outputFilename: String) {
val buffer = loadLoopAudio(filename)
File(outputFilename).outputStream().use {
it.write(buffer)
}
}
if (args.size == 1) {
playFile(args[0])
} else if (args.size == 2 && args[1] == "-loop") {
playLoop(args[0])
} else if (args.size == 3 && args[1] == "-writeloop") {
writeLoop(args[0], args[2])
} else {
printUsage()
}