ref: 9c44e4133323d462ed11d537a09cb9efa3c77803
parent: 9b056ef99bd476810c8d70289bf82b1ebd05d8e3
author: Iliyas Jorio <iliyas@jor.io>
date: Wed Jul 29 15:57:38 EDT 2020
soundfx.cpp implementation with cmixer
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,6 +11,8 @@
include_directories(.)
add_executable(Candy_Crisis
+ src/support/cmixer.cpp
+ src/support/cmixer.h
src/blitter.cpp
src/blitter.h
src/CandyCrisis.cpp
--- a/src/soundfx.cpp
+++ b/src/soundfx.cpp
@@ -5,55 +5,21 @@
#include "soundfx.h"
#include "music.h"
-#if 0
-#include "fmod.hpp"
-#include "fmod_errors.h"
-#endif
+#include "support/cmixer.h"
#include <stdio.h>
-#if 0
-FMOD::System *g_fmod;
-static FMOD::Sound *s_sound[kNumSounds];
-#endif
+static std::vector<cmixer::WavStream> soundBank;
MBoolean soundOn = true;
+float playerStereoSeparation = 1.0;
-#if 0
-void FMOD_ERRCHECK(int result)
-{
- if (result != FMOD_OK)
- {
- printf("FMOD error! (%d) %s\n", result, FMOD_ErrorString(FMOD_RESULT(result)));
- abort();
- }
-}
-#endif
-
void InitSound( void )
{
-#if 0
- FMOD_RESULT result = FMOD::System_Create(&g_fmod);
- FMOD_ERRCHECK(result);
+ cmixer::InitWithSDL();
- unsigned int version;
- result = g_fmod->getVersion(&version);
- FMOD_ERRCHECK(result);
-
- if (version < FMOD_VERSION)
- {
- printf("Error! You are using an old version of FMOD %08x. This program requires %08x\n", version, FMOD_VERSION);
- abort();
- }
-
- result = g_fmod->init(64, FMOD_INIT_NORMAL, 0);
- FMOD_ERRCHECK(result);
-
for (int index=0; index<kNumSounds; index++)
{
- /* NOTE: don't replace the sound flags with FMOD_DEFAULT! This will make some WAVs loop (and fail to release their channels). */
- result = g_fmod->createSound(QuickResourceName("snd", index+128, ".wav"), FMOD_LOOP_OFF | FMOD_2D | FMOD_HARDWARE, 0, &s_sound[index]);
- FMOD_ERRCHECK(result);
+ soundBank.emplace_back(cmixer::LoadWAVFromFile(QuickResourceName("snd", index+128, ".wav")));
}
-#endif
}
@@ -69,41 +35,21 @@
void PlayStereoFrequency( short player, short which, short freq )
{
- struct SpeakerMix
- {
- float left, right, center;
- };
-
- SpeakerMix speakerMixForPlayer[] =
- {
- { 1.0, 0.0, 0.0 },
- { 0.0, 1.0, 0.0 },
- { 0.0, 0.0, 1.0 },
- };
-
- const SpeakerMix& mix = speakerMixForPlayer[player];
-
if (soundOn)
{
-#if 0
- FMOD::Channel* channel = NULL;
- FMOD_RESULT result = g_fmod->playSound(FMOD_CHANNEL_FREE, s_sound[which], true, &channel);
- FMOD_ERRCHECK(result);
+ auto& effect = soundBank[which];
- result = channel->setSpeakerMix(mix.left, mix.right, mix.center, 0.0, 0.0, 0.0, 0.0, 0.0);
- FMOD_ERRCHECK(result);
+ double pan;
+ switch (player) {
+ case 0: pan = -playerStereoSeparation; break;
+ case 1: pan = +playerStereoSeparation; break;
+ default: pan = 0.0; break;
+ }
- float channelFrequency;
- result = s_sound[which]->getDefaults(&channelFrequency, NULL, NULL, NULL);
- FMOD_ERRCHECK(result);
+ effect.SetPan(pan);
+ effect.SetPitch(1.0 + freq/16.0);
+ effect.Play();
- result = channel->setFrequency((channelFrequency * (16 + freq)) / 16);
- FMOD_ERRCHECK(result);
-
- result = channel->setPaused(false);
- FMOD_ERRCHECK(result);
-
-#endif
UpdateSound();
}
}
@@ -110,7 +56,4 @@
void UpdateSound()
{
-#if 0
- g_fmod->update();
-#endif
}
--- /dev/null
+++ b/src/support/cmixer.cpp
@@ -1,0 +1,534 @@
+// Adapted from cmixer by rxi (https://github.com/rxi/cmixer)
+
+/*
+** Copyright (c) 2017 rxi
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+**/
+
+#include "cmixer.h"
+#include <SDL.h>
+
+#include <vector>
+#include <fstream>
+#include <list>
+
+using namespace cmixer;
+
+#define CLAMP(x, a, b) ((x) < (a) ? (a) : (x) > (b) ? (b) : (x))
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+#define FX_BITS (12)
+#define FX_UNIT (1 << FX_BITS)
+#define FX_MASK (FX_UNIT - 1)
+#define FX_FROM_FLOAT(f) ((long)((f) * FX_UNIT))
+#define DOUBLE_FROM_FX(f) ((double)f / FX_UNIT)
+#define FX_LERP(a, b, p) ((a) + ((((b) - (a)) * (p)) >> FX_BITS))
+
+#define BUFFER_MASK (BUFFER_SIZE - 1)
+
+//-----------------------------------------------------------------------------
+// Global mixer
+
+static struct Mixer {
+ SDL_mutex* sdlAudioMutex;
+
+ std::list<Source*> sources; // Linked list of active (playing) sources
+ int32_t pcmmixbuf[BUFFER_SIZE]; // Internal master buffer
+ int samplerate; // Master samplerate
+ int gain; // Master gain (fixed point)
+
+ void Init(int samplerate);
+ void Process(int16_t* dst, int len);
+ void Lock();
+ void Unlock();
+ void SetMasterGain(double newGain);
+} gMixer;
+
+//-----------------------------------------------------------------------------
+// Global init/shutdown
+
+static bool sdlAudioSubSystemInited = false;
+static SDL_AudioDeviceID sdlDeviceID = 0;
+
+void cmixer::InitWithSDL()
+{
+ if (sdlAudioSubSystemInited)
+ throw std::runtime_error("SDL audio subsystem already inited");
+
+ if (0 != SDL_InitSubSystem(SDL_INIT_AUDIO))
+ throw std::runtime_error("couldn't init SDL audio subsystem");
+
+ sdlAudioSubSystemInited = true;
+
+ // Init SDL audio
+ SDL_AudioSpec fmt = {};
+ fmt.freq = 44100;
+ fmt.format = AUDIO_S16;
+ fmt.channels = 2;
+ fmt.samples = 1024;
+ fmt.callback = [](void* udata, Uint8* stream, int size) {
+ gMixer.Process((int16_t*)stream, size / 2);
+ };
+
+ SDL_AudioSpec got;
+ sdlDeviceID = SDL_OpenAudioDevice(NULL, 0, &fmt, &got, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE);
+ if (!sdlDeviceID)
+ throw std::runtime_error(SDL_GetError());
+
+ // Init library
+ gMixer.Init(got.freq);
+ gMixer.SetMasterGain(0.5);
+
+ // Start audio
+ SDL_PauseAudioDevice(sdlDeviceID, 0);
+}
+
+void cmixer::ShutdownWithSDL()
+{
+ if (sdlDeviceID) {
+ SDL_CloseAudioDevice(sdlDeviceID);
+ sdlDeviceID = 0;
+ }
+ if (sdlAudioSubSystemInited) {
+ SDL_QuitSubSystem(SDL_INIT_AUDIO);
+ sdlAudioSubSystemInited = false;
+ }
+}
+
+double cmixer::GetMasterGain()
+{
+ return DOUBLE_FROM_FX(gMixer.gain);
+}
+
+void cmixer::SetMasterGain(double newGain)
+{
+ gMixer.SetMasterGain(newGain);
+}
+
+//-----------------------------------------------------------------------------
+// Global mixer impl
+
+void Mixer::Lock()
+{
+ SDL_LockMutex(sdlAudioMutex);
+}
+
+void Mixer::Unlock()
+{
+ SDL_UnlockMutex(sdlAudioMutex);
+}
+
+void Mixer::Init(int newSamplerate)
+{
+ sdlAudioMutex = SDL_CreateMutex();
+
+ samplerate = newSamplerate;
+ gain = FX_UNIT;
+}
+
+void Mixer::SetMasterGain(double newGain)
+{
+ if (newGain < 0)
+ newGain = 0;
+ gain = FX_FROM_FLOAT(newGain);
+}
+
+void Mixer::Process(int16_t* dst, int len)
+{
+ // Process in chunks of BUFFER_SIZE if `len` is larger than BUFFER_SIZE
+ while (len > BUFFER_SIZE) {
+ Process(dst, BUFFER_SIZE);
+ dst += BUFFER_SIZE;
+ len -= BUFFER_SIZE;
+ }
+
+ // Zeroset internal buffer
+ memset(pcmmixbuf, 0, len * sizeof(pcmmixbuf[0]));
+
+ // Process active sources
+ Lock();
+ for (auto si = sources.begin(); si != sources.end(); ) {
+ auto& s = **si;
+ s.Process(len);
+ // Remove source from list if it is no longer playing
+ if (s.state != CM_STATE_PLAYING) {
+ s.active = false;
+ si = sources.erase(si);
+ }
+ else {
+ ++si;
+ }
+ }
+ Unlock();
+
+ // Copy internal buffer to destination and clip
+ for (int i = 0; i < len; i++) {
+ int x = (pcmmixbuf[i] * gain) >> FX_BITS;
+ dst[i] = CLAMP(x, -32768, 32767);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Source implementation
+
+Source::Source(int theSampleRate, int theLength)
+{
+ memset(this, 0, sizeof(*this));
+ length = theLength;
+ samplerate = theSampleRate;
+ SetGain(1);
+ SetPan(0);
+ SetPitch(1);
+ SetLoop(0);
+ Stop();
+}
+
+Source::~Source()
+{
+ gMixer.Lock();
+ if (active) {
+ gMixer.sources.remove(this);
+ }
+ gMixer.Unlock();
+ //CMEvent e;
+ //e.type = CM_EVENT_DESTROY;
+ //e.udata = udata;
+ //handler(&e);
+}
+
+void Source::Rewind()
+{
+ Rewind2();
+ position = 0;
+ rewind = false;
+ end = length;
+ nextfill = 0;
+}
+
+void Source::FillBuffer(int offset, int length)
+{
+ FillBuffer(pcmbuf + offset, length);
+}
+
+void Source::Process(int len)
+{
+ int32_t* dst = gMixer.pcmmixbuf;
+
+ // Do rewind if flag is set
+ if (rewind) {
+ Rewind();
+ }
+
+ // Don't process if not playing
+ if (state != CM_STATE_PLAYING) {
+ return;
+ }
+
+ // Process audio
+ while (len > 0) {
+ // Get current position frame
+ int frame = int(position >> FX_BITS);
+
+ // Fill buffer if required
+ if (frame + 3 >= nextfill) {
+ FillBuffer((nextfill * 2) & BUFFER_MASK, BUFFER_SIZE / 2);
+ nextfill += BUFFER_SIZE / 4;
+ }
+
+ // Handle reaching the end of the playthrough
+ if (frame >= end) {
+ // As streams continiously fill the raw buffer in a loop we simply
+ // increment the end idx by one length and continue reading from it for
+ // another play-through
+ end = frame + this->length;
+ // Set state and stop processing if we're not set to loop
+ if (!loop) {
+ state = CM_STATE_STOPPED;
+ if (onComplete != nullptr)
+ onComplete();
+ break;
+ }
+ }
+
+ // Work out how many frames we should process in the loop
+ int n = MIN(nextfill - 2, end) - frame;
+ int count = (n << FX_BITS) / rate;
+ count = MAX(count, 1);
+ count = MIN(count, len / 2);
+ len -= count * 2;
+
+ // Add audio to master buffer
+ if (rate == FX_UNIT) {
+ // Add audio to buffer -- basic
+ n = frame * 2;
+ for (int i = 0; i < count; i++) {
+ dst[0] += (pcmbuf[(n ) & BUFFER_MASK] * lgain) >> FX_BITS;
+ dst[1] += (pcmbuf[(n + 1) & BUFFER_MASK] * rgain) >> FX_BITS;
+ n += 2;
+ dst += 2;
+ }
+ this->position += count * FX_UNIT;
+ }
+ else {
+ // Add audio to buffer -- interpolated
+ for (int i = 0; i < count; i++) {
+ n = int(position >> FX_BITS) * 2;
+ int p = position & FX_MASK;
+ int a = pcmbuf[(n ) & BUFFER_MASK];
+ int b = pcmbuf[(n + 2) & BUFFER_MASK];
+ dst[0] += (FX_LERP(a, b, p) * lgain) >> FX_BITS;
+ n++;
+ a = pcmbuf[(n ) & BUFFER_MASK];
+ b = pcmbuf[(n + 2) & BUFFER_MASK];
+ dst[1] += (FX_LERP(a, b, p) * rgain) >> FX_BITS;
+ position += rate;
+ dst += 2;
+ }
+ }
+
+ }
+}
+
+double Source::GetLength() const
+{
+ return length / (double)samplerate;
+}
+
+double Source::GetPosition() const
+{
+ return ((position >> FX_BITS) % length) / (double)samplerate;
+}
+
+int Source::GetState() const
+{
+ return state;
+}
+
+void Source::RecalcGains()
+{
+ double l = this->gain * (pan <= 0. ? 1. : 1. - pan);
+ double r = this->gain * (pan >= 0. ? 1. : 1. + pan);
+ this->lgain = FX_FROM_FLOAT(l);
+ this->rgain = FX_FROM_FLOAT(r);
+}
+
+void Source::SetGain(double newGain)
+{
+ gain = newGain;
+ RecalcGains();
+}
+
+void Source::SetPan(double newPan)
+{
+ pan = CLAMP(newPan, -1.0, 1.0);
+ RecalcGains();
+}
+
+void Source::SetPitch(double newPitch)
+{
+ double newRate;
+ if (newPitch > 0.) {
+ newRate = samplerate / (double)gMixer.samplerate * newPitch;
+ }
+ else {
+ newRate = 0.001;
+ }
+ rate = FX_FROM_FLOAT(newRate);
+}
+
+void Source::SetLoop(bool newLoop)
+{
+ loop = newLoop;
+}
+
+void Source::Play()
+{
+ gMixer.Lock();
+ state = CM_STATE_PLAYING;
+ if (!active) {
+ active = true;
+ gMixer.sources.push_front(this);
+ }
+ gMixer.Unlock();
+}
+
+void Source::Pause()
+{
+ state = CM_STATE_PAUSED;
+}
+
+void Source::TogglePause()
+{
+ if (state == CM_STATE_PAUSED)
+ Play();
+ else if (state == CM_STATE_PLAYING)
+ Pause();
+ else {
+ ;
+ }
+}
+
+void Source::Stop()
+{
+ state = CM_STATE_STOPPED;
+ rewind = true;
+}
+
+//-----------------------------------------------------------------------------
+// WavStream implementation
+
+#define WAV_PROCESS_LOOP(X) \
+ while (n--) { \
+ X \
+ dst += 2; \
+ idx++; \
+ }
+
+WavStream::WavStream(
+ int theSampleRate,
+ int theBitDepth,
+ int nChannels,
+ std::vector<char>&& data
+)
+ :Source(theSampleRate, int((data.size() / (theBitDepth / 8)) / nChannels))
+ ,udata(data)
+ ,idx(0)
+ ,bitdepth(theBitDepth)
+ ,channels(nChannels)
+{
+ bigEndian = false;
+}
+
+void WavStream::Rewind2()
+{
+ idx = 0;
+}
+
+void WavStream::FillBuffer(int16_t* dst, int len)
+{
+ int x, n;
+
+ len /= 2;
+
+ while (len > 0) {
+ n = MIN(len, length - idx);
+ len -= n;
+ if (bitdepth == 16 && channels == 1) {
+ WAV_PROCESS_LOOP({
+ dst[0] = dst[1] = data16()[idx];
+ });
+ }
+ else if (bitdepth == 16 && channels == 2) {
+ WAV_PROCESS_LOOP({
+ x = idx * 2;
+ dst[0] = data16()[x];
+ dst[1] = data16()[x + 1];
+ });
+ }
+ else if (bitdepth == 8 && channels == 1) {
+ WAV_PROCESS_LOOP({
+ dst[0] = dst[1] = (data8()[idx] - 128) << 8;
+ });
+ }
+ else if (bitdepth == 8 && channels == 2) {
+ WAV_PROCESS_LOOP({
+ x = idx * 2;
+ dst[0] = (data8()[x] - 128) << 8;
+ dst[1] = (data8()[x + 1] - 128) << 8;
+ });
+ }
+ else {
+ throw std::invalid_argument("big endian clips not supported");
+ }
+ // Loop back and continue filling buffer if we didn't fill the buffer
+ if (len > 0) {
+ idx = 0;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// LoadWAVFromFile for testing
+
+static std::vector<char> LoadFile(char const* filename)
+{
+ std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
+ auto pos = ifs.tellg();
+ std::vector<char> bytes(pos);
+ ifs.seekg(0, std::ios::beg);
+ ifs.read(&bytes[0], pos);
+ return bytes;
+}
+
+static const char* FindChunk(const char* data, int len, const char* id, int* size)
+{
+ // TODO : Error handling on malformed wav file
+ int idlen = strlen(id);
+ const char* p = data + 12;
+next:
+ *size = *((uint32_t*)(p + 4));
+ if (memcmp(p, id, idlen)) {
+ p += 8 + *size;
+ if (p > data + len) return NULL;
+ goto next;
+ }
+ return p + 8;
+}
+
+WavStream cmixer::LoadWAVFromFile(const char* path)
+{
+ int sz;
+ auto filebuf = LoadFile(path);
+ auto len = filebuf.size();
+ const char* data = filebuf.data();
+ const char* p = (char*)data;
+
+ // Check header
+ if (memcmp(p, "RIFF", 4) || memcmp(p + 8, "WAVE", 4))
+ throw std::invalid_argument("bad wav header");
+
+ // Find fmt subchunk
+ p = FindChunk(data, len, "fmt ", &sz);
+ if (!p)
+ throw std::invalid_argument("no fmt subchunk");
+
+ // Load fmt info
+ int format = *((uint16_t*)(p));
+ int channels = *((uint16_t*)(p + 2));
+ int samplerate = *((uint32_t*)(p + 4));
+ int bitdepth = *((uint16_t*)(p + 14));
+ if (format != 1)
+ throw std::invalid_argument("unsupported format");
+ if (channels == 0 || samplerate == 0 || bitdepth == 0)
+ throw std::invalid_argument("bad format");
+
+ // Find data subchunk
+ p = FindChunk(data, len, "data", &sz);
+ if (!p)
+ throw std::invalid_argument("no data subchunk");
+
+ return WavStream(
+ samplerate,
+ bitdepth,
+ channels,
+ std::vector<char>(p, p + sz));
+}
+
--- /dev/null
+++ b/src/support/cmixer.h
@@ -1,0 +1,116 @@
+// Adapted from cmixer by rxi (https://github.com/rxi/cmixer)
+
+/*
+** Copyright (c) 2017 rxi
+**
+** Permission is hereby granted, free of charge, to any person obtaining a copy
+** of this software and associated documentation files (the "Software"), to
+** deal in the Software without restriction, including without limitation the
+** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+** sell copies of the Software, and to permit persons to whom the Software is
+** furnished to do so, subject to the following conditions:
+**
+** The above copyright notice and this permission notice shall be included in
+** all copies or substantial portions of the Software.
+**
+** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+** IN THE SOFTWARE.
+**/
+
+#pragma once
+
+#include <vector>
+#include <functional>
+#include <cstdint>
+
+#define BUFFER_SIZE (512)
+
+namespace cmixer {
+
+enum {
+ CM_STATE_STOPPED,
+ CM_STATE_PLAYING,
+ CM_STATE_PAUSED
+};
+
+struct Source {
+ int16_t pcmbuf[BUFFER_SIZE]; // Internal buffer with raw stereo PCM
+ int samplerate; // Stream's native samplerate
+ int length; // Stream's length in frames
+ int end; // End index for the current play-through
+ int state; // Current state (playing|paused|stopped)
+ int64_t position; // Current playhead position (fixed point)
+ int lgain, rgain; // Left and right gain (fixed point)
+ int rate; // Playback rate (fixed point)
+ int nextfill; // Next frame idx where the buffer needs to be filled
+ bool loop; // Whether the source will loop when `end` is reached
+ bool rewind; // Whether the source will rewind before playing
+ bool active; // Whether the source is part of `sources` list
+ double gain; // Gain set by `cm_set_gain()`
+ double pan; // Pan set by `cm_set_pan()`
+ std::function<void()> onComplete; // Callback
+
+protected:
+ Source(int theSampleRate, int theLength);
+
+ virtual void Rewind2() = 0;
+ virtual void FillBuffer(int16_t* buffer, int length) = 0;
+
+public:
+ void Rewind();
+ void RecalcGains();
+ void FillBuffer(int offset, int length);
+ void Process(int len);
+
+public:
+ ~Source();
+ double GetLength() const;
+ double GetPosition() const;
+ int GetState() const;
+ void SetGain(double gain);
+ void SetPan(double pan);
+ void SetPitch(double pitch);
+ void SetLoop(bool loop);
+ void Play();
+ void Pause();
+ void TogglePause();
+ void Stop();
+};
+
+class WavStream : public Source {
+ int bitdepth;
+ int channels;
+ int idx;
+
+ std::vector<char> udata;
+
+ void Rewind2();
+ void FillBuffer(int16_t* buffer, int length);
+
+ inline uint8_t* data8() { return (uint8_t*)udata.data(); }
+ inline int16_t* data16() { return (int16_t*)udata.data(); }
+
+public:
+ bool bigEndian;
+
+ WavStream(
+ int theSampleRate,
+ int theBitDepth,
+ int nChannels,
+ std::vector<char>&& data
+ );
+};
+
+
+void InitWithSDL();
+void ShutdownWithSDL();
+double GetMasterGain();
+void SetMasterGain(double);
+WavStream LoadWAVFromFile(const char* path);
+
+}