/[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 3980 - (hide annotations) (download)
Tue Aug 3 14:37:25 2021 UTC (2 years, 8 months ago) by schoenebeck
File size: 23454 byte(s)
* Add new command line tool "wav2gig".

* Bumped version (4.3.0.svn30).

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

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC