568 lines
16 KiB
C
568 lines
16 KiB
C
|
/********************************************************************
|
||
|
* *
|
||
|
* THIS FILE IS PART OF THE OggVorbis SOFTWARE CODEC SOURCE CODE. *
|
||
|
* USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS *
|
||
|
* GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE *
|
||
|
* IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. *
|
||
|
* *
|
||
|
* THE OggVorbis SOURCE CODE IS (C) COPYRIGHT 1994-2001 *
|
||
|
* by the Xiph.Org Foundation http://www.xiph.org/ *
|
||
|
* *
|
||
|
********************************************************************
|
||
|
|
||
|
function: train a VQ codebook
|
||
|
last mod: $Id: vqgen.c 16037 2009-05-26 21:10:58Z xiphmont $
|
||
|
|
||
|
********************************************************************/
|
||
|
|
||
|
/* This code is *not* part of libvorbis. It is used to generate
|
||
|
trained codebooks offline and then spit the results into a
|
||
|
pregenerated codebook that is compiled into libvorbis. It is an
|
||
|
expensive (but good) algorithm. Run it on big iron. */
|
||
|
|
||
|
/* There are so many optimizations to explore in *both* stages that
|
||
|
considering the undertaking is almost withering. For now, we brute
|
||
|
force it all */
|
||
|
|
||
|
#include <stdlib.h>
|
||
|
#include <stdio.h>
|
||
|
#include <math.h>
|
||
|
#include <string.h>
|
||
|
|
||
|
#include "vqgen.h"
|
||
|
#include "bookutil.h"
|
||
|
|
||
|
/* Codebook generation happens in two steps:
|
||
|
|
||
|
1) Train the codebook with data collected from the encoder: We use
|
||
|
one of a few error metrics (which represent the distance between a
|
||
|
given data point and a candidate point in the training set) to
|
||
|
divide the training set up into cells representing roughly equal
|
||
|
probability of occurring.
|
||
|
|
||
|
2) Generate the codebook and auxiliary data from the trained data set
|
||
|
*/
|
||
|
|
||
|
/* Codebook training ****************************************************
|
||
|
*
|
||
|
* The basic idea here is that a VQ codebook is like an m-dimensional
|
||
|
* foam with n bubbles. The bubbles compete for space/volume and are
|
||
|
* 'pressurized' [biased] according to some metric. The basic alg
|
||
|
* iterates through allowing the bubbles to compete for space until
|
||
|
* they converge (if the damping is dome properly) on a steady-state
|
||
|
* solution. Individual input points, collected from libvorbis, are
|
||
|
* used to train the algorithm monte-carlo style. */
|
||
|
|
||
|
/* internal helpers *****************************************************/
|
||
|
#define vN(data,i) (data+v->elements*i)
|
||
|
|
||
|
/* default metric; squared 'distance' from desired value. */
|
||
|
float _dist(vqgen *v,float *a, float *b){
|
||
|
int i;
|
||
|
int el=v->elements;
|
||
|
float acc=0.f;
|
||
|
for(i=0;i<el;i++){
|
||
|
float val=(a[i]-b[i]);
|
||
|
acc+=val*val;
|
||
|
}
|
||
|
return sqrt(acc);
|
||
|
}
|
||
|
|
||
|
float *_weight_null(vqgen *v,float *a){
|
||
|
return a;
|
||
|
}
|
||
|
|
||
|
/* *must* be beefed up. */
|
||
|
void _vqgen_seed(vqgen *v){
|
||
|
long i;
|
||
|
for(i=0;i<v->entries;i++)
|
||
|
memcpy(_now(v,i),_point(v,i),sizeof(float)*v->elements);
|
||
|
v->seeded=1;
|
||
|
}
|
||
|
|
||
|
int directdsort(const void *a, const void *b){
|
||
|
float av=*((float *)a);
|
||
|
float bv=*((float *)b);
|
||
|
return (av<bv)-(av>bv);
|
||
|
}
|
||
|
|
||
|
void vqgen_cellmetric(vqgen *v){
|
||
|
int j,k;
|
||
|
float min=-1.f,max=-1.f,mean=0.f,acc=0.f;
|
||
|
long dup=0,unused=0;
|
||
|
#ifdef NOISY
|
||
|
int i;
|
||
|
char buff[80];
|
||
|
float spacings[v->entries];
|
||
|
int count=0;
|
||
|
FILE *cells;
|
||
|
sprintf(buff,"cellspace%d.m",v->it);
|
||
|
cells=fopen(buff,"w");
|
||
|
#endif
|
||
|
|
||
|
/* minimum, maximum, cell spacing */
|
||
|
for(j=0;j<v->entries;j++){
|
||
|
float localmin=-1.;
|
||
|
|
||
|
for(k=0;k<v->entries;k++){
|
||
|
if(j!=k){
|
||
|
float this=_dist(v,_now(v,j),_now(v,k));
|
||
|
if(this>0){
|
||
|
if(v->assigned[k] && (localmin==-1 || this<localmin))
|
||
|
localmin=this;
|
||
|
}else{
|
||
|
if(k<j){
|
||
|
dup++;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if(k<v->entries)continue;
|
||
|
|
||
|
if(v->assigned[j]==0){
|
||
|
unused++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
localmin=v->max[j]+localmin/2; /* this gives us rough diameter */
|
||
|
if(min==-1 || localmin<min)min=localmin;
|
||
|
if(max==-1 || localmin>max)max=localmin;
|
||
|
mean+=localmin;
|
||
|
acc++;
|
||
|
#ifdef NOISY
|
||
|
spacings[count++]=localmin;
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
fprintf(stderr,"cell diameter: %.03g::%.03g::%.03g (%ld unused/%ld dup)\n",
|
||
|
min,mean/acc,max,unused,dup);
|
||
|
|
||
|
#ifdef NOISY
|
||
|
qsort(spacings,count,sizeof(float),directdsort);
|
||
|
for(i=0;i<count;i++)
|
||
|
fprintf(cells,"%g\n",spacings[i]);
|
||
|
fclose(cells);
|
||
|
#endif
|
||
|
|
||
|
}
|
||
|
|
||
|
/* External calls *******************************************************/
|
||
|
|
||
|
/* We have two forms of quantization; in the first, each vector
|
||
|
element in the codebook entry is orthogonal. Residues would use this
|
||
|
quantization for example.
|
||
|
|
||
|
In the second, we have a sequence of monotonically increasing
|
||
|
values that we wish to quantize as deltas (to save space). We
|
||
|
still need to quantize so that absolute values are accurate. For
|
||
|
example, LSP quantizes all absolute values, but the book encodes
|
||
|
distance between values because each successive value is larger
|
||
|
than the preceeding value. Thus the desired quantibits apply to
|
||
|
the encoded (delta) values, not abs positions. This requires minor
|
||
|
additional encode-side trickery. */
|
||
|
|
||
|
void vqgen_quantize(vqgen *v,quant_meta *q){
|
||
|
|
||
|
float maxdel;
|
||
|
float mindel;
|
||
|
|
||
|
float delta;
|
||
|
float maxquant=((1<<q->quant)-1);
|
||
|
|
||
|
int j,k;
|
||
|
|
||
|
mindel=maxdel=_now(v,0)[0];
|
||
|
|
||
|
for(j=0;j<v->entries;j++){
|
||
|
float last=0.f;
|
||
|
for(k=0;k<v->elements;k++){
|
||
|
if(mindel>_now(v,j)[k]-last)mindel=_now(v,j)[k]-last;
|
||
|
if(maxdel<_now(v,j)[k]-last)maxdel=_now(v,j)[k]-last;
|
||
|
if(q->sequencep)last=_now(v,j)[k];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/* first find the basic delta amount from the maximum span to be
|
||
|
encoded. Loosen the delta slightly to allow for additional error
|
||
|
during sequence quantization */
|
||
|
|
||
|
delta=(maxdel-mindel)/((1<<q->quant)-1.5f);
|
||
|
|
||
|
q->min=_float32_pack(mindel);
|
||
|
q->delta=_float32_pack(delta);
|
||
|
|
||
|
mindel=_float32_unpack(q->min);
|
||
|
delta=_float32_unpack(q->delta);
|
||
|
|
||
|
for(j=0;j<v->entries;j++){
|
||
|
float last=0;
|
||
|
for(k=0;k<v->elements;k++){
|
||
|
float val=_now(v,j)[k];
|
||
|
float now=rint((val-last-mindel)/delta);
|
||
|
|
||
|
_now(v,j)[k]=now;
|
||
|
if(now<0){
|
||
|
/* be paranoid; this should be impossible */
|
||
|
fprintf(stderr,"fault; quantized value<0\n");
|
||
|
exit(1);
|
||
|
}
|
||
|
|
||
|
if(now>maxquant){
|
||
|
/* be paranoid; this should be impossible */
|
||
|
fprintf(stderr,"fault; quantized value>max\n");
|
||
|
exit(1);
|
||
|
}
|
||
|
if(q->sequencep)last=(now*delta)+mindel+last;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* much easier :-). Unlike in the codebook, we don't un-log log
|
||
|
scales; we just make sure they're properly offset. */
|
||
|
void vqgen_unquantize(vqgen *v,quant_meta *q){
|
||
|
long j,k;
|
||
|
float mindel=_float32_unpack(q->min);
|
||
|
float delta=_float32_unpack(q->delta);
|
||
|
|
||
|
for(j=0;j<v->entries;j++){
|
||
|
float last=0.f;
|
||
|
for(k=0;k<v->elements;k++){
|
||
|
float now=_now(v,j)[k];
|
||
|
now=fabs(now)*delta+last+mindel;
|
||
|
if(q->sequencep)last=now;
|
||
|
_now(v,j)[k]=now;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void vqgen_init(vqgen *v,int elements,int aux,int entries,float mindist,
|
||
|
float (*metric)(vqgen *,float *, float *),
|
||
|
float *(*weight)(vqgen *,float *),int centroid){
|
||
|
memset(v,0,sizeof(vqgen));
|
||
|
|
||
|
v->centroid=centroid;
|
||
|
v->elements=elements;
|
||
|
v->aux=aux;
|
||
|
v->mindist=mindist;
|
||
|
v->allocated=32768;
|
||
|
v->pointlist=_ogg_malloc(v->allocated*(v->elements+v->aux)*sizeof(float));
|
||
|
|
||
|
v->entries=entries;
|
||
|
v->entrylist=_ogg_malloc(v->entries*v->elements*sizeof(float));
|
||
|
v->assigned=_ogg_malloc(v->entries*sizeof(long));
|
||
|
v->bias=_ogg_calloc(v->entries,sizeof(float));
|
||
|
v->max=_ogg_calloc(v->entries,sizeof(float));
|
||
|
if(metric)
|
||
|
v->metric_func=metric;
|
||
|
else
|
||
|
v->metric_func=_dist;
|
||
|
if(weight)
|
||
|
v->weight_func=weight;
|
||
|
else
|
||
|
v->weight_func=_weight_null;
|
||
|
|
||
|
v->asciipoints=tmpfile();
|
||
|
|
||
|
}
|
||
|
|
||
|
void vqgen_addpoint(vqgen *v, float *p,float *a){
|
||
|
int k;
|
||
|
for(k=0;k<v->elements;k++)
|
||
|
fprintf(v->asciipoints,"%.12g\n",p[k]);
|
||
|
for(k=0;k<v->aux;k++)
|
||
|
fprintf(v->asciipoints,"%.12g\n",a[k]);
|
||
|
|
||
|
if(v->points>=v->allocated){
|
||
|
v->allocated*=2;
|
||
|
v->pointlist=_ogg_realloc(v->pointlist,v->allocated*(v->elements+v->aux)*
|
||
|
sizeof(float));
|
||
|
}
|
||
|
|
||
|
memcpy(_point(v,v->points),p,sizeof(float)*v->elements);
|
||
|
if(v->aux)memcpy(_point(v,v->points)+v->elements,a,sizeof(float)*v->aux);
|
||
|
|
||
|
/* quantize to the density mesh if it's selected */
|
||
|
if(v->mindist>0.f){
|
||
|
/* quantize to the mesh */
|
||
|
for(k=0;k<v->elements+v->aux;k++)
|
||
|
_point(v,v->points)[k]=
|
||
|
rint(_point(v,v->points)[k]/v->mindist)*v->mindist;
|
||
|
}
|
||
|
v->points++;
|
||
|
if(!(v->points&0xff))spinnit("loading... ",v->points);
|
||
|
}
|
||
|
|
||
|
/* yes, not threadsafe. These utils aren't */
|
||
|
static int sortit=0;
|
||
|
static int sortsize=0;
|
||
|
static int meshcomp(const void *a,const void *b){
|
||
|
if(((sortit++)&0xfff)==0)spinnit("sorting mesh...",sortit);
|
||
|
return(memcmp(a,b,sortsize));
|
||
|
}
|
||
|
|
||
|
void vqgen_sortmesh(vqgen *v){
|
||
|
sortit=0;
|
||
|
if(v->mindist>0.f){
|
||
|
long i,march=1;
|
||
|
|
||
|
/* sort to make uniqueness detection trivial */
|
||
|
sortsize=(v->elements+v->aux)*sizeof(float);
|
||
|
qsort(v->pointlist,v->points,sortsize,meshcomp);
|
||
|
|
||
|
/* now march through and eliminate dupes */
|
||
|
for(i=1;i<v->points;i++){
|
||
|
if(memcmp(_point(v,i),_point(v,i-1),sortsize)){
|
||
|
/* a new, unique entry. march it down */
|
||
|
if(i>march)memcpy(_point(v,march),_point(v,i),sortsize);
|
||
|
march++;
|
||
|
}
|
||
|
spinnit("eliminating density... ",v->points-i);
|
||
|
}
|
||
|
|
||
|
/* we're done */
|
||
|
fprintf(stderr,"\r%ld training points remining out of %ld"
|
||
|
" after density mesh (%ld%%)\n",march,v->points,march*100/v->points);
|
||
|
v->points=march;
|
||
|
|
||
|
}
|
||
|
v->sorted=1;
|
||
|
}
|
||
|
|
||
|
float vqgen_iterate(vqgen *v,int biasp){
|
||
|
long i,j,k;
|
||
|
|
||
|
float fdesired;
|
||
|
long desired;
|
||
|
long desired2;
|
||
|
|
||
|
float asserror=0.f;
|
||
|
float meterror=0.f;
|
||
|
float *new;
|
||
|
float *new2;
|
||
|
long *nearcount;
|
||
|
float *nearbias;
|
||
|
#ifdef NOISY
|
||
|
char buff[80];
|
||
|
FILE *assig;
|
||
|
FILE *bias;
|
||
|
FILE *cells;
|
||
|
sprintf(buff,"cells%d.m",v->it);
|
||
|
cells=fopen(buff,"w");
|
||
|
sprintf(buff,"assig%d.m",v->it);
|
||
|
assig=fopen(buff,"w");
|
||
|
sprintf(buff,"bias%d.m",v->it);
|
||
|
bias=fopen(buff,"w");
|
||
|
#endif
|
||
|
|
||
|
|
||
|
if(v->entries<2){
|
||
|
fprintf(stderr,"generation requires at least two entries\n");
|
||
|
exit(1);
|
||
|
}
|
||
|
|
||
|
if(!v->sorted)vqgen_sortmesh(v);
|
||
|
if(!v->seeded)_vqgen_seed(v);
|
||
|
|
||
|
fdesired=(float)v->points/v->entries;
|
||
|
desired=fdesired;
|
||
|
desired2=desired*2;
|
||
|
new=_ogg_malloc(sizeof(float)*v->entries*v->elements);
|
||
|
new2=_ogg_malloc(sizeof(float)*v->entries*v->elements);
|
||
|
nearcount=_ogg_malloc(v->entries*sizeof(long));
|
||
|
nearbias=_ogg_malloc(v->entries*desired2*sizeof(float));
|
||
|
|
||
|
/* fill in nearest points for entry biasing */
|
||
|
/*memset(v->bias,0,sizeof(float)*v->entries);*/
|
||
|
memset(nearcount,0,sizeof(long)*v->entries);
|
||
|
memset(v->assigned,0,sizeof(long)*v->entries);
|
||
|
if(biasp){
|
||
|
for(i=0;i<v->points;i++){
|
||
|
float *ppt=v->weight_func(v,_point(v,i));
|
||
|
float firstmetric=v->metric_func(v,_now(v,0),ppt)+v->bias[0];
|
||
|
float secondmetric=v->metric_func(v,_now(v,1),ppt)+v->bias[1];
|
||
|
long firstentry=0;
|
||
|
long secondentry=1;
|
||
|
|
||
|
if(!(i&0xff))spinnit("biasing... ",v->points+v->points+v->entries-i);
|
||
|
|
||
|
if(firstmetric>secondmetric){
|
||
|
float temp=firstmetric;
|
||
|
firstmetric=secondmetric;
|
||
|
secondmetric=temp;
|
||
|
firstentry=1;
|
||
|
secondentry=0;
|
||
|
}
|
||
|
|
||
|
for(j=2;j<v->entries;j++){
|
||
|
float thismetric=v->metric_func(v,_now(v,j),ppt)+v->bias[j];
|
||
|
if(thismetric<secondmetric){
|
||
|
if(thismetric<firstmetric){
|
||
|
secondmetric=firstmetric;
|
||
|
secondentry=firstentry;
|
||
|
firstmetric=thismetric;
|
||
|
firstentry=j;
|
||
|
}else{
|
||
|
secondmetric=thismetric;
|
||
|
secondentry=j;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
j=firstentry;
|
||
|
for(j=0;j<v->entries;j++){
|
||
|
|
||
|
float thismetric,localmetric;
|
||
|
float *nearbiasptr=nearbias+desired2*j;
|
||
|
long k=nearcount[j];
|
||
|
|
||
|
localmetric=v->metric_func(v,_now(v,j),ppt);
|
||
|
/* 'thismetric' is to be the bias value necessary in the current
|
||
|
arrangement for entry j to capture point i */
|
||
|
if(firstentry==j){
|
||
|
/* use the secondary entry as the threshhold */
|
||
|
thismetric=secondmetric-localmetric;
|
||
|
}else{
|
||
|
/* use the primary entry as the threshhold */
|
||
|
thismetric=firstmetric-localmetric;
|
||
|
}
|
||
|
|
||
|
/* support the idea of 'minimum distance'... if we want the
|
||
|
cells in a codebook to be roughly some minimum size (as with
|
||
|
the low resolution residue books) */
|
||
|
|
||
|
/* a cute two-stage delayed sorting hack */
|
||
|
if(k<desired){
|
||
|
nearbiasptr[k]=thismetric;
|
||
|
k++;
|
||
|
if(k==desired){
|
||
|
spinnit("biasing... ",v->points+v->points+v->entries-i);
|
||
|
qsort(nearbiasptr,desired,sizeof(float),directdsort);
|
||
|
}
|
||
|
|
||
|
}else if(thismetric>nearbiasptr[desired-1]){
|
||
|
nearbiasptr[k]=thismetric;
|
||
|
k++;
|
||
|
if(k==desired2){
|
||
|
spinnit("biasing... ",v->points+v->points+v->entries-i);
|
||
|
qsort(nearbiasptr,desired2,sizeof(float),directdsort);
|
||
|
k=desired;
|
||
|
}
|
||
|
}
|
||
|
nearcount[j]=k;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* inflate/deflate */
|
||
|
|
||
|
for(i=0;i<v->entries;i++){
|
||
|
float *nearbiasptr=nearbias+desired2*i;
|
||
|
|
||
|
spinnit("biasing... ",v->points+v->entries-i);
|
||
|
|
||
|
/* due to the delayed sorting, we likely need to finish it off....*/
|
||
|
if(nearcount[i]>desired)
|
||
|
qsort(nearbiasptr,nearcount[i],sizeof(float),directdsort);
|
||
|
|
||
|
v->bias[i]=nearbiasptr[desired-1];
|
||
|
|
||
|
}
|
||
|
}else{
|
||
|
memset(v->bias,0,v->entries*sizeof(float));
|
||
|
}
|
||
|
|
||
|
/* Now assign with new bias and find new midpoints */
|
||
|
for(i=0;i<v->points;i++){
|
||
|
float *ppt=v->weight_func(v,_point(v,i));
|
||
|
float firstmetric=v->metric_func(v,_now(v,0),ppt)+v->bias[0];
|
||
|
long firstentry=0;
|
||
|
|
||
|
if(!(i&0xff))spinnit("centering... ",v->points-i);
|
||
|
|
||
|
for(j=0;j<v->entries;j++){
|
||
|
float thismetric=v->metric_func(v,_now(v,j),ppt)+v->bias[j];
|
||
|
if(thismetric<firstmetric){
|
||
|
firstmetric=thismetric;
|
||
|
firstentry=j;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
j=firstentry;
|
||
|
|
||
|
#ifdef NOISY
|
||
|
fprintf(cells,"%g %g\n%g %g\n\n",
|
||
|
_now(v,j)[0],_now(v,j)[1],
|
||
|
ppt[0],ppt[1]);
|
||
|
#endif
|
||
|
|
||
|
firstmetric-=v->bias[j];
|
||
|
meterror+=firstmetric;
|
||
|
|
||
|
if(v->centroid==0){
|
||
|
/* set up midpoints for next iter */
|
||
|
if(v->assigned[j]++){
|
||
|
for(k=0;k<v->elements;k++)
|
||
|
vN(new,j)[k]+=ppt[k];
|
||
|
if(firstmetric>v->max[j])v->max[j]=firstmetric;
|
||
|
}else{
|
||
|
for(k=0;k<v->elements;k++)
|
||
|
vN(new,j)[k]=ppt[k];
|
||
|
v->max[j]=firstmetric;
|
||
|
}
|
||
|
}else{
|
||
|
/* centroid */
|
||
|
if(v->assigned[j]++){
|
||
|
for(k=0;k<v->elements;k++){
|
||
|
if(vN(new,j)[k]>ppt[k])vN(new,j)[k]=ppt[k];
|
||
|
if(vN(new2,j)[k]<ppt[k])vN(new2,j)[k]=ppt[k];
|
||
|
}
|
||
|
if(firstmetric>v->max[firstentry])v->max[j]=firstmetric;
|
||
|
}else{
|
||
|
for(k=0;k<v->elements;k++){
|
||
|
vN(new,j)[k]=ppt[k];
|
||
|
vN(new2,j)[k]=ppt[k];
|
||
|
}
|
||
|
v->max[firstentry]=firstmetric;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* assign midpoints */
|
||
|
|
||
|
for(j=0;j<v->entries;j++){
|
||
|
#ifdef NOISY
|
||
|
fprintf(assig,"%ld\n",v->assigned[j]);
|
||
|
fprintf(bias,"%g\n",v->bias[j]);
|
||
|
#endif
|
||
|
asserror+=fabs(v->assigned[j]-fdesired);
|
||
|
if(v->assigned[j]){
|
||
|
if(v->centroid==0){
|
||
|
for(k=0;k<v->elements;k++)
|
||
|
_now(v,j)[k]=vN(new,j)[k]/v->assigned[j];
|
||
|
}else{
|
||
|
for(k=0;k<v->elements;k++)
|
||
|
_now(v,j)[k]=(vN(new,j)[k]+vN(new2,j)[k])/2.f;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
asserror/=(v->entries*fdesired);
|
||
|
|
||
|
fprintf(stderr,"Pass #%d... ",v->it);
|
||
|
fprintf(stderr,": dist %g(%g) metric error=%g \n",
|
||
|
asserror,fdesired,meterror/v->points);
|
||
|
v->it++;
|
||
|
|
||
|
free(new);
|
||
|
free(nearcount);
|
||
|
free(nearbias);
|
||
|
#ifdef NOISY
|
||
|
fclose(assig);
|
||
|
fclose(bias);
|
||
|
fclose(cells);
|
||
|
#endif
|
||
|
return(asserror);
|
||
|
}
|
||
|
|