shithub: candycrisis

Download patch

ref: 9c44e4133323d462ed11d537a09cb9efa3c77803
parent: 9b056ef99bd476810c8d70289bf82b1ebd05d8e3
author: Iliyas Jorio <iliyas@jor.io>
date: Wed Jul 29 15:57:38 EDT 2020

soundfx.cpp implementation with cmixer

diff: cannot open b/src/support//null: file does not exist: 'b/src/support//null'
--- 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);
+
+}