/[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 3989 - (show annotations) (download)
Sat Aug 28 13:46:54 2021 UTC (17 months, 1 week 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 /***************************************************************************
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 } else {
368 s->MIDIUnityNote = wav->note;
369 }
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