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

* Bumped version (4.3.0.svn30).

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
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