2005-04-23 17:24:24 +00:00
|
|
|
//
|
|
|
|
// OOALSASound.m: Interface for oolite to the Advanced Linux Sound
|
|
|
|
// Achitecture.
|
|
|
|
// Implements methods from NSSound which are used by oolite.
|
|
|
|
//
|
|
|
|
// Note: this only implements as much as oolite needs from NSSound.
|
|
|
|
// It's also a bit of a stopgap measure since GNUstep NSSound doesn't
|
|
|
|
// yet support ALSA and the sound server crashes all the time.
|
|
|
|
//
|
|
|
|
// Dylan Smith, 2005-04-22
|
|
|
|
//
|
|
|
|
#import <Foundation/NSData.h>
|
|
|
|
#import <Foundation/NSThread.h>
|
|
|
|
#import <Foundation/NSLock.h>
|
|
|
|
#import "OOAlsaSound.h"
|
|
|
|
#include <math.h>
|
|
|
|
|
|
|
|
@implementation OOSound
|
|
|
|
|
|
|
|
- (BOOL) pause
|
|
|
|
{
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL) isPlaying
|
|
|
|
{
|
|
|
|
return isPlaying;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL) play
|
|
|
|
{
|
|
|
|
if(!soundThread)
|
|
|
|
{
|
|
|
|
soundThread = [[OOAlsaSoundThread alloc] init];
|
|
|
|
}
|
|
|
|
|
|
|
|
// do we need to upsample?
|
|
|
|
if(_samplingRate < SAMPLERATE)
|
|
|
|
{
|
|
|
|
NSLog(@"sampleRate was %f; resampling", _samplingRate);
|
|
|
|
[self resample];
|
|
|
|
}
|
|
|
|
[soundThread playBuffer: self];
|
2005-04-30 23:25:21 +00:00
|
|
|
return YES;
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL) stop
|
|
|
|
{
|
|
|
|
[soundThread stopTrack: self];
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL) resume
|
|
|
|
{
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) dealloc
|
|
|
|
{
|
|
|
|
[soundThread stopTrack: self];
|
|
|
|
[super dealloc];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) resample
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
int stretchSample=SAMPLERATE / _samplingRate;
|
|
|
|
long newDataSize=_dataSize * stretchSample;
|
2005-04-27 21:11:19 +00:00
|
|
|
Frame *newBuf=(Frame *)malloc(newDataSize);
|
|
|
|
Frame *newBufPtr=newBuf;
|
|
|
|
const Frame *oldBuf=(const Frame *)[_data bytes];
|
|
|
|
const Frame *oldBufPtr;
|
|
|
|
const Frame *oldBufEnd=oldBuf+(_dataSize / sizeof(Frame));
|
|
|
|
|
|
|
|
Sample lastChana;
|
|
|
|
Sample lastChanb;
|
|
|
|
Sample thisChana=0;
|
|
|
|
Sample thisChanb=0;
|
|
|
|
|
|
|
|
for(oldBufPtr=oldBuf; oldBufPtr < oldBufEnd; oldBufPtr ++)
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-27 21:11:19 +00:00
|
|
|
// Keep the last sample value and split out the ones
|
|
|
|
// we are looking at (or use zero if we're at the first
|
|
|
|
// frame of the buffer). Then make a simple straight line
|
|
|
|
// between the two to antialise the sound that's being
|
|
|
|
// resampled.
|
|
|
|
lastChana=thisChana;
|
|
|
|
lastChanb=thisChanb;
|
|
|
|
|
|
|
|
thisChana=(*oldBufPtr & 0xFFFF0000) >> 16;
|
|
|
|
thisChanb=*oldBufPtr & 0x0000FFFF;
|
|
|
|
|
|
|
|
short stepChana=(thisChana-lastChana) / stretchSample;
|
|
|
|
short stepChanb=(thisChanb-lastChanb) / stretchSample;
|
|
|
|
|
|
|
|
// we'll increment chana and chanb by the step as we
|
|
|
|
// write them.
|
|
|
|
short chana=lastChana;
|
|
|
|
short chanb=lastChanb;
|
|
|
|
|
2005-04-23 17:24:24 +00:00
|
|
|
for(i=0; i < stretchSample; i++)
|
|
|
|
{
|
2005-04-27 21:11:19 +00:00
|
|
|
*newBufPtr=chana;
|
|
|
|
*newBufPtr <<= 16;
|
|
|
|
|
|
|
|
// see later comment about shorts being aligned on longword
|
|
|
|
// boundaries.
|
|
|
|
*newBufPtr |= (chanb & 0x0000FFFF);
|
|
|
|
chana+=stepChana;
|
|
|
|
chanb+=stepChanb;
|
|
|
|
newBufPtr++;
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
[_data release];
|
|
|
|
_data=[NSData dataWithBytes: newBuf length: newDataSize];
|
|
|
|
[_data retain];
|
|
|
|
_samplingRate=SAMPLERATE;
|
|
|
|
_dataSize=newDataSize;
|
|
|
|
_frameCount=newDataSize >> 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
// These methods reveal the internals of the NSSound.
|
|
|
|
- (NSData *)getData
|
|
|
|
{
|
|
|
|
return _data;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Float!? Surely an integer Mr. Stallman!
|
2005-04-30 20:41:06 +00:00
|
|
|
- (float)getSampleRate
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
|
|
|
return _samplingRate;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (float)getFrameSize
|
|
|
|
{
|
|
|
|
return _frameSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (long)getDataSize
|
|
|
|
{
|
|
|
|
return _dataSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (long)getFrameCount
|
|
|
|
{
|
|
|
|
return _frameCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (int)getChannelCount
|
|
|
|
{
|
|
|
|
return _channelCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)resetState
|
|
|
|
{
|
|
|
|
isPlaying=NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)startup
|
|
|
|
{
|
|
|
|
isPlaying=YES;
|
|
|
|
}
|
|
|
|
|
2005-04-23 23:25:30 +00:00
|
|
|
- (const unsigned char *)getBufferEnd;
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-23 23:25:30 +00:00
|
|
|
const unsigned char *buf=[_data bytes];
|
2005-04-23 17:24:24 +00:00
|
|
|
buf+=_dataSize;
|
|
|
|
return buf;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
// Now here comes the really yucky stuff.
|
|
|
|
// Some of this may not be strictly necesary, but the ALSA documentation
|
|
|
|
// is truly abysmal. If you know ALSA better and can make a better
|
|
|
|
// implementation of this I won't feel insulted, trust me :-) In fact
|
|
|
|
// I think I might be a little bit relieved.
|
|
|
|
@implementation OOAlsaSoundThread
|
|
|
|
|
|
|
|
- (id)init
|
|
|
|
{
|
2005-04-30 20:41:06 +00:00
|
|
|
// Crank up the player thread.
|
2005-04-23 17:24:24 +00:00
|
|
|
[NSThread detachNewThreadSelector:@selector(soundThread:)
|
|
|
|
toTarget:self
|
|
|
|
withObject: nil];
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)soundThread: (id)obj
|
|
|
|
{
|
|
|
|
NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
|
|
|
|
if(![self initAlsa])
|
|
|
|
{
|
|
|
|
NSLog(@"initAlsa returned NO, sound thread aborting");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2005-04-30 20:41:06 +00:00
|
|
|
|
|
|
|
unsigned short int silence[1024];
|
|
|
|
memset(silence, 0, sizeof(silence));
|
|
|
|
|
|
|
|
// Prepare/reset the PCM device for playback.
|
|
|
|
// According to ALSA docs, just opening the device
|
|
|
|
// implicitly prepares it, but it can't hurt to make
|
|
|
|
// sure, especially knowing ALSA consistency...
|
|
|
|
|
|
|
|
snd_pcm_prepare(pcm_handle);
|
|
|
|
|
2005-04-23 17:24:24 +00:00
|
|
|
// do this forever
|
2005-04-30 20:41:06 +00:00
|
|
|
// Note we have to play silence instead of stopping, it seems
|
|
|
|
// to prevent problems with stalls/buffer underruns with some
|
|
|
|
// hardware. It apparently uses more CPU to actually stop than
|
|
|
|
// keep continously feeding the sound card.
|
2005-04-23 17:24:24 +00:00
|
|
|
while(1)
|
2005-04-30 20:41:06 +00:00
|
|
|
{
|
|
|
|
int res;
|
2005-04-23 17:24:24 +00:00
|
|
|
// play it one chunk at a time.
|
2005-04-30 20:41:06 +00:00
|
|
|
if([self mixChunks])
|
|
|
|
{
|
|
|
|
res = snd_pcm_writei(pcm_handle, chunkBuf, bufsz);
|
|
|
|
}
|
|
|
|
else // Nothing to play? Play silence so we don't get an underrun.
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-30 20:41:06 +00:00
|
|
|
res = snd_pcm_writei(pcm_handle, silence, sizeof(silence) / 4);
|
|
|
|
}
|
|
|
|
// Check for errors/problems
|
|
|
|
if (res < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA: error during snd_pcm_writei() (%s), resetting stream.",
|
|
|
|
snd_strerror(res));
|
|
|
|
snd_pcm_prepare(pcm_handle);
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[pool release];
|
|
|
|
}
|
|
|
|
|
|
|
|
// do all the initialization. The ALSA website doesn't document this
|
|
|
|
// very well but the function calls are in the main self explanatory.
|
|
|
|
- (BOOL) initAlsa
|
|
|
|
{
|
|
|
|
int dir;
|
2005-04-28 20:15:20 +00:00
|
|
|
int periods = PERIODS;
|
|
|
|
snd_pcm_uframes_t periodsize = PERIODSIZE;
|
|
|
|
bufsz=MIXBUFSIZE;
|
|
|
|
|
2005-04-28 18:27:53 +00:00
|
|
|
int i;
|
|
|
|
|
|
|
|
// init track pointers.
|
|
|
|
for(i=0; i < MAXTRACKS; i++)
|
|
|
|
{
|
|
|
|
trackBuffer[i].buf=NULL;
|
|
|
|
trackBuffer[i].bufEnd=NULL;
|
|
|
|
}
|
2005-04-23 17:24:24 +00:00
|
|
|
|
|
|
|
snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;
|
|
|
|
|
|
|
|
// init the PCM name. TODO: perhaps load this from a config plist?
|
|
|
|
// wanna get it working first...
|
|
|
|
char *pcm_name=strdup("default");
|
|
|
|
snd_pcm_hw_params_alloca(&hwparams);
|
|
|
|
|
|
|
|
// open it up
|
|
|
|
if(snd_pcm_open(&pcm_handle, pcm_name, stream, 0) < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA failed to initialize %s", pcm_name);
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(snd_pcm_hw_params_any(pcm_handle, hwparams) < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA cannot configure PCM device");
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: investigate mmaped access
|
|
|
|
if(snd_pcm_hw_params_set_access(pcm_handle, hwparams,
|
|
|
|
SND_PCM_ACCESS_RW_INTERLEAVED) < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA failed to set access");
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
// assumption...
|
|
|
|
if(snd_pcm_hw_params_set_format(pcm_handle, hwparams,
|
|
|
|
SND_PCM_FORMAT_S16_LE) < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA failed to set the format");
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
// set sample rate and channels
|
|
|
|
int exact_rate=SAMPLERATE;
|
|
|
|
if(snd_pcm_hw_params_set_rate_near
|
|
|
|
(pcm_handle, hwparams, &exact_rate, &dir))
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA can't set the rate %d", exact_rate);
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(snd_pcm_hw_params_set_channels(pcm_handle, hwparams, CHANNELS) < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA can't set channels to %d", CHANNELS);
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: find out exactly what periods are useful for.
|
2005-04-28 20:15:20 +00:00
|
|
|
snd_pcm_uframes_t origPeriods=periods;
|
|
|
|
int periodDir=0;
|
|
|
|
if(snd_pcm_hw_params_set_periods_near
|
|
|
|
(pcm_handle, hwparams, &periods, &periodDir) < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA could not set periods");
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
// http://www.suse.de/~mana/alsa090_howto.html
|
|
|
|
// bufsz is the size in frames not bytes
|
|
|
|
unsigned long hwbufsz=(periods * periodsize) >> 2;
|
|
|
|
unsigned long origbufsz=hwbufsz;
|
|
|
|
|
|
|
|
if(origPeriods != periods)
|
|
|
|
{
|
|
|
|
NSLog(@"Tried to set %d periods but ended up with %d",
|
|
|
|
origPeriods, periods);
|
|
|
|
}
|
2005-04-27 21:11:19 +00:00
|
|
|
|
|
|
|
// bufsz = buffer size in frames. fpp = frames per period.
|
|
|
|
// We try to allocate a buffer of the number of frames per
|
|
|
|
// period but it might not happen.
|
|
|
|
if(snd_pcm_hw_params_set_buffer_size_near
|
2005-04-28 18:27:53 +00:00
|
|
|
(pcm_handle, hwparams, &hwbufsz) < 0)
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-28 18:27:53 +00:00
|
|
|
NSLog(@"ALSA could not set the buffer size to %d", hwbufsz);
|
2005-04-23 17:24:24 +00:00
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2005-04-28 20:15:20 +00:00
|
|
|
if(hwbufsz != origbufsz)
|
2005-04-27 21:11:19 +00:00
|
|
|
{
|
|
|
|
NSLog(@"Sound card can't take a buffer of %d - using %d instead",
|
2005-04-28 20:15:20 +00:00
|
|
|
origbufsz, hwbufsz);
|
2005-04-28 18:27:53 +00:00
|
|
|
|
|
|
|
// If the hwbufsz is smaller than our default bufsz, downsize
|
|
|
|
// bufsz.
|
|
|
|
if(hwbufsz < bufsz)
|
|
|
|
bufsz=hwbufsz;
|
2005-04-27 21:11:19 +00:00
|
|
|
}
|
|
|
|
|
2005-04-28 18:27:53 +00:00
|
|
|
// convert bufsz to bytes and allocate our mixer buffer.
|
|
|
|
chunkBuf=(Frame *)malloc(bufsz * FRAMESIZE);
|
2005-04-27 21:11:19 +00:00
|
|
|
|
2005-04-23 17:24:24 +00:00
|
|
|
if(snd_pcm_hw_params(pcm_handle, hwparams) < 0)
|
|
|
|
{
|
|
|
|
NSLog(@"ALSA could not set HW params");
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2005-04-30 20:41:06 +00:00
|
|
|
// playBuffer doesn't actually do the playing - it merely adds the
|
|
|
|
// sound to the list of tracks the player thread should mix.
|
2005-04-23 17:24:24 +00:00
|
|
|
- (void) playBuffer: (OOSound *)sound
|
|
|
|
{
|
|
|
|
NSLock *addlock=[[NSLock alloc] init];
|
|
|
|
[addlock lock];
|
|
|
|
int i;
|
|
|
|
int slot=0;
|
|
|
|
BOOL slotAssigned=NO;
|
|
|
|
for(i=0; i < MAXTRACKS; i++)
|
|
|
|
{
|
|
|
|
if(!track[i] && !slotAssigned)
|
|
|
|
{
|
|
|
|
slot=i;
|
|
|
|
slotAssigned=YES;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!slotAssigned)
|
|
|
|
{
|
|
|
|
NSLog(@"No free tracks");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2005-04-30 20:41:06 +00:00
|
|
|
[sound retain]; // we don't want it unexpectedly disappearing
|
2005-04-23 17:24:24 +00:00
|
|
|
track[slot]=sound;
|
2005-04-28 18:27:53 +00:00
|
|
|
trackBuffer[slot].buf=(Frame *)[[sound getData] bytes];
|
|
|
|
trackBuffer[slot].bufEnd=(Frame *)[sound getBufferEnd];
|
2005-04-23 17:24:24 +00:00
|
|
|
[sound startup];
|
|
|
|
[addlock unlock];
|
|
|
|
}
|
|
|
|
|
2005-04-28 18:27:53 +00:00
|
|
|
- (BOOL) mixChunks
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
|
|
|
int i;
|
2005-04-28 18:27:53 +00:00
|
|
|
Frame *current;
|
2005-04-23 17:24:24 +00:00
|
|
|
|
2005-04-28 18:27:53 +00:00
|
|
|
// Check there's something to mix.
|
|
|
|
for(i=0; i < MAXTRACKS; i++)
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-28 18:27:53 +00:00
|
|
|
if(track[i])
|
|
|
|
break;
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
2005-04-28 18:27:53 +00:00
|
|
|
if(i == MAXTRACKS)
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-28 18:27:53 +00:00
|
|
|
// No tracks to do, exit now.
|
|
|
|
return NO;
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
|
|
|
|
2005-04-28 18:27:53 +00:00
|
|
|
for(current=chunkBuf; current < chunkBuf + bufsz; current++)
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-26 22:12:06 +00:00
|
|
|
long sumChanA=0;
|
|
|
|
long sumChanB=0;
|
2005-04-28 18:27:53 +00:00
|
|
|
for(i=0; i < MAXTRACKS; i++)
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-30 20:41:06 +00:00
|
|
|
if(!trackBuffer[i].buf) continue;
|
|
|
|
if(trackBuffer[i].buf >= trackBuffer[i].bufEnd) continue;
|
|
|
|
sumChanA += (long)*trackBuffer[i].buf >> 16;
|
|
|
|
sumChanB += (long)(*trackBuffer[i].buf << 16) >> 16;
|
|
|
|
trackBuffer[i].buf++;
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
2005-04-26 22:12:06 +00:00
|
|
|
|
2005-04-30 20:41:06 +00:00
|
|
|
if(sumChanA > 32767) sumChanA=32767;
|
|
|
|
else if(sumChanA < -32768) sumChanA=-32768;
|
|
|
|
if(sumChanB > 32767) sumChanB=32767;
|
|
|
|
else if(sumChanB < -32768) sumChanB=-32768;
|
|
|
|
|
|
|
|
*current = (sumChanA << 16) | (sumChanB & 0xffff);
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
|
|
|
|
2005-04-28 18:27:53 +00:00
|
|
|
// drop tracks we don't want any more
|
|
|
|
for(i=0; i < MAXTRACKS; i++)
|
2005-04-23 17:24:24 +00:00
|
|
|
{
|
2005-04-28 18:27:53 +00:00
|
|
|
if(trackBuffer[i].buf && trackBuffer[i].buf == trackBuffer[i].bufEnd)
|
|
|
|
{
|
2005-04-28 20:40:56 +00:00
|
|
|
[track[i] resetState];
|
2005-04-28 18:27:53 +00:00
|
|
|
[track[i] release];
|
|
|
|
track[i]=nil;
|
|
|
|
trackBuffer[i].buf=NULL;
|
|
|
|
}
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
2005-04-28 18:27:53 +00:00
|
|
|
return YES;
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void) stopTrack: (OOSound *)trackToStop
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
for(i=0; i < MAXTRACKS; i++)
|
|
|
|
{
|
|
|
|
if(trackToStop == track[i])
|
|
|
|
{
|
|
|
|
track[i]=nil;
|
2005-04-28 18:27:53 +00:00
|
|
|
trackBuffer[i].buf=NULL;
|
2005-04-23 17:24:24 +00:00
|
|
|
[trackToStop resetState];
|
2005-04-23 23:25:30 +00:00
|
|
|
[trackToStop release];
|
2005-04-23 17:24:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|