/[svn]/libgig/trunk/src/tools/wav2gig.cpp
ViewVC logotype

Annotation of /libgig/trunk/src/tools/wav2gig.cpp

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3989 - (hide annotations) (download)
Sat Aug 28 13:46:54 2021 UTC (2 years, 7 months ago) by schoenebeck
File size: 23564 byte(s)
* wav2gig tool: use note number from wav file name if there was
  no note info stored in the wav file itself already.

1 schoenebeck 3980 /***************************************************************************
2     * *
3     * Copyright (C) 2021 Christian Schoenebeck *
4     * <cuse@users.sourceforge.net> *
5     * *
6     * This program is part of libgig. *
7     * *
8     * This program is free software; you can redistribute it and/or modify *
9     * it under the terms of the GNU General Public License as published by *
10     * the Free Software Foundation; either version 2 of the License, or *
11     * (at your option) any later version. *
12     * *
13     * This program is distributed in the hope that it will be useful, *
14     * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16     * GNU General Public License for more details. *
17     * *
18     * You should have received a copy of the GNU General Public License *
19     * along with this program; if not, write to the Free Software *
20     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, *
21     * MA 02111-1307 USA *
22     ***************************************************************************/
23    
24     #ifdef HAVE_CONFIG_H
25     # include <config.h>
26     #endif
27 schoenebeck 3981 #include <errno.h>
28     #include <sys/stat.h>
29     #include <dirent.h>
30     #include <string.h>
31 schoenebeck 3980 #include <iostream>
32     #include <cstdlib>
33     #include <string>
34     #include <set>
35     #include <map>
36 schoenebeck 3981 #include <regex>
37 schoenebeck 3980
38     #if !defined(WIN32)
39     # include <unistd.h>
40     #endif
41    
42     #include "../gig.h"
43     #include "../helper.h" // for ToString()
44    
45     // only libsndfile is available for Windows, so we use that for writing the sound files
46     #ifdef WIN32
47     # define HAVE_SNDFILE 1
48     #endif // WIN32
49    
50     // abort compilation here if libsndfile is not available
51     #if !HAVE_SNDFILE
52     # error "It seems as if libsndfile is not available!"
53     # error "(HAVE_SNDFILE is false)"
54     #endif
55    
56     #if HAVE_SNDFILE
57     # ifdef LIBSNDFILE_HEADER_FILE
58     # include LIBSNDFILE_HEADER_FILE(sndfile.h)
59     # else
60     # include <sndfile.h>
61     # endif
62     #endif // HAVE_SNDFILE
63    
64     #ifdef WIN32
65     # define DIR_SEPARATOR '\\'
66     #else
67     # define DIR_SEPARATOR '/'
68     #endif
69    
70     using namespace std;
71    
72     static string Revision() {
73     string s = "$Revision$";
74     return s.substr(11, s.size() - 13); // cut dollar signs, spaces and CVS macro keyword
75     }
76    
77     static void printVersion() {
78     cout << "wav2gig revision " << Revision() << endl;
79     cout << "using " << gig::libraryName() << " " << gig::libraryVersion() << endl;
80     }
81    
82     static void printUsage() {
83     cout << "wav2gig - Create GigaStudio file from a set of WAV files." << endl;
84     cout << endl;
85     cout << "Usage: wav2gig [-v] [-f] [-r] GIGFILE WAVFILEORDIR1 [ WAVFILEORDIR2 ... ]" << endl;
86     cout << endl;
87     cout << " -v Print version and exit." << endl;
88     cout << endl;
89     cout << " -f Overwrite output gig file if it already exists." << endl;
90     cout << endl;
91     cout << " -r Recurse through all subdirs of provided input WAV dirs." << endl;
92     cout << endl;
93     }
94    
95     static bool beginsWith(const string& haystack, const string& needle) {
96     return haystack.substr(0, needle.size()) == needle;
97     }
98    
99     static bool endsWith(const string& haystack, const string& needle) {
100     return haystack.substr(haystack.size() - needle.size(), needle.size()) == needle;
101     }
102    
103     static bool fileExists(const string& filename) {
104     FILE* hFile = fopen(filename.c_str(), "r");
105     if (!hFile) return false;
106     fclose(hFile);
107     return true;
108     }
109    
110     static bool isDir(const string& dirname) {
111     struct stat sb;
112     return (stat(dirname.c_str(), &sb) == 0) && S_ISDIR(sb.st_mode);
113     }
114    
115     static bool isRegularFile(const string& filename) {
116     struct stat sb;
117     return (stat(filename.c_str(), &sb) == 0) && S_ISREG(sb.st_mode);
118     }
119    
120     // this could also be replaced by fopen(name, "w") to simply truncate the file to zero
121     static void deleteFile(const string& filename) {
122     #if defined(WIN32)
123     DeleteFile(filename.c_str());
124     #else
125     unlink(filename.c_str());
126     #endif
127     }
128    
129     static bool isGigFileName(const string& filename) {
130     return endsWith(filename, ".gig") || endsWith(filename, ".GIG");
131     }
132    
133     static bool isWavFileName(const string& filename) {
134     return endsWith(filename, ".wav") || endsWith(filename, ".WAV");
135     }
136    
137     static bool isValidWavFile(const string& filename) {
138     SF_INFO info;
139     info.format = 0;
140     SNDFILE* hFile = sf_open(filename.c_str(), SFM_READ, &info);
141     if (!hFile) {
142     cerr << "Could not open input wav file \"" << filename << "\"" << endl;
143     return false;
144     }
145     sf_close(hFile);
146     switch (info.format & 0xff) {
147     case SF_FORMAT_PCM_S8:
148     case SF_FORMAT_PCM_16:
149     case SF_FORMAT_PCM_U8:
150     case SF_FORMAT_PCM_24:
151     case SF_FORMAT_PCM_32:
152     case SF_FORMAT_FLOAT:
153     case SF_FORMAT_DOUBLE:
154     return true;
155     default:
156     cerr << "Format of input wav file \"" << filename << "\" not supported!" << endl;
157     return false;
158     }
159     return false;
160     }
161    
162     static void collectWavFilesOfDir(set<string>& result, string path, bool bRecurse, bool* pbError = NULL) {
163     DIR* d = opendir(path.c_str());
164     if (!d) {
165     if (pbError) *pbError = true;
166     cerr << strerror(errno) << " : '" << path << "'" << endl;
167     return;
168     }
169    
170     for (struct dirent* e = readdir(d); e; e = readdir(d)) {
171     if (string(e->d_name) == "." || string(e->d_name) == "..")
172     continue;
173    
174     const string fullName = path + DIR_SEPARATOR + e->d_name;
175    
176     struct stat s;
177     if (stat(fullName.c_str(), &s)) {
178     if (pbError) *pbError = true;
179     cerr << strerror(errno) << " : '" << fullName << "'" << endl;
180     continue;
181     }
182    
183     if (S_ISREG(s.st_mode) && isWavFileName(fullName) && isValidWavFile(fullName)) {
184     result.insert(fullName);
185     } else if (S_ISDIR(s.st_mode) && bRecurse) {
186     collectWavFilesOfDir(result, fullName, bRecurse, pbError);
187     }
188     }
189    
190     closedir(d);
191     }
192    
193     static void collectWavFiles(set<string>& result, string path, bool bRecurse, bool* pbError = NULL) {
194     struct stat s;
195     if (stat(path.c_str(), &s)) {
196     if (pbError) *pbError = true;
197     cerr << strerror(errno) << " : '" << path << "'" << endl;
198     return;
199     }
200     if (S_ISREG(s.st_mode) && isWavFileName(path) && isValidWavFile(path)) {
201     result.insert(path);
202     } else if (S_ISDIR(s.st_mode)) {
203     collectWavFilesOfDir(result, path, bRecurse, pbError);
204     } else {
205     if (pbError) *pbError = true;
206     cerr << "Neither a regular (.wav) file nor directory : '" << path << "'" << endl;
207     }
208     }
209    
210     struct WavInfo {
211     string fileName;
212     int note;
213     int velocity;
214     SF_INFO sfinfo;
215     string noteName;
216     string name1;
217     string name2;
218     SF_INSTRUMENT sfinst;
219     bool hasSfInst;
220    
221     bool isStereo() const { return sfinfo.channels == 2; }
222    
223     string outputSampleName() const {
224     return name1 + "_" + noteName + "_" + ToString(velocity);
225     }
226    
227     void assertValid() const {
228     if (note < 0 || note > 127) {
229     cerr << "ERROR: note number " << note << " of \"" << fileName << "\" is invalid!" << endl;
230     exit(EXIT_FAILURE);
231     }
232     if (velocity < 0 || velocity > 127) {
233     cerr << "ERROR: velocity number " << velocity << " of \"" << fileName << "\" is invalid!" << endl;
234     exit(EXIT_FAILURE);
235     }
236     }
237     };
238    
239     class WavRegion : public map<int,WavInfo> {
240     public:
241     typedef map<int,WavInfo> base_t;
242    
243     // WavRegion () :
244     // map<int,WavInfo>()
245     // {
246     // }
247     //
248     // WavRegion (const WavRegion& x) :
249     // map<int,WavInfo>(x)
250     // {
251     // }
252     //
253     // WavRegion& operator= (const WavRegion& x) {
254     // base_t::operator=(x);
255     // return *this;
256     // }
257    
258     bool isStereo() const {
259     for (const auto& it : *this)
260     if (it.second.isStereo())
261     return true;
262     return false;
263     }
264     };
265    
266     typedef map<int,WavRegion> WavInstrument;
267    
268     static WavInfo getWavInfo(string filename) {
269     WavInfo wav;
270     wav.fileName = filename;
271     wav.sfinfo = {};
272     {
273     SNDFILE* hFile = sf_open(filename.c_str(), SFM_READ, &wav.sfinfo);
274     if (!hFile) {
275     cerr << "Could not open input wav file \"" << filename << "\"" << endl;
276     exit(EXIT_FAILURE);
277     }
278     wav.hasSfInst = (sf_command(hFile, SFC_GET_INSTRUMENT,
279     &wav.sfinst, sizeof(wav.sfinst)) != SF_FALSE);
280     sf_close(hFile);
281     switch (wav.sfinfo.channels) {
282     case 1:
283     case 2:
284     break;
285     default:
286     cerr << int(wav.sfinfo.channels) << " audio channels in WAV file \"" << filename << "\"; this is not supported!" << endl;
287     exit(EXIT_FAILURE);
288     }
289     }
290     {
291     regex rx(
292     "^([^-]+) - " // name 1 (e.g. "BSTEIN18")
293     "([^-]+) - " // name 2 (e.g. "noname")
294     "([^-]+) - " // velocity value (e.g. "18")
295     "([^-]+) - " // note number (e.g. "021")
296     "([^.]+)" // note name (e.g. "a-1")
297     );
298     smatch m;
299     regex_search(filename, m, rx);
300     if (m.size() < 5) {
301     cerr << "Invalid file name format: \"" << filename << "\"!" << endl;
302     exit(EXIT_FAILURE);
303     }
304     wav.name1 = m[1];
305     string sVelocity = m[3];
306     wav.velocity = atoi(sVelocity.c_str());
307     string sNote = m[4];
308     wav.note = atoi(sNote.c_str());
309     wav.name2 = m[2];
310     wav.noteName = m[5];
311     }
312     return wav;
313     }
314    
315     inline int getDimensionIndex(gig::Region* region, gig::dimension_t type) {
316     for (int d = 0; d < region->Dimensions; ++d)
317     if (region->pDimensionDefinitions[d].dimension == type)
318     return d;
319     return -1;
320     }
321    
322     static gig::Sample* createSample(gig::File* gig, WavInfo* wav) {
323     gig::Sample* s = gig->AddSample();
324    
325     s->pInfo->Name = wav->outputSampleName();
326     s->Channels = wav->sfinfo.channels;
327     s->SamplesPerSecond = wav->sfinfo.samplerate;
328    
329     switch (wav->sfinfo.format & 0xff) {
330     case SF_FORMAT_PCM_S8:
331     case SF_FORMAT_PCM_16:
332     case SF_FORMAT_PCM_U8:
333     s->BitDepth = 16;
334     break;
335     case SF_FORMAT_PCM_24:
336     case SF_FORMAT_PCM_32:
337     case SF_FORMAT_FLOAT:
338     case SF_FORMAT_DOUBLE:
339     s->BitDepth = 24;
340     break;
341     default:
342     throw gig::Exception("format not supported");
343     }
344    
345     s->FrameSize = s->Channels * s->BitDepth / 8;
346     if (wav->hasSfInst) {
347     s->MIDIUnityNote = wav->sfinst.basenote;
348     s->FineTune = wav->sfinst.detune;
349     if (wav->sfinst.loop_count && wav->sfinst.loops[0].mode != SF_LOOP_NONE) {
350     s->Loops = 1;
351     switch (wav->sfinst.loops[0].mode) {
352     case SF_LOOP_FORWARD:
353     s->LoopType = gig::loop_type_normal;
354     break;
355     case SF_LOOP_BACKWARD:
356     s->LoopType = gig::loop_type_backward;
357     break;
358     case SF_LOOP_ALTERNATING:
359     s->LoopType = gig::loop_type_bidirectional;
360     break;
361     }
362     s->LoopStart = wav->sfinst.loops[0].start;
363     s->LoopEnd = wav->sfinst.loops[0].end;
364     s->LoopPlayCount = wav->sfinst.loops[0].count;
365     s->LoopSize = s->LoopEnd - s->LoopStart + 1;
366     }
367 schoenebeck 3989 } else {
368     s->MIDIUnityNote = wav->note;
369 schoenebeck 3980 }
370    
371     // schedule for resize (will be performed when gig->Save() is called)
372     s->Resize(wav->sfinfo.frames);
373    
374     return s;
375     }
376    
377     int main(int argc, char *argv[]) {
378     bool bForce = false;
379     bool bRecursive = false;
380    
381     // validate & parse arguments provided to this program
382     int iArg;
383     for (iArg = 1; iArg < argc; ++iArg) {
384     const string opt = argv[iArg];
385     if (opt == "--") { // common for all command line tools: separator between initial option arguments and subsequent file arguments
386     iArg++;
387     break;
388     }
389     if (opt.substr(0, 1) != "-") break;
390    
391     if (opt == "-v") {
392     printVersion();
393     return EXIT_SUCCESS;
394     } else if (opt == "-f") {
395     bForce = true;
396     } else if (opt == "-r") {
397     bRecursive = true;
398     } else {
399     cerr << "Unknown option '" << opt << "'" << endl;
400     cerr << endl;
401     printUsage();
402     return EXIT_FAILURE;
403     }
404     }
405     if (argc < 3) {
406     printUsage();
407     return EXIT_FAILURE;
408     }
409    
410     set<string> inNames; // may be file names and/or dir names
411     string outFileName;
412    
413     // all options have been processed, all subsequent args should be file/dir arguments
414     for (int i = 0; iArg < argc; ++iArg, ++i) {
415     if (i == 0) {
416     outFileName = argv[iArg];
417     } else {
418     inNames.insert(argv[iArg]);
419     }
420     }
421     if (outFileName.empty()) {
422     cerr << "You must provide one output file (.gig format)!" << endl;
423     return EXIT_FAILURE;
424     }
425     if (inNames.empty()) {
426     cerr << "You must provide at least one input WAV file or directory!" << endl;
427     return EXIT_FAILURE;
428     }
429     if (!isGigFileName(outFileName)) {
430     cerr << "Provided output file name should end with \".gig\"!" << endl;
431     return EXIT_FAILURE;
432     }
433    
434     // now collect the actual list of input WAV files
435     set<string> wavFileNames;
436     cout << "Scanning for input WAV files ... " << flush;
437     for (set<string>::const_iterator it = inNames.begin();
438     it != inNames.end(); ++it)
439     {
440     bool error = false;
441     collectWavFiles(wavFileNames, *it, bRecursive, &error);
442     if (error) return EXIT_FAILURE;
443     }
444     if (wavFileNames.empty()) {
445     cerr << "No input WAV file provided (or found)!" << endl;
446     return EXIT_FAILURE;
447     }
448     cout << "(" << int(wavFileNames.size()) << " found).\n";
449    
450     // check if output file already exists
451     if (fileExists(outFileName)) {
452     if (bForce) deleteFile(outFileName);
453     else {
454     cerr << "Output file '" << outFileName << "' already exists. Use -f to overwrite it." << endl;
455     return EXIT_FAILURE;
456     }
457     }
458    
459     // order all input wav files into regions and velocity splits
460     WavInstrument wavInstrument;
461     cout << "Preprocessing input WAV files by their names ... " << flush;
462     for (set<string>::const_iterator it = wavFileNames.begin();
463     it != wavFileNames.end(); ++it)
464     {
465     WavInfo wavInfo = getWavInfo(*it);
466     wavInfo.assertValid(); // make sure collected informations are OK
467     if (wavInstrument[wavInfo.note].count(wavInfo.velocity)) {
468     cerr << "Velocity conflict between file '" << wavInfo.fileName
469     << "' and file '" << wavInstrument[wavInfo.note][wavInfo.velocity].fileName << "'!" << endl;
470     return EXIT_FAILURE;
471     }
472     wavInstrument[wavInfo.note][wavInfo.velocity] = wavInfo;
473     }
474     if (wavInstrument.empty()) {
475     cerr << "After sorting the WAV files around, there is no single WAV left to create a GIG file with!" << endl;
476     return EXIT_FAILURE;
477     }
478     cout << "OK\n";
479    
480     // create and assemble a new .gig file as output
481     try {
482     cout << "Creating new gig file and one new gig instrument ... " << flush;
483    
484     // start with an empty .gig file
485     gig::File gig;
486    
487     gig::Instrument* instr = gig.AddInstrument();
488     instr->pInfo->Name = "Unnamed by wav2gig";
489    
490     cout << "OK\n";
491    
492     map<gig::Sample*,WavInfo> queuedSamples;
493    
494     cout << "Assembling new gig instrument with interpreted multi sample structure ... " << flush;
495     for (auto& itWavRgn : wavInstrument) {
496     const int note = itWavRgn.first;
497     WavRegion& wavRgn = itWavRgn.second;
498    
499     gig::Region* gigRegion = instr->AddRegion();
500     gigRegion->SetKeyRange(note/*low*/, note/*high*/);
501    
502     if (wavRgn.isStereo()) {
503     gig::dimension_def_t dim;
504     dim.dimension = gig::dimension_samplechannel;
505     dim.bits = 1; // 2^(1) = 2
506     dim.zones = 2; // stereo = 2 audio channels = 2 split zones
507     gigRegion->AddDimension(&dim);
508     }
509    
510     if (wavRgn.size() > 1) {
511     gig::dimension_def_t dim;
512     dim.dimension = gig::dimension_velocity;
513     dim.zones = wavRgn.size();
514     // Find the number of bits required to hold the
515     // specified amount of zones.
516     int zoneBits = dim.zones - 1;
517     for (dim.bits = 0; zoneBits > 1; dim.bits += 2, zoneBits >>= 2);
518     dim.bits += zoneBits;
519     gigRegion->AddDimension(&dim);
520     }
521    
522     const int iStereoDimensionIndex = getDimensionIndex(gigRegion, gig::dimension_samplechannel);
523     const int iVelocityDimensionIndex = getDimensionIndex(gigRegion, gig::dimension_velocity);
524    
525     int iVelocitySplitZone = 0;
526     for (auto& itWav : wavRgn) {
527     const int velocity = itWav.first;
528     WavInfo& wav = itWav.second;
529     gig::Sample* gigSample = createSample(&gig, &wav);
530     queuedSamples[gigSample] = wav;
531    
532     uint8_t iDimBits[8] = {};
533    
534     for (int iAudioChannel = 0; iAudioChannel < (wavRgn.isStereo() ? 2 : 1); ++iAudioChannel) {
535    
536     // if region has velocity splits, select the respective velocity split zone
537     if (wavRgn.size() > 1) {
538     if (iVelocityDimensionIndex < 0)
539     throw gig::Exception("Could not resolve velocity dimension index");
540     iDimBits[iVelocityDimensionIndex] = iVelocitySplitZone;
541     }
542    
543     // select dimension bit for this stereo dimension split
544     if (iAudioChannel > 0) {
545     if (iStereoDimensionIndex < 0)
546     throw gig::Exception("Could not resolve stereo dimension index");
547     iDimBits[iStereoDimensionIndex] = 1;
548     }
549    
550     gig::DimensionRegion* dimRgn = gigRegion->GetDimensionRegionByBit(iDimBits);
551     if (!dimRgn)
552     throw gig::Exception("Internal error: Could not resolve Dimension Region");
553    
554     // if this is a velocity split, apply the precise velocity split range values
555     if (wavRgn.size() > 1) {
556     dimRgn->VelocityUpperLimit = velocity; // gig v2
557     dimRgn->DimensionUpperLimits[iVelocityDimensionIndex] = velocity; // gig v3 and above
558     }
559    
560     dimRgn->pSample = gigSample;
561     if (gigSample) {
562     dimRgn->UnityNote = gigSample->MIDIUnityNote;
563     if (gigSample->Loops) {
564     DLS::sample_loop_t loop;
565     loop.Size = sizeof(loop);
566     loop.LoopType = gigSample->LoopType;
567     loop.LoopStart = gigSample->LoopStart;
568     loop.LoopLength = gigSample->LoopEnd - gigSample->LoopStart;
569     dimRgn->AddSampleLoop(&loop);
570     }
571     }
572    
573     dimRgn->FineTune = gigSample->FineTune;
574     }
575    
576     iVelocitySplitZone++;
577     }
578     }
579     cout << "OK\n";
580    
581     cout << "Saving initial gig file layout ... " << flush;
582     // save result to disk (as .gig file)
583     gig.Save(outFileName);
584     cout << "OK\n";
585    
586     cout << "Copying audio sample data ... " << flush;
587     // finally write the actual wav sample data directly to the created gig file
588     for (auto& itSmpl : queuedSamples) {
589     gig::Sample* gigSample = itSmpl.first;
590     WavInfo& wav = itSmpl.second;
591    
592     SF_INFO info = {};
593     SNDFILE* hFile = sf_open(wav.fileName.c_str(), SFM_READ, &info);
594     sf_command(hFile, SFC_SET_SCALE_FLOAT_INT_READ, 0, SF_TRUE);
595     if (!hFile) throw gig::Exception("could not open file");
596     // determine sample's bit depth
597     int bitdepth;
598     switch (info.format & 0xff) {
599     case SF_FORMAT_PCM_S8:
600     case SF_FORMAT_PCM_16:
601     case SF_FORMAT_PCM_U8:
602     bitdepth = 16;
603     break;
604     case SF_FORMAT_PCM_24:
605     case SF_FORMAT_PCM_32:
606     case SF_FORMAT_FLOAT:
607     case SF_FORMAT_DOUBLE:
608     bitdepth = 24;
609     break;
610     default:
611     sf_close(hFile); // close sound file
612     throw gig::Exception("format not supported");
613     }
614    
615     const int bufsize = 10000;
616     switch (bitdepth) {
617     case 16: {
618     short* buffer = new short[bufsize * info.channels];
619     sf_count_t cnt = info.frames;
620     while (cnt) {
621     // libsndfile does the conversion for us (if needed)
622     int n = sf_readf_short(hFile, buffer, bufsize);
623     // write from buffer directly (physically) into .gig file
624     gigSample->Write(buffer, n);
625     cnt -= n;
626     }
627     delete[] buffer;
628     break;
629     }
630     case 24: {
631     int* srcbuf = new int[bufsize * info.channels];
632     uint8_t* dstbuf = new uint8_t[bufsize * 3 * info.channels];
633     sf_count_t cnt = info.frames;
634     while (cnt) {
635     // libsndfile returns 32 bits, convert to 24
636     int n = sf_readf_int(hFile, srcbuf, bufsize);
637     int j = 0;
638     for (int i = 0 ; i < n * info.channels ; i++) {
639     dstbuf[j++] = srcbuf[i] >> 8;
640     dstbuf[j++] = srcbuf[i] >> 16;
641     dstbuf[j++] = srcbuf[i] >> 24;
642     }
643     // write from buffer directly (physically) into .gig file
644     gigSample->Write(dstbuf, n);
645     cnt -= n;
646     }
647     delete[] srcbuf;
648     delete[] dstbuf;
649     break;
650     }
651     }
652     sf_close(hFile);
653     }
654     cout << "OK\n";
655    
656     } catch (RIFF::Exception e) {
657     cerr << "Failed generating output file:" << endl;
658     e.PrintMessage();
659     return EXIT_FAILURE;
660     } catch (...) {
661     cerr << "Unknown exception while trying to assemble output file." << endl;
662     return EXIT_FAILURE;
663     }
664    
665     return EXIT_SUCCESS;
666     }

Properties

Name Value
svn:keywords Revision

  ViewVC Help
Powered by ViewVC