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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3982 - (show annotations) (download)
Tue Aug 3 15:53:34 2021 UTC (17 months, 3 weeks ago) by schoenebeck
File size: 23513 byte(s)
- wav2gig: enable CVS style revision macro expansion

1 /***************************************************************************
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 #include <errno.h>
28 #include <sys/stat.h>
29 #include <dirent.h>
30 #include <string.h>
31 #include <iostream>
32 #include <cstdlib>
33 #include <string>
34 #include <set>
35 #include <map>
36 #include <regex>
37
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 }
368
369 // schedule for resize (will be performed when gig->Save() is called)
370 s->Resize(wav->sfinfo.frames);
371
372 return s;
373 }
374
375 int main(int argc, char *argv[]) {
376 bool bForce = false;
377 bool bRecursive = false;
378
379 // validate & parse arguments provided to this program
380 int iArg;
381 for (iArg = 1; iArg < argc; ++iArg) {
382 const string opt = argv[iArg];
383 if (opt == "--") { // common for all command line tools: separator between initial option arguments and subsequent file arguments
384 iArg++;
385 break;
386 }
387 if (opt.substr(0, 1) != "-") break;
388
389 if (opt == "-v") {
390 printVersion();
391 return EXIT_SUCCESS;
392 } else if (opt == "-f") {
393 bForce = true;
394 } else if (opt == "-r") {
395 bRecursive = true;
396 } else {
397 cerr << "Unknown option '" << opt << "'" << endl;
398 cerr << endl;
399 printUsage();
400 return EXIT_FAILURE;
401 }
402 }
403 if (argc < 3) {
404 printUsage();
405 return EXIT_FAILURE;
406 }
407
408 set<string> inNames; // may be file names and/or dir names
409 string outFileName;
410
411 // all options have been processed, all subsequent args should be file/dir arguments
412 for (int i = 0; iArg < argc; ++iArg, ++i) {
413 if (i == 0) {
414 outFileName = argv[iArg];
415 } else {
416 inNames.insert(argv[iArg]);
417 }
418 }
419 if (outFileName.empty()) {
420 cerr << "You must provide one output file (.gig format)!" << endl;
421 return EXIT_FAILURE;
422 }
423 if (inNames.empty()) {
424 cerr << "You must provide at least one input WAV file or directory!" << endl;
425 return EXIT_FAILURE;
426 }
427 if (!isGigFileName(outFileName)) {
428 cerr << "Provided output file name should end with \".gig\"!" << endl;
429 return EXIT_FAILURE;
430 }
431
432 // now collect the actual list of input WAV files
433 set<string> wavFileNames;
434 cout << "Scanning for input WAV files ... " << flush;
435 for (set<string>::const_iterator it = inNames.begin();
436 it != inNames.end(); ++it)
437 {
438 bool error = false;
439 collectWavFiles(wavFileNames, *it, bRecursive, &error);
440 if (error) return EXIT_FAILURE;
441 }
442 if (wavFileNames.empty()) {
443 cerr << "No input WAV file provided (or found)!" << endl;
444 return EXIT_FAILURE;
445 }
446 cout << "(" << int(wavFileNames.size()) << " found).\n";
447
448 // check if output file already exists
449 if (fileExists(outFileName)) {
450 if (bForce) deleteFile(outFileName);
451 else {
452 cerr << "Output file '" << outFileName << "' already exists. Use -f to overwrite it." << endl;
453 return EXIT_FAILURE;
454 }
455 }
456
457 // order all input wav files into regions and velocity splits
458 WavInstrument wavInstrument;
459 cout << "Preprocessing input WAV files by their names ... " << flush;
460 for (set<string>::const_iterator it = wavFileNames.begin();
461 it != wavFileNames.end(); ++it)
462 {
463 WavInfo wavInfo = getWavInfo(*it);
464 wavInfo.assertValid(); // make sure collected informations are OK
465 if (wavInstrument[wavInfo.note].count(wavInfo.velocity)) {
466 cerr << "Velocity conflict between file '" << wavInfo.fileName
467 << "' and file '" << wavInstrument[wavInfo.note][wavInfo.velocity].fileName << "'!" << endl;
468 return EXIT_FAILURE;
469 }
470 wavInstrument[wavInfo.note][wavInfo.velocity] = wavInfo;
471 }
472 if (wavInstrument.empty()) {
473 cerr << "After sorting the WAV files around, there is no single WAV left to create a GIG file with!" << endl;
474 return EXIT_FAILURE;
475 }
476 cout << "OK\n";
477
478 // create and assemble a new .gig file as output
479 try {
480 cout << "Creating new gig file and one new gig instrument ... " << flush;
481
482 // start with an empty .gig file
483 gig::File gig;
484
485 gig::Instrument* instr = gig.AddInstrument();
486 instr->pInfo->Name = "Unnamed by wav2gig";
487
488 cout << "OK\n";
489
490 map<gig::Sample*,WavInfo> queuedSamples;
491
492 cout << "Assembling new gig instrument with interpreted multi sample structure ... " << flush;
493 for (auto& itWavRgn : wavInstrument) {
494 const int note = itWavRgn.first;
495 WavRegion& wavRgn = itWavRgn.second;
496
497 gig::Region* gigRegion = instr->AddRegion();
498 gigRegion->SetKeyRange(note/*low*/, note/*high*/);
499
500 if (wavRgn.isStereo()) {
501 gig::dimension_def_t dim;
502 dim.dimension = gig::dimension_samplechannel;
503 dim.bits = 1; // 2^(1) = 2
504 dim.zones = 2; // stereo = 2 audio channels = 2 split zones
505 gigRegion->AddDimension(&dim);
506 }
507
508 if (wavRgn.size() > 1) {
509 gig::dimension_def_t dim;
510 dim.dimension = gig::dimension_velocity;
511 dim.zones = wavRgn.size();
512 // Find the number of bits required to hold the
513 // specified amount of zones.
514 int zoneBits = dim.zones - 1;
515 for (dim.bits = 0; zoneBits > 1; dim.bits += 2, zoneBits >>= 2);
516 dim.bits += zoneBits;
517 gigRegion->AddDimension(&dim);
518 }
519
520 const int iStereoDimensionIndex = getDimensionIndex(gigRegion, gig::dimension_samplechannel);
521 const int iVelocityDimensionIndex = getDimensionIndex(gigRegion, gig::dimension_velocity);
522
523 int iVelocitySplitZone = 0;
524 for (auto& itWav : wavRgn) {
525 const int velocity = itWav.first;
526 WavInfo& wav = itWav.second;
527 gig::Sample* gigSample = createSample(&gig, &wav);
528 queuedSamples[gigSample] = wav;
529
530 uint8_t iDimBits[8] = {};
531
532 for (int iAudioChannel = 0; iAudioChannel < (wavRgn.isStereo() ? 2 : 1); ++iAudioChannel) {
533
534 // if region has velocity splits, select the respective velocity split zone
535 if (wavRgn.size() > 1) {
536 if (iVelocityDimensionIndex < 0)
537 throw gig::Exception("Could not resolve velocity dimension index");
538 iDimBits[iVelocityDimensionIndex] = iVelocitySplitZone;
539 }
540
541 // select dimension bit for this stereo dimension split
542 if (iAudioChannel > 0) {
543 if (iStereoDimensionIndex < 0)
544 throw gig::Exception("Could not resolve stereo dimension index");
545 iDimBits[iStereoDimensionIndex] = 1;
546 }
547
548 gig::DimensionRegion* dimRgn = gigRegion->GetDimensionRegionByBit(iDimBits);
549 if (!dimRgn)
550 throw gig::Exception("Internal error: Could not resolve Dimension Region");
551
552 // if this is a velocity split, apply the precise velocity split range values
553 if (wavRgn.size() > 1) {
554 dimRgn->VelocityUpperLimit = velocity; // gig v2
555 dimRgn->DimensionUpperLimits[iVelocityDimensionIndex] = velocity; // gig v3 and above
556 }
557
558 dimRgn->pSample = gigSample;
559 if (gigSample) {
560 dimRgn->UnityNote = gigSample->MIDIUnityNote;
561 if (gigSample->Loops) {
562 DLS::sample_loop_t loop;
563 loop.Size = sizeof(loop);
564 loop.LoopType = gigSample->LoopType;
565 loop.LoopStart = gigSample->LoopStart;
566 loop.LoopLength = gigSample->LoopEnd - gigSample->LoopStart;
567 dimRgn->AddSampleLoop(&loop);
568 }
569 }
570
571 dimRgn->FineTune = gigSample->FineTune;
572 }
573
574 iVelocitySplitZone++;
575 }
576 }
577 cout << "OK\n";
578
579 cout << "Saving initial gig file layout ... " << flush;
580 // save result to disk (as .gig file)
581 gig.Save(outFileName);
582 cout << "OK\n";
583
584 cout << "Copying audio sample data ... " << flush;
585 // finally write the actual wav sample data directly to the created gig file
586 for (auto& itSmpl : queuedSamples) {
587 gig::Sample* gigSample = itSmpl.first;
588 WavInfo& wav = itSmpl.second;
589
590 SF_INFO info = {};
591 SNDFILE* hFile = sf_open(wav.fileName.c_str(), SFM_READ, &info);
592 sf_command(hFile, SFC_SET_SCALE_FLOAT_INT_READ, 0, SF_TRUE);
593 if (!hFile) throw gig::Exception("could not open file");
594 // determine sample's bit depth
595 int bitdepth;
596 switch (info.format & 0xff) {
597 case SF_FORMAT_PCM_S8:
598 case SF_FORMAT_PCM_16:
599 case SF_FORMAT_PCM_U8:
600 bitdepth = 16;
601 break;
602 case SF_FORMAT_PCM_24:
603 case SF_FORMAT_PCM_32:
604 case SF_FORMAT_FLOAT:
605 case SF_FORMAT_DOUBLE:
606 bitdepth = 24;
607 break;
608 default:
609 sf_close(hFile); // close sound file
610 throw gig::Exception("format not supported");
611 }
612
613 const int bufsize = 10000;
614 switch (bitdepth) {
615 case 16: {
616 short* buffer = new short[bufsize * info.channels];
617 sf_count_t cnt = info.frames;
618 while (cnt) {
619 // libsndfile does the conversion for us (if needed)
620 int n = sf_readf_short(hFile, buffer, bufsize);
621 // write from buffer directly (physically) into .gig file
622 gigSample->Write(buffer, n);
623 cnt -= n;
624 }
625 delete[] buffer;
626 break;
627 }
628 case 24: {
629 int* srcbuf = new int[bufsize * info.channels];
630 uint8_t* dstbuf = new uint8_t[bufsize * 3 * info.channels];
631 sf_count_t cnt = info.frames;
632 while (cnt) {
633 // libsndfile returns 32 bits, convert to 24
634 int n = sf_readf_int(hFile, srcbuf, bufsize);
635 int j = 0;
636 for (int i = 0 ; i < n * info.channels ; i++) {
637 dstbuf[j++] = srcbuf[i] >> 8;
638 dstbuf[j++] = srcbuf[i] >> 16;
639 dstbuf[j++] = srcbuf[i] >> 24;
640 }
641 // write from buffer directly (physically) into .gig file
642 gigSample->Write(dstbuf, n);
643 cnt -= n;
644 }
645 delete[] srcbuf;
646 delete[] dstbuf;
647 break;
648 }
649 }
650 sf_close(hFile);
651 }
652 cout << "OK\n";
653
654 } catch (RIFF::Exception e) {
655 cerr << "Failed generating output file:" << endl;
656 e.PrintMessage();
657 return EXIT_FAILURE;
658 } catch (...) {
659 cerr << "Unknown exception while trying to assemble output file." << endl;
660 return EXIT_FAILURE;
661 }
662
663 return EXIT_SUCCESS;
664 }

Properties

Name Value
svn:executable *
svn:keywords Revision

  ViewVC Help
Powered by ViewVC