aboutsummaryrefslogtreecommitdiff
path: root/src/library_openal.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/library_openal.js')
-rw-r--r--src/library_openal.js505
1 files changed, 467 insertions, 38 deletions
diff --git a/src/library_openal.js b/src/library_openal.js
index eb152f62..bdbb2dca 100644
--- a/src/library_openal.js
+++ b/src/library_openal.js
@@ -5,6 +5,10 @@ var LibraryOpenAL = {
$AL: {
contexts: [],
currentContext: null,
+
+ stringCache: {},
+ alcStringCache: {},
+
QUEUE_INTERVAL: 25,
QUEUE_LOOKAHEAD: 100,
@@ -170,8 +174,8 @@ var LibraryOpenAL = {
},
alcOpenDevice: function(deviceName) {
- if (typeof(AudioContext) == "function" ||
- typeof(webkitAudioContext) == "function") {
+ if (typeof(AudioContext) !== "undefined" ||
+ typeof(webkitAudioContext) !== "undefined") {
return 1; // non-null pointer -- we just simulate one device
} else {
return 0;
@@ -201,12 +205,15 @@ var LibraryOpenAL = {
}
if (ctx) {
+ var gain = ctx.createGain();
+ gain.connect(ctx.destination);
var context = {
ctx: ctx,
err: 0,
src: [],
buf: [],
- interval: setInterval(function() { AL.updateSources(context); }, AL.QUEUE_INTERVAL)
+ interval: setInterval(function() { AL.updateSources(context); }, AL.QUEUE_INTERVAL),
+ gain: gain
};
AL.contexts.push(context);
return AL.contexts.length;
@@ -232,6 +239,42 @@ var LibraryOpenAL = {
return _alGetError();
},
+ alcGetIntegerv: function(device, param, size, data) {
+ if (size == 0 || !data) {
+ AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */;
+ return;
+ }
+
+ switch(param) {
+ case 0x1000 /* ALC_MAJOR_VERSION */:
+ {{{ makeSetValue('data', '0', '1', 'i32') }}};
+ break;
+ case 0x1001 /* ALC_MINOR_VERSION */:
+ {{{ makeSetValue('data', '0', '1', 'i32') }}};
+ break;
+ case 0x1002 /* ALC_ATTRIBUTES_SIZE */:
+ if (!device) {
+ AL.currentContext.err = 0xA001 /* ALC_INVALID_DEVICE */;
+ return 0;
+ }
+ {{{ makeSetValue('data', '0', '1', 'i32') }}};
+ break;
+ case 0x1003 /* ALC_ALL_ATTRIBUTES */:
+ if (!device) {
+ AL.currentContext.err = 0xA001 /* ALC_INVALID_DEVICE */;
+ return 0;
+ }
+ {{{ makeSetValue('data', '0', '0', 'i32') }}};
+ break;
+ default:
+#if OPENAL_DEBUG
+ console.log("alcGetIntegerv with param " + param + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA003 /* ALC_INVALID_ENUM */;
+ break;
+ }
+ },
+
alDeleteSources: function(count, sources) {
if (!AL.currentContext) {
#if OPENAL_DEBUG
@@ -254,7 +297,7 @@ var LibraryOpenAL = {
}
for (var i = 0; i < count; ++i) {
var gain = AL.currentContext.ctx.createGain();
- gain.connect(AL.currentContext.ctx.destination);
+ gain.connect(AL.currentContext.gain);
AL.currentContext.src.push({
state: 0x1011 /* AL_INITIAL */,
queue: [],
@@ -294,6 +337,34 @@ var LibraryOpenAL = {
this._velocity = val;
if (this.panner) this.panner.setVelocity(val[0], val[1], val[2]);
},
+ get direction() {
+ return this._direction || [0, 0, 0];
+ },
+ set direction(val) {
+ this._direction = val;
+ if (this.panner) this.panner.setOrientation(val[0], val[1], val[2]);
+ },
+ get coneOuterGain() {
+ return this._coneOuterGain || 0.0;
+ },
+ set coneOuterGain(val) {
+ this._coneOuterGain = val;
+ if (this.panner) this.panner.coneOuterGain = val;
+ },
+ get coneInnerAngle() {
+ return this._coneInnerAngle || 360.0;
+ },
+ set coneInnerAngle(val) {
+ this._coneInnerAngle = val;
+ if (this.panner) this.panner.coneInnerAngle = val;
+ },
+ get coneOuterAngle() {
+ return this._coneOuterAngle || 360.0;
+ },
+ set coneOuterAngle(val) {
+ this._coneOuterAngle = val;
+ if (this.panner) this.panner.coneOuterAngle = val;
+ },
gain: gain,
panner: null,
buffersPlayed: 0,
@@ -303,6 +374,18 @@ var LibraryOpenAL = {
}
},
+ alIsSource: function(sourceId) {
+ if (!AL.currentContext) {
+ return false;
+ }
+
+ if (!AL.currentContext.src[sourceId - 1]) {
+ return false;
+ } else {
+ return true;
+ }
+ },
+
alSourcei__deps: ['updateSource'],
alSourcei: function(source, param, value) {
if (!AL.currentContext) {
@@ -320,6 +403,12 @@ var LibraryOpenAL = {
return;
}
switch (param) {
+ case 0x1001 /* AL_CONE_INNER_ANGLE */:
+ src.coneInnerAngle = value;
+ break;
+ case 0x1002 /* AL_CONE_OUTER_ANGLE */:
+ src.coneOuterAngle = value;
+ break;
case 0x1007 /* AL_LOOPING */:
src.loop = (value === 1 /* AL_TRUE */);
break;
@@ -406,12 +495,15 @@ var LibraryOpenAL = {
case 0x1021 /* AL_ROLLOFF_FACTOR */:
src.rolloffFactor = value;
break;
- // case 0x1022 /* AL_CONE_OUTER_GAIN */:
- // break;
- // case 0x1001 /* AL_CONE_INNER_ANGLE */:
- // break;
- // case 0x1002 /* AL_CONE_OUTER_ANGLE */:
- // break;
+ case 0x1022 /* AL_CONE_OUTER_GAIN */:
+ src.coneOuterGain = value;
+ break;
+ case 0x1001 /* AL_CONE_INNER_ANGLE */:
+ src.coneInnerAngle = value;
+ break;
+ case 0x1002 /* AL_CONE_OUTER_ANGLE */:
+ src.coneOuterAngle = value;
+ break;
case 0x1020 /* AL_REFERENCE_DISTANCE */:
src.refDistance = value;
break;
@@ -443,6 +535,9 @@ var LibraryOpenAL = {
case 0x1004 /* AL_POSITION */:
src.position = [v1, v2, v3];
break;
+ case 0x1005 /* AL_DIRECTION */:
+ src.direction = [v1, v2, v3];
+ break;
case 0x1006 /* AL_VELOCITY */:
src.velocity = [v1, v2, v3];
break;
@@ -594,6 +689,21 @@ var LibraryOpenAL = {
}
},
+ alIsBuffer: function(bufferId) {
+ if (!AL.currentContext) {
+ return false;
+ }
+ if (bufferId > AL.currentContext.buf.length) {
+ return false;
+ }
+
+ if (!AL.currentContext.buf[bufferId - 1]) {
+ return false;
+ } else {
+ return true;
+ }
+ },
+
alBufferData: function(buffer, format, data, size, freq) {
if (!AL.currentContext) {
#if OPENAL_DEBUG
@@ -633,6 +743,7 @@ var LibraryOpenAL = {
}
try {
AL.currentContext.buf[buffer - 1] = AL.currentContext.ctx.createBuffer(channels, size / (bytes * channels), freq);
+ AL.currentContext.buf[buffer - 1].bytesPerSample = bytes;
} catch (e) {
AL.currentContext.err = 0xA003 /* AL_INVALID_VALUE */;
return;
@@ -657,6 +768,41 @@ var LibraryOpenAL = {
}
},
+ alGetBufferi: function(buffer, param, value)
+ {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alGetBufferi called without a valid context");
+#endif
+ return;
+ }
+ var buf = AL.currentContext.buf[buffer - 1];
+ if (!buf) {
+#if OPENAL_DEBUG
+ console.error("alGetBufferi called with an invalid buffer");
+#endif
+ AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */;
+ return;
+ }
+ switch (param) {
+ case 0x2001 /* AL_FREQUENCY */:
+ {{{ makeSetValue('value', '0', 'buf.sampleRate', 'i32') }}};
+ break;
+ case 0x2002 /* AL_BITS */:
+ {{{ makeSetValue('value', '0', 'buf.bytesPerSample * 8', 'i32') }}};
+ break;
+ case 0x2003 /* AL_CHANNELS */:
+ {{{ makeSetValue('value', '0', 'buf.numberOfChannels', 'i32') }}};
+ break;
+ case 0x2004 /* AL_SIZE */:
+ {{{ makeSetValue('value', '0', 'buf.length * buf.bytesPerSample * buf.numberOfChannels', 'i32') }}};
+ break;
+ default:
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+ },
+
alSourcePlay__deps: ['setSourceState'],
alSourcePlay: function(source) {
if (!AL.currentContext) {
@@ -743,6 +889,12 @@ var LibraryOpenAL = {
case 0x202 /* AL_SOURCE_RELATIVE */:
{{{ makeSetValue('value', '0', 'src.panner ? 1 : 0', 'i32') }}};
break;
+ case 0x1001 /* AL_CONE_INNER_ANGLE */:
+ {{{ makeSetValue('value', '0', 'src.coneInnerAngle', 'i32') }}};
+ break;
+ case 0x1002 /* AL_CONE_OUTER_ANGLE */:
+ {{{ makeSetValue('value', '0', 'src.coneOuterAngle', 'i32') }}};
+ break;
case 0x1009 /* AL_BUFFER */:
if (!src.queue.length) {
{{{ makeSetValue('value', '0', '0', 'i32') }}};
@@ -809,12 +961,15 @@ var LibraryOpenAL = {
case 0x1021 /* AL_ROLLOFF_FACTOR */:
{{{ makeSetValue('value', '0', 'src.rolloffFactor', 'float') }}}
break;
- // case 0x1022 /* AL_CONE_OUTER_GAIN */:
- // break;
- // case 0x1001 /* AL_CONE_INNER_ANGLE */:
- // break;
- // case 0x1002 /* AL_CONE_OUTER_ANGLE */:
- // break;
+ case 0x1022 /* AL_CONE_OUTER_GAIN */:
+ {{{ makeSetValue('value', '0', 'src.coneOuterGain', 'float') }}}
+ break;
+ case 0x1001 /* AL_CONE_INNER_ANGLE */:
+ {{{ makeSetValue('value', '0', 'src.coneInnerAngle', 'float') }}}
+ break;
+ case 0x1002 /* AL_CONE_OUTER_ANGLE */:
+ {{{ makeSetValue('value', '0', 'src.coneOuterAngle', 'float') }}}
+ break;
case 0x1020 /* AL_REFERENCE_DISTANCE */:
{{{ makeSetValue('value', '0', 'src.refDistance', 'float') }}}
break;
@@ -830,6 +985,49 @@ var LibraryOpenAL = {
}
},
+ alGetSourcefv: function(source, param, values) {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alGetSourcefv called without a valid context");
+#endif
+ return;
+ }
+ var src = AL.currentContext.src[source - 1];
+ if (!src) {
+#if OPENAL_DEBUG
+ console.error("alGetSourcefv called with an invalid source");
+#endif
+ AL.currentContext.err = 0xA001 /* AL_INVALID_NAME */;
+ return;
+ }
+ switch (param) {
+ case 0x1004 /* AL_POSITION */:
+ var position = src.position;
+ {{{ makeSetValue('values', '0', 'position[0]', 'float') }}}
+ {{{ makeSetValue('values', '4', 'position[1]', 'float') }}}
+ {{{ makeSetValue('values', '8', 'position[2]', 'float') }}}
+ break;
+ case 0x1005 /* AL_DIRECTION */:
+ var direction = src.direction;
+ {{{ makeSetValue('values', '0', 'direction[0]', 'float') }}}
+ {{{ makeSetValue('values', '4', 'direction[1]', 'float') }}}
+ {{{ makeSetValue('values', '8', 'direction[2]', 'float') }}}
+ break;
+ case 0x1006 /* AL_VELOCITY */:
+ var velocity = src.velocity;
+ {{{ makeSetValue('values', '0', 'velocity[0]', 'float') }}}
+ {{{ makeSetValue('values', '4', 'velocity[1]', 'float') }}}
+ {{{ makeSetValue('values', '8', 'velocity[2]', 'float') }}}
+ break;
+ default:
+#if OPENAL_DEBUG
+ console.error("alGetSourcefv with param " + param + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+ },
+
alDistanceModel: function(model) {
if (model !== 0 /* AL_NONE */) {
#if OPENAL_DEBUG
@@ -838,6 +1036,136 @@ var LibraryOpenAL = {
}
},
+ alGetListenerf: function(pname, values) {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alGetListenerf called without a valid context");
+#endif
+ return;
+ }
+ switch (pname) {
+ case 0x100A /* AL_GAIN */:
+ {{{ makeSetValue('value', '0', 'AL.currentContext.gain.gain', 'float') }}}
+ break;
+ default:
+#if OPENAL_DEBUG
+ console.error("alGetListenerf with param " + pname + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+
+ },
+
+ alGetListenerfv: function(pname, values) {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alGetListenerfv called without a valid context");
+#endif
+ return;
+ }
+ switch (pname) {
+ case 0x1004 /* AL_POSITION */:
+ var position = AL.currentContext.ctx.listener._position || [0,0,0];
+ {{{ makeSetValue('values', '0', 'position[0]', 'float') }}}
+ {{{ makeSetValue('values', '4', 'position[1]', 'float') }}}
+ {{{ makeSetValue('values', '8', 'position[2]', 'float') }}}
+ break;
+ case 0x1006 /* AL_VELOCITY */:
+ var velocity = AL.currentContext.ctx.listener._velocity || [0,0,0];
+ {{{ makeSetValue('values', '0', 'velocity[0]', 'float') }}}
+ {{{ makeSetValue('values', '4', 'velocity[1]', 'float') }}}
+ {{{ makeSetValue('values', '8', 'velocity[2]', 'float') }}}
+ break;
+ case 0x100F /* AL_ORIENTATION */:
+ var orientation = AL.currentContext.ctx.listener._orientation || [0,0,0,0,0,0];
+ {{{ makeSetValue('values', '0', 'orientation[0]', 'float') }}}
+ {{{ makeSetValue('values', '4', 'orientation[1]', 'float') }}}
+ {{{ makeSetValue('values', '8', 'orientation[2]', 'float') }}}
+ {{{ makeSetValue('values', '12', 'orientation[3]', 'float') }}}
+ {{{ makeSetValue('values', '16', 'orientation[4]', 'float') }}}
+ {{{ makeSetValue('values', '20', 'orientation[5]', 'float') }}}
+ break;
+ default:
+#if OPENAL_DEBUG
+ console.error("alGetListenerfv with param " + pname + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+ },
+
+ alGetListeneri: function(pname, value) {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alGetListeneri called without a valid context");
+#endif
+ return;
+ }
+ switch (pname) {
+ default:
+#if OPENAL_DEBUG
+ console.error("alGetListeneri with param " + pname + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+ },
+
+ alListenerf: function(param, value) {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alListenerf called without a valid context");
+#endif
+ return;
+ }
+ switch (param) {
+ case 0x100A /* AL_GAIN */:
+ AL.currentContext.gain.value = value;
+ break;
+ default:
+#if OPENAL_DEBUG
+ console.error("alListenerf with param " + param + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+ },
+
+ alEnable: function(param) {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alEnable called without a valid context");
+#endif
+ return;
+ }
+ switch (param) {
+ default:
+#if OPENAL_DEBUG
+ console.error("alEnable with param " + param + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+ },
+
+ alDisable: function(param) {
+ if (!AL.currentContext) {
+#if OPENAL_DEBUG
+ console.error("alDisable called without a valid context");
+#endif
+ return;
+ }
+ switch (pname) {
+ default:
+#if OPENAL_DEBUG
+ console.error("alDisable with param " + param + " not implemented yet");
+#endif
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ break;
+ }
+ },
+
alListenerfv: function(param, values) {
if (!AL.currentContext) {
#if OPENAL_DEBUG
@@ -847,32 +1175,32 @@ var LibraryOpenAL = {
}
switch (param) {
case 0x1004 /* AL_POSITION */:
- AL.currentContext.ctx.listener.setPosition(
- {{{ makeGetValue('values', '0', 'float') }}},
- {{{ makeGetValue('values', '4', 'float') }}},
- {{{ makeGetValue('values', '8', 'float') }}}
- );
+ var x = {{{ makeGetValue('values', '0', 'float') }}};
+ var y = {{{ makeGetValue('values', '4', 'float') }}};
+ var z = {{{ makeGetValue('values', '8', 'float') }}};
+ AL.currentContext.ctx.listener._position = [x, y, z];
+ AL.currentContext.ctx.listener.setPosition(x, y, z);
break;
case 0x1006 /* AL_VELOCITY */:
- AL.currentContext.ctx.listener.setVelocity(
- {{{ makeGetValue('values', '0', 'float') }}},
- {{{ makeGetValue('values', '4', 'float') }}},
- {{{ makeGetValue('values', '8', 'float') }}}
- );
+ var x = {{{ makeGetValue('values', '0', 'float') }}};
+ var y = {{{ makeGetValue('values', '4', 'float') }}};
+ var z = {{{ makeGetValue('values', '8', 'float') }}};
+ AL.currentContext.ctx.listener._velocity = [x, y, z];
+ AL.currentContext.ctx.listener.setVelocity(x, y, z);
break;
case 0x100F /* AL_ORIENTATION */:
- AL.currentContext.ctx.listener.setOrientation(
- {{{ makeGetValue('values', '0', 'float') }}},
- {{{ makeGetValue('values', '4', 'float') }}},
- {{{ makeGetValue('values', '8', 'float') }}},
- {{{ makeGetValue('values', '12', 'float') }}},
- {{{ makeGetValue('values', '16', 'float') }}},
- {{{ makeGetValue('values', '20', 'float') }}}
- );
+ var x = {{{ makeGetValue('values', '0', 'float') }}};
+ var y = {{{ makeGetValue('values', '4', 'float') }}};
+ var z = {{{ makeGetValue('values', '8', 'float') }}};
+ var x2 = {{{ makeGetValue('values', '12', 'float') }}};
+ var y2 = {{{ makeGetValue('values', '16', 'float') }}};
+ var z2 = {{{ makeGetValue('values', '20', 'float') }}};
+ AL.currentContext.ctx.listener._orientation = [x, y, z, x2, y2, z2];
+ AL.currentContext.ctx.listener.setOrientation(x, y, z, x2, y2, z2);
break;
default:
#if OPENAL_DEBUG
- console.log("alListenerfv with param " + param + " not implemented yet");
+ console.error("alListenerfv with param " + param + " not implemented yet");
#endif
AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
break;
@@ -888,15 +1216,116 @@ var LibraryOpenAL = {
},
alGetString: function(param) {
- return allocate(intArrayFromString('NA'), 'i8', ALLOC_NORMAL);
+ if (AL.stringCache[param]) return AL.stringCache[param];
+ var ret;
+ switch (param) {
+ case 0 /* AL_NO_ERROR */:
+ ret = 'No Error';
+ break;
+ case 0xA001 /* AL_INVALID_NAME */:
+ ret = 'Invalid Name';
+ break;
+ case 0xA002 /* AL_INVALID_ENUM */:
+ ret = 'Invalid Enum';
+ break;
+ case 0xA003 /* AL_INVALID_VALUE */:
+ ret = 'Invalid Value';
+ break;
+ case 0xA004 /* AL_INVALID_OPERATION */:
+ ret = 'Invalid Operation';
+ break;
+ case 0xA005 /* AL_OUT_OF_MEMORY */:
+ ret = 'Out of Memory';
+ break;
+ case 0xB001 /* AL_VENDOR */:
+ ret = 'Emscripten';
+ break;
+ case 0xB002 /* AL_VERSION */:
+ ret = '1.1';
+ break;
+ case 0xB003 /* AL_RENDERER */:
+ ret = 'WebAudio';
+ break;
+ case 0xB004 /* AL_EXTENSIONS */:
+ ret = '';
+ break;
+ default:
+ AL.currentContext.err = 0xA002 /* AL_INVALID_ENUM */;
+ return 0;
+ }
+
+ ret = allocate(intArrayFromString(ret), 'i8', ALLOC_NORMAL);
+
+ AL.stringCache[param] = ret;
+
+ return ret;
},
alGetProcAddress: function(fname) {
return 0;
},
- alcGetString: function(param) {
- return allocate(intArrayFromString('NA'), 'i8', ALLOC_NORMAL);
+ alcGetString: function(device, param) {
+ if (AL.alcStringCache[param]) return AL.alcStringCache[param];
+ var ret;
+ switch (param) {
+ case 0 /* ALC_NO_ERROR */:
+ ret = 'No Error';
+ break;
+ case 0xA001 /* ALC_INVALID_DEVICE */:
+ ret = 'Invalid Device';
+ break;
+ case 0xA002 /* ALC_INVALID_CONTEXT */:
+ ret = 'Invalid Context';
+ break;
+ case 0xA003 /* ALC_INVALID_ENUM */:
+ ret = 'Invalid Enum';
+ break;
+ case 0xA004 /* ALC_INVALID_VALUE */:
+ ret = 'Invalid Value';
+ break;
+ case 0xA005 /* ALC_OUT_OF_MEMORY */:
+ ret = 'Out of Memory';
+ break;
+ case 0x1004 /* ALC_DEFAULT_DEVICE_SPECIFIER */:
+ if (typeof(AudioContext) == "function" ||
+ typeof(webkitAudioContext) == "function") {
+ ret = 'Device';
+ } else {
+ return 0;
+ }
+ break;
+ case 0x1005 /* ALC_DEVICE_SPECIFIER */:
+ if (typeof(AudioContext) == "function" ||
+ typeof(webkitAudioContext) == "function") {
+ ret = 'Device\0';
+ } else {
+ ret = '\0';
+ }
+ break;
+ case 0x311 /* ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER */:
+ return 0;
+ break;
+ case 0x310 /* ALC_CAPTURE_DEVICE_SPECIFIER */:
+ ret = '\0'
+ break;
+ case 0x1006 /* ALC_EXTENSIONS */:
+ if (!device) {
+ AL.currentContext.err = 0xA001 /* ALC_INVALID_DEVICE */;
+ return 0;
+ }
+ ret = '';
+ break;
+ default:
+ AL.currentContext.err = 0xA003 /* ALC_INVALID_ENUM */;
+ return 0;
+ }
+
+ ret = allocate(intArrayFromString(ret), 'i8', ALLOC_NORMAL);
+
+ AL.alcStringCache[param] = ret;
+
+ return ret;
},
alcGetProcAddress: function(device, fname) {