-
Notifications
You must be signed in to change notification settings - Fork 425
/
Audio.scala
197 lines (169 loc) · 6.99 KB
/
Audio.scala
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
package li.cil.oc.util
import java.nio.ByteBuffer
import li.cil.oc.OpenComputers
import li.cil.oc.Settings
import net.minecraft.client.Minecraft
import net.minecraft.client.audio.PositionedSoundRecord
import net.minecraft.init.SoundEvents
import net.minecraft.util.ResourceLocation
import net.minecraft.util.SoundCategory
import net.minecraft.util.SoundEvent
import net.minecraft.util.math.BlockPos
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent
import org.lwjgl.BufferUtils
import org.lwjgl.openal.AL
import org.lwjgl.openal.AL10
import org.lwjgl.openal.OpenALException
import scala.collection.mutable
/**
* This class contains the logic used by computers' internal "speakers".
* It can generate square waves with a specific frequency and duration
* and will play them through OpenAL, acquiring sources as necessary.
* Tones that have finished playing are disposed automatically in the
* tick handler.
*/
object Audio {
private def sampleRate = Settings.get.beepSampleRate
private def amplitude = Settings.get.beepAmplitude
private def maxDistance = Settings.get.beepRadius
private val sources = mutable.Set.empty[Source]
private def volume = Minecraft.getMinecraft.gameSettings.getSoundLevel(SoundCategory.BLOCKS)
private var disableAudio = false
def play(x: Float, y: Float, z: Float, frequencyInHz: Int, durationInMilliseconds: Int): Unit = {
play(x, y, z, ".", frequencyInHz, durationInMilliseconds)
}
def play(x: Float, y: Float, z: Float, pattern: String, frequencyInHz: Int = 1000, durationInMilliseconds: Int = 200): Unit = {
val mc = Minecraft.getMinecraft
val distanceBasedGain = math.max(0, 1 - mc.player.getDistance(x, y, z) / maxDistance).toFloat
val gain = distanceBasedGain * volume
if (gain <= 0 || amplitude <= 0) return
if (disableAudio) {
// Fallback audio generation, using built-in Minecraft sound. This can be
// necessary on certain systems with audio cards that do not have enough
// memory. May still fail, but at least we can say we tried!
// Valid range is 20-2000Hz, clamp it to that and get a relative value.
// MC's pitch system supports a minimum pitch of 0.5, however, so up it
// by that.
val clampedFrequency = ((frequencyInHz - 20) max 0 min 1980) / 1980f + 0.5f
var delay = 0
for (ch <- pattern) {
val record = new PositionedSoundRecord(SoundEvents.BLOCK_NOTE_HARP, SoundCategory.BLOCKS, gain, clampedFrequency, new BlockPos(x, y, z))
if (delay == 0) mc.getSoundHandler.playSound(record)
else mc.getSoundHandler.playDelayedSound(record, delay)
delay += ((if (ch == '.') durationInMilliseconds else 2 * durationInMilliseconds) * 20 / 1000) max 1
}
}
else {
if (AL.isCreated) {
val sampleCounts = pattern.toCharArray.
map(ch => if (ch == '.') durationInMilliseconds else 2 * durationInMilliseconds).
map(_ * sampleRate / 1000)
// 50ms pause between pattern parts.
val pauseSampleCount = 50 * sampleRate / 1000
val data = BufferUtils.createByteBuffer(sampleCounts.sum + (sampleCounts.length - 1) * pauseSampleCount)
val step = frequencyInHz / sampleRate.toFloat
var offset = 0f
for (sampleCount <- sampleCounts) {
for (sample <- 0 until sampleCount) {
val angle = 2 * math.Pi * offset
val value = (math.signum(math.sin(angle)) * amplitude).toByte ^ 0x80
offset += step
if (offset > 1) offset -= 1
data.put(value.toByte)
}
if (data.hasRemaining) {
for (sample <- 0 until pauseSampleCount) {
data.put(127: Byte)
}
}
}
data.rewind()
// Watch out for sound cards running out of memory... this apparently
// really does happen. I'm assuming this is due to too many sounds being
// kept loaded, since from what I can see OC's releasing its audio
// memory as it should.
try sources.synchronized(sources += new Source(x, y, z, data, gain)) catch {
case e: LessUselessOpenALException =>
if (e.errorCode == AL10.AL_OUT_OF_MEMORY) {
// Well... let's just stop here.
OpenComputers.log.info("Couldn't play computer speaker sound because your sound card ran out of memory. Either your sound card is just really low-end, or there are just too many sounds in use already by other mods. Disabling computer speakers to avoid spamming your log file now.")
disableAudio = true
}
else {
OpenComputers.log.warn("Error playing computer speaker sound.", e)
}
}
}
}
}
def update() {
if (!disableAudio) {
sources.synchronized(sources --= sources.filter(_.checkFinished))
// Clear error stack.
if (AL.isCreated) {
try AL10.alGetError() catch {
case _: UnsatisfiedLinkError =>
OpenComputers.log.warn("Negotiations with OpenAL broke down, disabling sounds.")
disableAudio = true
}
}
}
}
private class Source(val x: Float, y: Float, z: Float, val data: ByteBuffer, val gain: Float) {
// Clear error stack.
AL10.alGetError()
val (source, buffer) = {
val buffer = AL10.alGenBuffers()
checkALError()
try {
AL10.alBufferData(buffer, AL10.AL_FORMAT_MONO8, data, sampleRate)
checkALError()
val source = AL10.alGenSources()
checkALError()
try {
AL10.alSourceQueueBuffers(source, buffer)
checkALError()
AL10.alSource3f(source, AL10.AL_POSITION, x, y, z)
AL10.alSourcef(source, AL10.AL_REFERENCE_DISTANCE, maxDistance)
AL10.alSourcef(source, AL10.AL_MAX_DISTANCE, maxDistance)
AL10.alSourcef(source, AL10.AL_GAIN, gain * 0.3f)
checkALError()
AL10.alSourcePlay(source)
checkALError()
(source, buffer)
}
catch {
case t: Throwable =>
AL10.alDeleteSources(source)
throw t
}
}
catch {
case t: Throwable =>
AL10.alDeleteBuffers(buffer)
throw t
}
}
def checkFinished = AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING && {
AL10.alDeleteSources(source)
AL10.alDeleteBuffers(buffer)
true
}
}
// Having the error code in an accessible way is really cool, you know.
class LessUselessOpenALException(val errorCode: Int) extends OpenALException(errorCode)
// Custom implementation of Util.checkALError() that uses our custom exception.
def checkALError(): Unit = {
val errorCode = AL10.alGetError()
if (errorCode != AL10.AL_NO_ERROR) {
throw new LessUselessOpenALException(errorCode)
}
}
MinecraftForge.EVENT_BUS.register(this)
@SubscribeEvent
def onTick(e: ClientTickEvent) {
update()
}
}