Files
itgmania212121/stepmania/src/RageSound.cpp
T
2002-12-18 07:48:54 +00:00

637 lines
16 KiB
C++

/*
* Handle loading and decoding of sounds through SDL_sound. This file
* is portable; actual playing is handled in RageSoundManager.
* For small files, pre-decode the entire file into a regular buffer. We
* might want to play many samples at once, and we don't want to have to decode
* 5-10 mp3s simultaneously during play.
*
* For larger files, decode them on the fly. These are usually music, and there's
* usually only one of those playing at a time. When we get updates, decode data
* at the same rate we're playing it. If we don't do this, and we're being read
* in large chunks, we're forced to decode in larger chunks as well, which can
* cause framerate problems.
*
* Known problems:
* I hear a click in one speaker at the beginning of some MP3s. This is probably
* something wrong with my SDL_sound MAD wrapper ...
*
* TODO:
* Rate (speed)
* Configurable buffer sizes (stored in SoundManager) and so on
*
* We need (yet) another layer of abstraction: RageSoundSource. It'll just
* implement the SDL_sound interface (in a class). Two implementations;
* one, used normally, that just wraps SDL_sound; and another that is given
* a list of sounds and time offsets and transparently mixes them together
* at the given times, filling the gaps with silence. This should be an easy
* way to handle autoplay tracks in keyed games. Normal background music can
* be passed to it with an offset of 0 (or the gap, however it works out).
*/
#include "stdafx.h"
#include "RageSound.h"
#include "RageSoundManager.h"
#include "RageUtil.h"
#include "RageLog.h"
#include "RageException.h"
#include "RageTimer.h"
#include "SDL_sound-1.0.0/SDL_sound.h"
#ifdef _DEBUG
#pragma comment(lib, "SDL_sound-1.0.0/lib/sdl_sound_static_d.lib")
#else
#pragma comment(lib, "SDL_sound-1.0.0/lib/sdl_sound_static.lib")
#endif
const int channels = 2;
const int samplesize = 2 * channels; /* 16-bit */
const int samplerate = 44100;
/* If a sound is smaller than this, we'll load it entirely into memory. */
const int max_prebuf_size = 1024*256;
/* The most data to buffer when streaming. This should generally be at least as large
* as the largest hardware buffer. */
const int internal_buffer_size = 1024*16;
/* The amount of data to read from SDL_sound at once. */
const int read_block_size = 1024;
RageSound::RageSound()
{
ASSERT(SOUNDMAN);
LockMutex L(SOUNDMAN->lock);
static bool initialized = false;
if(!initialized)
{
if(!Sound_Init())
throw RageException( "RageSoundManager::RageSoundManager: error initializing sound loader: %s", Sound_GetError());
initialized = true;
}
stream.Sample = NULL;
position = 0;
playing = false;
Loop = false;
AutoStop = true;
speed = 1.0f;
stream.buf.reserve(internal_buffer_size);
m_StartSeconds = 0;
m_LengthSeconds = -1;
/* Register ourselves, so we receive Update()s. */
SOUNDMAN->all_sounds.insert(this);
}
RageSound::~RageSound()
{
Unload();
/* Unregister ourselves. */
SOUNDMAN->all_sounds.erase(this);
}
RageSound::RageSound(const RageSound &cpy)
{
ASSERT(SOUNDMAN);
LockMutex L(SOUNDMAN->lock);
stream.Sample = NULL;
full_buf = cpy.full_buf;
big = cpy.big;
AutoStop = cpy.AutoStop;
m_sFilePath = cpy.m_sFilePath;
m_StartSeconds = cpy.m_StartSeconds;
m_LengthSeconds = cpy.m_LengthSeconds;
Loop = cpy.Loop;
position = cpy.position;
playing = false;
speed = cpy.speed;
if(big)
{
/* We can't copy the Sound_Sample, so load a new one.
* Don't bother trying to load it in a small buffer. */
stream.buf.reserve(internal_buffer_size);
Load(cpy.GetLoadedFilePath(), false);
}
/* Register ourselves, so we receive Update()s. */
SOUNDMAN->all_sounds.insert(this);
}
void RageSound::Unload()
{
if(IsPlaying())
Stop();
Sound_FreeSample(stream.Sample);
stream.Sample = NULL;
m_sFilePath = "";
stream.buf.clear();
full_buf.erase();
}
void RageSound::Load(CString sSoundFilePath, bool cache)
{
LOG->Trace( "RageSound::LoadSound( '%s' )", sSoundFilePath.GetString() );
Unload();
m_sFilePath = sSoundFilePath;
Sound_AudioInfo sound_desired;
sound_desired.channels = channels;
sound_desired.format = AUDIO_S16SYS;
sound_desired.rate = samplerate;
Sound_Sample *NewSample = Sound_NewSampleFromFile(sSoundFilePath.GetString(),
&sound_desired, read_block_size);
if( NewSample == NULL )
throw RageException( "RageSound::LoadSound: error loading %s: %s",
sSoundFilePath.GetString(), Sound_GetError() );
/* Try to decode into full_buf. */
big = false;
if(!cache)
big = true; /* Don't. */
/* Check the length, and see if we think it'll fit in the buffer. */
{
int len = Sound_Length(NewSample);
/* This will fail with EAGAIN if it'll take a while. We only want
* to do this if it's fast. */
if(len != -1) {
float secs = len / 1000.f;
int pcmsize = int(secs * samplerate * samplesize); /* seconds -> bytes */
if(pcmsize > max_prebuf_size)
big = true; /* Don't bother trying to preload it. */
else
full_buf.reserve(pcmsize);
}
Sound_Rewind(NewSample);
}
while(!big) {
int cnt = Sound_Decode(NewSample);
if(cnt < 0)
throw RageException("Read error on %s: %s",
sSoundFilePath.GetString(), Sound_GetError() ); /* XXX (see other error-handling XXX) */
/* Add the buffer. */
full_buf.append((const char *)NewSample->buffer,
(const char *)NewSample->buffer+cnt);
if(full_buf.size() > max_prebuf_size) {
full_buf.erase();
big = true; /* too big */
}
if(NewSample->flags & SOUND_SAMPLEFLAG_EOF)
break;
}
if(big) {
/* Oops; we need to stream it. */
stream.Sample = NewSample;
Sound_Rewind(stream.Sample);
} else {
/* We're done with the stream. */
Sound_FreeSample(NewSample);
}
position = 0;
}
void RageSound::SetStartSeconds( float secs )
{
m_StartSeconds = secs;
}
void RageSound::SetLengthSeconds(float secs)
{
m_LengthSeconds = secs;
}
/* Start playing from m_StartSeconds. TODO: a way to start playing
* from the current pos (unpause) */
void RageSound::Play()
{
LockMutex L(SOUNDMAN->lock);
if(playing) Stop();
playing = true;
SetPositionSeconds(m_StartSeconds);
// Tell the sound manager to start mixing us
SOUNDMAN->StartMixing(this);
}
void RageSound::Update(float delta)
{
if(playing && big)
stream.FillBuf(int(delta * samplerate * samplesize));
}
/* Fill the buffer by about "bytes" worth of data. (We might go a little
* over, and we won't overflow our buffer.) */
int RageSound::stream_t::FillBuf(int bytes)
{
ASSERT(Sample);
bool got_something = false;
if(Sample->flags & SOUND_SAMPLEFLAG_EOF)
return got_something; /* EOF */
while(bytes > 0)
{
if(buf.size()+read_block_size > buf.capacity())
break; /* full */
int cnt = Sound_Decode(Sample);
if(Sample->flags & SOUND_SAMPLEFLAG_EOF)
return got_something; /* EOF */
if(Sample->flags & SOUND_SAMPLEFLAG_ERROR)
{
/* There was a fatal error; get it with Sound_GetError().
* XXX: How should we handle sound errors? We can't
* just return error, since we're in a separate thread.
* Most of the time we should probably just warn and move
* on (no big deal), but the gameplay screen should query
* periodically and do something more intelligent when we
* fail (so we don't play out the rest of the song in
* silence) ... */
throw RageException("Read error: %s",
Sound_GetError() );
}
buf.write((const char *)Sample->buffer, cnt);
bytes -= cnt;
got_something = true;
}
return got_something;
}
/* Called by the mixer: return a block of sound data.
* Be careful; this is called in a separate thread. */
int RageSound::GetPCM(char *buffer, int size, int sampleno)
{
LockMutex L(SOUNDMAN->lock);
/* If the sound is paused, just fill the buffer with silence.
* Hmm. Pausing is annoying, since we'll get startup latency if
* we keep our buffer playing; we should stop the stream completely
* when we pause ... */
ASSERT(playing);
int bytes_stored = 0;
pos_map[0] = pos_map[1];
pos_map[1] = pos_map[2];
pos_map[2] = pos_map[3];
pos_map[3].clear();
/* "sampleno" is the audio driver's conception of time. "position"
* is ours. Keep track of sampleno->position mappings for two GetPCM calls.
*
* This way, when we query the time later on, we can derive position
* values from the sampleno values returned from GetPosition.
*
* We need to keep two buffers worth of values, since we might loop at
* the end of a buffer. */
/* Now actually put data from the correct buffer into the output. */
while(size)
{
int got;
if(position < 0) {
/* We havn't *really* started playing yet, so just feed silence. How
* many more bytes of silence do we need? */
got = -position * samplesize;
got = min(got, size);
memset(buffer, 0, got);
} else if(big) {
/* Feed data out of our streaming buffer. */
ASSERT(stream.Sample);
got = min(int(stream.buf.size()), size);
stream.buf.read(buffer, got);
} else {
/* Feed data out of our full buffer. */
int byte_pos = position * samplesize;
got = min(int(full_buf.size())-byte_pos, size);
got = max(got, 0);
if(got)
memcpy(buffer, full_buf.data()+byte_pos, got);
}
if(!got)
{
/* We need more data. Find out if we've hit EOF. */
bool HitEOF = true;
if(big) {
/* If we don't have any data left buffered, fill the buffer by up to
* as much as we need. */
if(stream.buf.size() || stream.FillBuf(size))
HitEOF = false; /* we have more */
} else {
unsigned byte_pos = position * samplesize; /* samples -> bytes */
if(byte_pos < full_buf.size())
HitEOF = false; /* we have more */
}
/* If we've passed the stop point (m_StartSeconds+m_LengthSeconds), pretend
* we've hit EOF. */
if(m_LengthSeconds != -1 &&
float(position)/samplerate > m_StartSeconds+m_LengthSeconds)
HitEOF = true;
if(!HitEOF)
continue;
/* We're at EOF. If we're not looping, just stop. */
if(Loop)
{
/* Rewind and start over. */
SetPositionSeconds(m_StartSeconds);
continue;
}
if(AutoStop)
break;
/* We're out of data, but we're not going to stop, so fill in the
* rest with silence. */
memset(buffer, 0, size);
got = size;
}
/* Save this sampleno/position map. */
pos_map[3].push_back(pos_map_t(sampleno, position, got/samplesize));
int got_samples = got / samplesize; /* bytes -> samples */
/* This block goes from position to position+got_samples. */
const float FADE_TIME = 1.5f;
/* XXX: Loop shouldn't set fading; add a Fade_Time member?
*
* We want to fade when there's FADE_TIME seconds left, but if
* m_LengthSeconds is -1, we don't know the length we're playing.
* (m_LengthSeconds is the length to play, not the length of the
* source.) If we don't know the length, don't fade. */
if(Loop && m_LengthSeconds != -1) {
Sint16 *p = (Sint16 *) buffer;
int this_position = position;
for(int samp = 0; samp < got_samples; ++samp)
{
float fSecsUntilSilent = (m_StartSeconds + m_LengthSeconds) - float(this_position)/samplerate;
float fVolPercent = fSecsUntilSilent / FADE_TIME;
fVolPercent = clamp(fVolPercent, 0.f, 1.f);
for(int i = 0; i < channels; ++i) {
*p = short(*p * fVolPercent);
p++;
}
this_position++;
}
}
bytes_stored += got;
position += got_samples;
size -= got;
buffer += got;
sampleno += got_samples;
}
return bytes_stored;
}
/* After we finish via the GetPCM return value, this is called by
* RageSoundManager once the sound is actually flushed, which means
* we're *really* stopped. */
void RageSound::SoundStopped()
{
LOG->Trace("stop completed");
Stop();
}
void RageSound::Pause()
{
if(!playing) return;
// Tell the sound manager to stop mixing this sound.
SOUNDMAN->StopMixing(this);
playing = false;
}
void RageSound::Stop()
{
/* Tell the sound manager to stop mixing this sound. */
SOUNDMAN->StopMixing(this);
if(big) {
ASSERT(stream.Sample);
Sound_Rewind(stream.Sample);
stream.buf.clear();
}
playing = false;
position = 0;
for(int i = 0; i < 4; ++i)
pos_map[i].clear();
}
float RageSound::GetLengthSeconds()
{
if(big) {
ASSERT(stream.Sample);
int len = Sound_Length(stream.Sample);
if(len == -1 && stream.Sample->flags & SOUND_SAMPLEFLAG_EAGAIN) {
/* This indicates the length check will take a little while; call
* it again to confirm. */
len = Sound_Length(stream.Sample);
}
if(len < 0)
return -1; /* XXX: put a Sound_GetError() error message somewhere */
return len / 1000.f; /* ms -> secs */
} else {
/* We have the whole file loaded; just return the position. */
return full_buf.size() / (float(samplerate)*samplesize);
}
}
float RageSound::GetPositionSeconds() const
{
LockMutex L(SOUNDMAN->lock);
/* If we're not playing, just report the static position. */
if( !IsPlaying() )
return position / float(samplerate);
/* If we don't yet have any position data, GetPCM hasn't yet been called at all,
* so report the static position. */
{
bool HasData = false;
for(int i = 0; i < 4; ++i) {
if(!pos_map[i].empty()) HasData = true;
}
if(!HasData) {
LOG->Trace("no data yet; %i", position);
return position / float(samplerate);
}
}
/* Get our current hardware position. */
int cur_sample = SOUNDMAN->GetPosition(this);
/* Concatenate the pos_maps. (Just a cheat to simplify this a little; this
* is small, so this isn't very expensive.) */
vector<pos_map_t> posmaps;
for(int n = 0; n < 4; ++n)
posmaps.insert(posmaps.end(), pos_map[n].begin(), pos_map[n].end());
/* sampleno is probably in one of the pos_maps. Search through them
* to figure out what position this sampleno maps to. */
int closest_position = 0, closest_position_dist = INT_MAX;
for(unsigned i = 0; i < posmaps.size(); ++i) {
if(cur_sample >= posmaps[i].sampleno &&
cur_sample < posmaps[i].sampleno+posmaps[i].samples)
{
/* cur_sample lies in this block; it's an exact match. Figure
* out the exact position. */
int diff = posmaps[i].position - posmaps[i].sampleno;
return float(cur_sample + diff) / samplerate;
}
/* See if the current position is close to the beginning of this block. */
int dist = abs(posmaps[i].sampleno - cur_sample);
if(dist < closest_position_dist)
{
closest_position_dist = dist;
closest_position = posmaps[i].position;
}
/* See if the current position is close to the end of this block. */
dist = abs(posmaps[i].sampleno + posmaps[i].samples - cur_sample);
if(dist < closest_position_dist)
{
closest_position_dist = dist + posmaps[i].samples;
closest_position = posmaps[i].position + posmaps[i].samples;
}
}
/* The sample is out of the range of data we've actually sent.
* Return the closest position.
*
* There are three cases when this happens:
* 1. After the first GetPCM call, but before it actually gets heard.
* 2. After GetPCM returns EOF and the sound has flushed, but before
* SoundStopped has been called.
* 3. Underflow; we'll be given a larger sample number than we know about.
*/
return closest_position / float(samplerate);
}
void RageSound::SetPositionSeconds( float fSeconds )
{
LockMutex L(SOUNDMAN->lock);
position = int(fSeconds * samplerate);
if( fSeconds < 0 )
fSeconds = 0;
if(big) {
ASSERT(stream.Sample);
Sound_AccurateSeek(stream.Sample, int(fSeconds * 1000));
stream.buf.clear();
}
}
void RageSound::SetPlaybackRate( float fScale )
{
LockMutex L(SOUNDMAN->lock);
speed = fScale;
}
/* This is used to start music. It probably belongs in RageSoundManager. */
void RageSound::LoadAndPlayIfNotAlready( CString sSoundFilePath )
{
SOUNDMAN->lock.Lock();
if( GetLoadedFilePath() == sSoundFilePath && IsPlaying() )
return; // do nothing
SOUNDMAN->lock.Unlock();
Load( sSoundFilePath );
SetLooping();
Play();
}
void CircBuf::reserve(unsigned n)
{
clear();
buf.erase();
buf.insert(buf.end(), n, 0);
}
void CircBuf::clear()
{
cnt = start = 0;
}
void CircBuf::write(const char *buffer, unsigned buffer_size)
{
ASSERT(size() + buffer_size <= capacity()); /* overflow */
while(buffer_size)
{
unsigned write_pos = start + size();
if(write_pos >= buf.size()) write_pos -= buf.size();
int cpy = min(buffer_size, buf.size() - write_pos);
buf.replace(write_pos, cpy, buffer, cpy);
cnt += cpy;
buffer += cpy;
buffer_size -= cpy;
}
}
void CircBuf::read(char *buffer, unsigned buffer_size)
{
ASSERT(size() >= buffer_size); /* underflow */
while(buffer_size)
{
unsigned total = min(buf.size() - start, size());
unsigned cpy = min(buffer_size, total);
buf.copy(buffer, cpy, start);
start += cpy;
if(start == buf.size()) start = 0;
cnt -= cpy;
buffer += cpy;
buffer_size -= cpy;
}
}