|
Packit |
c32a2d |
/*
|
|
Packit |
c32a2d |
wav.c: write wav/au/cdr files (and headerless raw
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
copyright ?-2015 by the mpg123 project - free software under the terms of the LGPL 2.1
|
|
Packit |
c32a2d |
see COPYING and AUTHORS files in distribution or http://mpg123.org
|
|
Packit |
c32a2d |
initially written by Samuel Audet
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
Geez, why are WAV RIFF headers are so secret? I got something together,
|
|
Packit |
c32a2d |
but wow... anyway, I hope someone will find this useful.
|
|
Packit |
c32a2d |
- Samuel Audet
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
minor simplifications and ugly AU/CDR format stuff by MH
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
It's not a very clean code ... Fix this!
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
ThOr: The usage of stdio streams means we loose control over what data is actually written. On a full disk, fwrite() happily suceeds for ages, only a fflush fails.
|
|
Packit |
c32a2d |
Now: Do we want to fflush() after every write? That defeats the purpose of buffered I/O. So, switching to good old write() is an option (kernel doing disk buffering anyway).
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
ThOr: Again reworked things for libout123, with non-static state.
|
|
Packit |
c32a2d |
This set of builtin "modules" is what we can use in automated tests
|
|
Packit |
c32a2d |
of libout123. Code still not very nice, but I tried to keep modification
|
|
Packit |
c32a2d |
of what stood the test of time minimal. One still can add a module to
|
|
Packit |
c32a2d |
libout123 that uses sndfile and similar libraries for more choice on writing
|
|
Packit |
c32a2d |
output files.
|
|
Packit |
c32a2d |
*/
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
#include "out123_int.h"
|
|
Packit |
c32a2d |
#include "wav.h"
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
#include <errno.h>
|
|
Packit |
c32a2d |
#include "debug.h"
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* Create the two WAV headers. */
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
#define WAVE_FORMAT 1
|
|
Packit |
c32a2d |
#define RIFF_NAME riff_template
|
|
Packit |
c32a2d |
#define RIFF_STRUCT_NAME riff
|
|
Packit |
c32a2d |
#include "wavhead.h"
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
#undef WAVE_FORMAT
|
|
Packit |
c32a2d |
#undef RIFF_NAME
|
|
Packit |
c32a2d |
#undef RIFF_STRUCT_NAME
|
|
Packit |
c32a2d |
#define WAVE_FORMAT 3
|
|
Packit |
c32a2d |
#define RIFF_NAME riff_float_template
|
|
Packit |
c32a2d |
#define RIFF_STRUCT_NAME riff_float
|
|
Packit |
c32a2d |
#define FLOATOUT
|
|
Packit |
c32a2d |
#include "wavhead.h"
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* AU header struct... */
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
struct auhead {
|
|
Packit |
c32a2d |
byte magic[4];
|
|
Packit |
c32a2d |
byte headlen[4];
|
|
Packit |
c32a2d |
byte datalen[4];
|
|
Packit |
c32a2d |
byte encoding[4];
|
|
Packit |
c32a2d |
byte rate[4];
|
|
Packit |
c32a2d |
byte channels[4];
|
|
Packit |
c32a2d |
byte dummy[8];
|
|
Packit |
c32a2d |
} const auhead_template = {
|
|
Packit |
c32a2d |
{ 0x2e,0x73,0x6e,0x64 } , { 0x00,0x00,0x00,0x20 } ,
|
|
Packit |
c32a2d |
{ 0xff,0xff,0xff,0xff } , { 0,0,0,0 } , { 0,0,0,0 } , { 0,0,0,0 } ,
|
|
Packit |
c32a2d |
{ 0,0,0,0,0,0,0,0 }};
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
struct wavdata
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
FILE *wavfp;
|
|
Packit |
c32a2d |
long datalen;
|
|
Packit |
c32a2d |
int flipendian;
|
|
Packit |
c32a2d |
int bytes_per_sample;
|
|
Packit |
c32a2d |
int floatwav; /* If we write a floating point WAV file. */
|
|
Packit |
c32a2d |
/*
|
|
Packit |
c32a2d |
Open routines only prepare a header, stored here and written on first
|
|
Packit |
c32a2d |
actual data write. If no data is written at all, proper files will
|
|
Packit |
c32a2d |
still get a header via the update at closing; non-seekable streams will
|
|
Packit |
c32a2d |
just have no no header if there is no data.
|
|
Packit |
c32a2d |
*/
|
|
Packit |
c32a2d |
void *the_header;
|
|
Packit |
c32a2d |
size_t the_header_size;
|
|
Packit |
c32a2d |
};
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
static struct wavdata* wavdata_new(void)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = malloc(sizeof(struct wavdata));
|
|
Packit |
c32a2d |
if(wdat)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
wdat->wavfp = NULL;
|
|
Packit |
c32a2d |
wdat->datalen = 0;
|
|
Packit |
c32a2d |
wdat->flipendian = 0;
|
|
Packit |
c32a2d |
wdat->bytes_per_sample = -1;
|
|
Packit |
c32a2d |
wdat->floatwav = 0;
|
|
Packit |
c32a2d |
wdat->the_header = NULL;
|
|
Packit |
c32a2d |
wdat->the_header_size = 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
return wdat;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
static void wavdata_del(struct wavdata *wdat)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!wdat) return;
|
|
Packit |
c32a2d |
if(wdat->wavfp && wdat->wavfp != stdout)
|
|
Packit |
c32a2d |
compat_fclose(wdat->wavfp);
|
|
Packit |
c32a2d |
if(wdat->the_header)
|
|
Packit |
c32a2d |
free(wdat->the_header);
|
|
Packit |
c32a2d |
free(wdat);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* Pointer types are for pussies;-) */
|
|
Packit |
c32a2d |
static void* wavhead_new(void const *template, size_t size)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
void *header = malloc(size);
|
|
Packit |
c32a2d |
if(header)
|
|
Packit |
c32a2d |
memcpy(header, template, size);
|
|
Packit |
c32a2d |
return header;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* Convertfunctions: */
|
|
Packit |
c32a2d |
/* always little endian */
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
static void long2littleendian(long inval,byte *outval,int b)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
int i;
|
|
Packit |
c32a2d |
for(i=0;i
|
|
Packit |
c32a2d |
outval[i] = (inval>>(i*8)) & 0xff;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* always big endian */
|
|
Packit |
c32a2d |
static void long2bigendian(long inval,byte *outval,int b)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
int i;
|
|
Packit |
c32a2d |
for(i=0;i
|
|
Packit |
c32a2d |
outval[i] = (inval>>((b-i-1)*8)) & 0xff;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
static long from_little(byte *inval, int b)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long ret = 0;
|
|
Packit |
c32a2d |
int i;
|
|
Packit |
c32a2d |
for(i=0;i
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
return ret;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
static int testEndian(void)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long i,a=0,b=0,c=0;
|
|
Packit |
c32a2d |
int ret = 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
for(i=0;i
|
|
Packit |
c32a2d |
((byte *)&a)[i] = i;
|
|
Packit |
c32a2d |
b<<=8;
|
|
Packit |
c32a2d |
b |= i;
|
|
Packit |
c32a2d |
c |= i << (i*8);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
if(a == b)
|
|
Packit |
c32a2d |
ret = 1;
|
|
Packit |
c32a2d |
else if(a != c) {
|
|
Packit |
c32a2d |
ret = -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
return ret;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* return: 0 is good, -1 is bad */
|
|
Packit |
c32a2d |
static int open_file(struct wavdata *wdat, char *filename)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
debug2("open_file(%p, %s)", (void*)wdat, filename ? filename : "<nil>");
|
|
Packit |
c32a2d |
if(!wdat)
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
#if defined(HAVE_SETUID) && defined(HAVE_GETUID)
|
|
Packit |
c32a2d |
/* TODO: get rid of that and settle that you rather not install mpg123
|
|
Packit |
c32a2d |
setuid-root. Why should you?
|
|
Packit |
c32a2d |
In case this program is setuid, create files owned by original user. */
|
|
Packit |
c32a2d |
setuid(getuid());
|
|
Packit |
c32a2d |
#endif
|
|
Packit |
c32a2d |
if(!filename || !strcmp("-",filename) || !strcmp("", filename))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
wdat->wavfp = stdout;
|
|
Packit |
c32a2d |
#ifdef WIN32
|
|
Packit |
c32a2d |
_setmode(STDOUT_FILENO, _O_BINARY);
|
|
Packit |
c32a2d |
#endif
|
|
Packit |
c32a2d |
/* If stdout is redirected to a file, seeks suddenly can work.
|
|
Packit |
c32a2d |
Doing one here to ensure that such a file has the same output
|
|
Packit |
c32a2d |
it had when opening directly as such. */
|
|
Packit |
c32a2d |
fseek(wdat->wavfp, 0L, SEEK_SET);
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
wdat->wavfp = compat_fopen(filename, "wb");
|
|
Packit |
c32a2d |
if(!wdat->wavfp)
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* return: 0 is good, -1 is bad
|
|
Packit |
c32a2d |
Works for any partial state of setup, especially should not complain if
|
|
Packit |
c32a2d |
ao->userptr == NULL. */
|
|
Packit |
c32a2d |
static int close_file(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = ao->userptr;
|
|
Packit |
c32a2d |
int ret = 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(wdat->wavfp != NULL && wdat->wavfp != stdout)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(compat_fclose(wdat->wavfp))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error1("problem closing the audio file, probably because of flushing to disk: %s\n", strerror(errno));
|
|
Packit |
c32a2d |
ret = -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* Always cleanup here. */
|
|
Packit |
c32a2d |
wdat->wavfp = NULL;
|
|
Packit |
c32a2d |
wavdata_del(wdat);
|
|
Packit |
c32a2d |
ao->userptr = NULL;
|
|
Packit |
c32a2d |
return ret;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* return: 0 is good, -1 is bad */
|
|
Packit |
c32a2d |
static int write_header(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = ao->userptr;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat)
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(
|
|
Packit |
c32a2d |
wdat->the_header_size > 0
|
|
Packit |
c32a2d |
&& (
|
|
Packit |
c32a2d |
fwrite(wdat->the_header, wdat->the_header_size, 1, wdat->wavfp) != 1
|
|
Packit |
c32a2d |
|| fflush(wdat->wavfp)
|
|
Packit |
c32a2d |
)
|
|
Packit |
c32a2d |
)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error1("cannot write header: %s", strerror(errno));
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int au_open(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = NULL;
|
|
Packit |
c32a2d |
struct auhead *auhead = NULL;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(ao->format < 0)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->rate = 44100;
|
|
Packit |
c32a2d |
ao->channels = 2;
|
|
Packit |
c32a2d |
ao->format = MPG123_ENC_SIGNED_16;
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(ao->format & MPG123_ENC_FLOAT)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error("AU file support for float values not there yet");
|
|
Packit |
c32a2d |
goto au_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(
|
|
Packit |
c32a2d |
!(wdat = wavdata_new())
|
|
Packit |
c32a2d |
|| !(auhead = wavhead_new(&auhead_template, sizeof(auhead_template)))
|
|
Packit |
c32a2d |
)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->errcode = OUT123_DOOM;
|
|
Packit |
c32a2d |
goto au_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
wdat->the_header = auhead;
|
|
Packit |
c32a2d |
wdat->the_header_size = sizeof(*auhead);
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
wdat->flipendian = 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
switch(ao->format)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
case MPG123_ENC_SIGNED_16:
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
int endiantest = testEndian();
|
|
Packit |
c32a2d |
if(endiantest == -1)
|
|
Packit |
c32a2d |
goto au_open_bad;
|
|
Packit |
c32a2d |
wdat->flipendian = !endiantest; /* big end */
|
|
Packit |
c32a2d |
long2bigendian(3,auhead->encoding,sizeof(auhead->encoding));
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
break;
|
|
Packit |
c32a2d |
case MPG123_ENC_UNSIGNED_8:
|
|
Packit |
c32a2d |
ao->format = MPG123_ENC_ULAW_8;
|
|
Packit |
c32a2d |
case MPG123_ENC_ULAW_8:
|
|
Packit |
c32a2d |
long2bigendian(1,auhead->encoding,sizeof(auhead->encoding));
|
|
Packit |
c32a2d |
break;
|
|
Packit |
c32a2d |
default:
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error("AU output is only a hack. This audio mode isn't supported yet.");
|
|
Packit |
c32a2d |
goto au_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
long2bigendian(0xffffffff,auhead->datalen,sizeof(auhead->datalen));
|
|
Packit |
c32a2d |
long2bigendian(ao->rate,auhead->rate,sizeof(auhead->rate));
|
|
Packit |
c32a2d |
long2bigendian(ao->channels,auhead->channels,sizeof(auhead->channels));
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(open_file(wdat, ao->device) < 0)
|
|
Packit |
c32a2d |
goto au_open_bad;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
wdat->datalen = 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
ao->userptr = wdat;
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
au_open_bad:
|
|
Packit |
c32a2d |
if(auhead)
|
|
Packit |
c32a2d |
free(auhead);
|
|
Packit |
c32a2d |
if(wdat)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
wdat->the_header = NULL;
|
|
Packit |
c32a2d |
wavdata_del(wdat);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int cdr_open(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = NULL;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(ao->format < 0)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->rate = 44100;
|
|
Packit |
c32a2d |
ao->channels = 2;
|
|
Packit |
c32a2d |
ao->format = MPG123_ENC_SIGNED_16;
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(
|
|
Packit |
c32a2d |
ao->format != MPG123_ENC_SIGNED_16
|
|
Packit |
c32a2d |
|| ao->rate != 44100
|
|
Packit |
c32a2d |
|| ao->channels != 2
|
|
Packit |
c32a2d |
)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error("Oops .. not forced to 16 bit, 44 kHz, stereo?");
|
|
Packit |
c32a2d |
goto cdr_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!(wdat = wavdata_new()))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->errcode = OUT123_DOOM;
|
|
Packit |
c32a2d |
goto cdr_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
wdat->flipendian = !testEndian(); /* big end */
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(open_file(wdat, ao->device) < 0)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error("cannot open file for writing");
|
|
Packit |
c32a2d |
goto cdr_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
ao->userptr = wdat;
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
cdr_open_bad:
|
|
Packit |
c32a2d |
if(wdat)
|
|
Packit |
c32a2d |
wavdata_del(wdat);
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* RAW files are headerless WAVs where the format does not matter. */
|
|
Packit |
c32a2d |
int raw_open(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(ao->format < 0)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->rate = 44100;
|
|
Packit |
c32a2d |
ao->channels = 2;
|
|
Packit |
c32a2d |
ao->format = MPG123_ENC_SIGNED_16;
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!(wdat = wavdata_new()))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->errcode = OUT123_DOOM;
|
|
Packit |
c32a2d |
goto raw_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(open_file(wdat, ao->device) < 0)
|
|
Packit |
c32a2d |
goto raw_open_bad;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
ao->userptr = wdat;
|
|
Packit |
c32a2d |
return 1;
|
|
Packit |
c32a2d |
raw_open_bad:
|
|
Packit |
c32a2d |
if(wdat)
|
|
Packit |
c32a2d |
wavdata_del(wdat);
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int wav_open(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
int bps;
|
|
Packit |
c32a2d |
struct wavdata *wdat = NULL;
|
|
Packit |
c32a2d |
struct riff *inthead = NULL;
|
|
Packit |
c32a2d |
struct riff_float *floathead = NULL;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(ao->format < 0)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->rate = 44100;
|
|
Packit |
c32a2d |
ao->channels = 2;
|
|
Packit |
c32a2d |
ao->format = MPG123_ENC_SIGNED_16;
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!(wdat = wavdata_new()))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->errcode = OUT123_DOOM;
|
|
Packit |
c32a2d |
goto wav_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
wdat->floatwav = (ao->format & MPG123_ENC_FLOAT);
|
|
Packit |
c32a2d |
if(wdat->floatwav)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!(floathead = wavhead_new( &riff_float_template
|
|
Packit |
c32a2d |
, sizeof(riff_float_template)) ))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->errcode = OUT123_DOOM;
|
|
Packit |
c32a2d |
goto wav_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
wdat->the_header = floathead;
|
|
Packit |
c32a2d |
wdat->the_header_size = sizeof(*floathead);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!(inthead = wavhead_new( &riff_template
|
|
Packit |
c32a2d |
, sizeof(riff_template)) ))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
ao->errcode = OUT123_DOOM;
|
|
Packit |
c32a2d |
goto wav_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
wdat->the_header = inthead;
|
|
Packit |
c32a2d |
wdat->the_header_size = sizeof(*inthead);
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* standard MS PCM, and its format specific is BitsPerSample */
|
|
Packit |
c32a2d |
long2littleendian(1, inthead->WAVE.fmt.FormatTag
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.FormatTag));
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(ao->format == MPG123_ENC_FLOAT_32)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(3, floathead->WAVE.fmt.FormatTag
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.fmt.FormatTag));
|
|
Packit |
c32a2d |
long2littleendian(bps=32, floathead->WAVE.fmt.BitsPerSample
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.fmt.BitsPerSample));
|
|
Packit |
c32a2d |
wdat->flipendian = testEndian();
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else if(ao->format == MPG123_ENC_SIGNED_32)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(bps=32, inthead->WAVE.fmt.BitsPerSample
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.BitsPerSample));
|
|
Packit |
c32a2d |
wdat->flipendian = testEndian();
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else if(ao->format == MPG123_ENC_SIGNED_24)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(bps=24, inthead->WAVE.fmt.BitsPerSample
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.BitsPerSample));
|
|
Packit |
c32a2d |
wdat->flipendian = testEndian();
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else if(ao->format == MPG123_ENC_SIGNED_16)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(bps=16, inthead->WAVE.fmt.BitsPerSample
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.BitsPerSample));
|
|
Packit |
c32a2d |
wdat->flipendian = testEndian();
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else if(ao->format == MPG123_ENC_UNSIGNED_8)
|
|
Packit |
c32a2d |
long2littleendian(bps=8, inthead->WAVE.fmt.BitsPerSample
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.BitsPerSample));
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error("Format not supported.");
|
|
Packit |
c32a2d |
goto wav_open_bad;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(wdat->floatwav)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(ao->channels, floathead->WAVE.fmt.Channels
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.fmt.Channels));
|
|
Packit |
c32a2d |
long2littleendian(ao->rate, floathead->WAVE.fmt.SamplesPerSec
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.fmt.SamplesPerSec));
|
|
Packit |
c32a2d |
long2littleendian( (int)(ao->channels * ao->rate * bps)>>3
|
|
Packit |
c32a2d |
, floathead->WAVE.fmt.AvgBytesPerSec
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.fmt.AvgBytesPerSec) );
|
|
Packit |
c32a2d |
long2littleendian( (int)(ao->channels * bps)>>3
|
|
Packit |
c32a2d |
, floathead->WAVE.fmt.BlockAlign
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.fmt.BlockAlign) );
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(ao->channels, inthead->WAVE.fmt.Channels
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.Channels));
|
|
Packit |
c32a2d |
long2littleendian(ao->rate, inthead->WAVE.fmt.SamplesPerSec
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.SamplesPerSec));
|
|
Packit |
c32a2d |
long2littleendian( (int)(ao->channels * ao->rate * bps)>>3
|
|
Packit |
c32a2d |
, inthead->WAVE.fmt.AvgBytesPerSec
|
|
Packit |
c32a2d |
,sizeof(inthead->WAVE.fmt.AvgBytesPerSec) );
|
|
Packit |
c32a2d |
long2littleendian( (int)(ao->channels * bps)>>3
|
|
Packit |
c32a2d |
, inthead->WAVE.fmt.BlockAlign
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.fmt.BlockAlign) );
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(open_file(wdat, ao->device) < 0)
|
|
Packit |
c32a2d |
goto wav_open_bad;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(wdat->floatwav)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(wdat->datalen, floathead->WAVE.data.datalen
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.data.datalen));
|
|
Packit |
c32a2d |
long2littleendian(wdat->datalen+sizeof(floathead->WAVE)
|
|
Packit |
c32a2d |
, floathead->WAVElen, sizeof(floathead->WAVElen));
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
long2littleendian(wdat->datalen, inthead->WAVE.data.datalen
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.data.datalen));
|
|
Packit |
c32a2d |
long2littleendian( wdat->datalen+sizeof(inthead->WAVE)
|
|
Packit |
c32a2d |
, inthead->WAVElen
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVElen) );
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
wdat->bytes_per_sample = bps>>3;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
ao->userptr = wdat;
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
wav_open_bad:
|
|
Packit |
c32a2d |
if(inthead)
|
|
Packit |
c32a2d |
free(inthead);
|
|
Packit |
c32a2d |
if(floathead)
|
|
Packit |
c32a2d |
free(floathead);
|
|
Packit |
c32a2d |
if(wdat)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
wdat->the_header = NULL;
|
|
Packit |
c32a2d |
wavdata_del(wdat);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int wav_write(out123_handle *ao, unsigned char *buf, int len)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = ao->userptr;
|
|
Packit |
c32a2d |
int temp;
|
|
Packit |
c32a2d |
int i;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat || !wdat->wavfp)
|
|
Packit |
c32a2d |
return 0; /* Really? Zero? */
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(wdat->datalen == 0 && write_header(ao) < 0)
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* Endianess conversion. Not fancy / optimized. */
|
|
Packit |
c32a2d |
if(wdat->flipendian)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(wdat->bytes_per_sample == 4) /* 32 bit */
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(len & 3)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error("Number of bytes no multiple of 4 (32bit)!");
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
for(i=0;i
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
int j;
|
|
Packit |
c32a2d |
unsigned char tmp[4];
|
|
Packit |
c32a2d |
for(j = 0; j<=3; ++j) tmp[j] = buf[i+j];
|
|
Packit |
c32a2d |
for(j = 0; j<=3; ++j) buf[i+j] = tmp[3-j];
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else /* 16 bit */
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(len & 1)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
error("Odd number of bytes!");
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
for(i=0;i
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
unsigned char tmp;
|
|
Packit |
c32a2d |
tmp = buf[i+0];
|
|
Packit |
c32a2d |
buf[i+0] = buf[i+1];
|
|
Packit |
c32a2d |
buf[i+1] = tmp;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
temp = fwrite(buf, 1, len, wdat->wavfp);
|
|
Packit |
c32a2d |
if(temp <= 0) return temp;
|
|
Packit |
c32a2d |
/* That would kill it of early when running out of disk space. */
|
|
Packit |
c32a2d |
#if 0
|
|
Packit |
c32a2d |
if(fflush(wdat->wavfp))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error1("flushing failed: %s\n", strerror(errno));
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
#endif
|
|
Packit |
c32a2d |
wdat->datalen += temp;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
return temp;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int wav_close(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = ao->userptr;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat) /* Special case: Opened only for format query. */
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat || !wdat->wavfp)
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* flush before seeking to catch out-of-disk explicitly at least at the end */
|
|
Packit |
c32a2d |
if(fflush(wdat->wavfp))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error1("cannot flush WAV stream: %s", strerror(errno));
|
|
Packit |
c32a2d |
return close_file(ao);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
if(fseek(wdat->wavfp, 0L, SEEK_SET) >= 0)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(wdat->floatwav)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct riff_float *floathead = wdat->the_header;
|
|
Packit |
c32a2d |
long2littleendian(wdat->datalen
|
|
Packit |
c32a2d |
, floathead->WAVE.data.datalen
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.data.datalen));
|
|
Packit |
c32a2d |
long2littleendian(wdat->datalen+sizeof(floathead->WAVE)
|
|
Packit |
c32a2d |
, floathead->WAVElen
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVElen));
|
|
Packit |
c32a2d |
long2littleendian( wdat->datalen
|
|
Packit |
c32a2d |
/ (
|
|
Packit |
c32a2d |
from_little(floathead->WAVE.fmt.Channels,2)
|
|
Packit |
c32a2d |
* from_little(floathead->WAVE.fmt.BitsPerSample,2)/8
|
|
Packit |
c32a2d |
)
|
|
Packit |
c32a2d |
, floathead->WAVE.fact.samplelen
|
|
Packit |
c32a2d |
, sizeof(floathead->WAVE.fact.samplelen) );
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct riff *inthead = wdat->the_header;
|
|
Packit |
c32a2d |
long2littleendian(wdat->datalen, inthead->WAVE.data.datalen
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVE.data.datalen));
|
|
Packit |
c32a2d |
long2littleendian(wdat->datalen+sizeof(inthead->WAVE), inthead->WAVElen
|
|
Packit |
c32a2d |
, sizeof(inthead->WAVElen));
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
/* Always (over)writing the header here; also for stdout, when
|
|
Packit |
c32a2d |
fseek worked, this overwrite works. */
|
|
Packit |
c32a2d |
write_header(ao);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else if(!AOQUIET)
|
|
Packit |
c32a2d |
warning("Cannot rewind WAV file. File-format isn't fully conform now.");
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
return close_file(ao);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int au_close(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = ao->userptr;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat) /* Special case: Opened only for format query. */
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat->wavfp)
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* flush before seeking to catch out-of-disk explicitly at least at the end */
|
|
Packit |
c32a2d |
if(fflush(wdat->wavfp))
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(!AOQUIET)
|
|
Packit |
c32a2d |
error1("cannot flush WAV stream: %s", strerror(errno));
|
|
Packit |
c32a2d |
return close_file(ao);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
if(fseek(wdat->wavfp, 0L, SEEK_SET) >= 0)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct auhead *auhead = wdat->the_header;
|
|
Packit |
c32a2d |
long2bigendian(wdat->datalen, auhead->datalen, sizeof(auhead->datalen));
|
|
Packit |
c32a2d |
/* Always (over)writing the header here; also for stdout, when
|
|
Packit |
c32a2d |
fseek worked, this overwrite works. */
|
|
Packit |
c32a2d |
write_header(ao);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
else if(!AOQUIET)
|
|
Packit |
c32a2d |
warning("Cannot rewind AU file. File-format isn't fully conform now.");
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
return close_file(ao);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* CDR data also uses that. */
|
|
Packit |
c32a2d |
int raw_close(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = ao->userptr;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat) /* Special case: Opened only for format query. */
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat->wavfp)
|
|
Packit |
c32a2d |
return -1;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
return close_file(ao);
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* Some trivial functions to interface with out123's module architecture. */
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int cdr_formats(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
if(ao->rate == 44100 && ao->channels == 2)
|
|
Packit |
c32a2d |
return MPG123_ENC_SIGNED_16;
|
|
Packit |
c32a2d |
else
|
|
Packit |
c32a2d |
return 0;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int au_formats(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
return MPG123_ENC_SIGNED_16|MPG123_ENC_UNSIGNED_8|MPG123_ENC_ULAW_8;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int raw_formats(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
return MPG123_ENC_ANY;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
int wav_formats(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
return
|
|
Packit |
c32a2d |
MPG123_ENC_SIGNED_16
|
|
Packit |
c32a2d |
| MPG123_ENC_UNSIGNED_8
|
|
Packit |
c32a2d |
| MPG123_ENC_FLOAT_32
|
|
Packit |
c32a2d |
| MPG123_ENC_SIGNED_24
|
|
Packit |
c32a2d |
| MPG123_ENC_SIGNED_32;
|
|
Packit |
c32a2d |
}
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
/* Draining is flushing to disk. Words do suck at times.
|
|
Packit |
c32a2d |
One could call fsync(), too, but to be safe, that would need to
|
|
Packit |
c32a2d |
be called on the directory, too. Also, apps randomly calling
|
|
Packit |
c32a2d |
fsync() can cause annoying issues in a system. */
|
|
Packit |
c32a2d |
void wav_drain(out123_handle *ao)
|
|
Packit |
c32a2d |
{
|
|
Packit |
c32a2d |
struct wavdata *wdat = ao->userptr;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(!wdat)
|
|
Packit |
c32a2d |
return;
|
|
Packit |
c32a2d |
|
|
Packit |
c32a2d |
if(fflush(wdat->wavfp) && !AOQUIET)
|
|
Packit |
c32a2d |
error1("flushing failed: %s\n", strerror(errno));
|
|
Packit |
c32a2d |
}
|