1 /*
2 * Copyright (C) 2002 - David W. Durham
3 *
4 * This file is part of ReZound, an audio editing application.
5 *
6 * ReZound is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published
8 * by the Free Software Foundation; either version 2 of the License,
9 * or (at your option) any later version.
10 *
11 * ReZound is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
19 */
20
21 /*
22 * This SoundTranslator class handles mp3 I/O by interfacing with the
23 * lame executable. They do have a lame library, but the API is not
24 * documented and the library itself does not deal with mp3 files only
25 * with mp3 chunks. Plus, doing it this way should avoid any possible
26 * patent issues even though there's not supposed to be any with lame.
27 */
28
29 #include "ClameSoundTranslator.h"
30
31 #include <stdexcept>
32
33 #include <CPath.h>
34 #include <TAutoBuffer.h>
35 #include <endian_util.h>
36
37 #include "CSound.h"
38 #include "AFrontendHooks.h"
39 #include "AStatusComm.h"
40
41 static string gPathToLame="";
42
43 struct RWaveHeader
44 {
45 char RIFF_ID[4];
46 uint32_t fileSize; // bogus coming from lame
47 char WAVE_ID[4];
48 char fmt_ID[4];
49 uint32_t fmtSize;
50 uint16_t dataType; // 1 => PCM
51 uint16_t channelCount;
52 uint32_t sampleRate;
53 uint32_t bytesPerSec;
54 uint16_t bytesPerSample;
55 uint16_t bitsPerSample;
56 char data_ID[4];
57 uint32_t dataLength; // bogus coming from lame
58
convertFromLERWaveHeader59 void convertFromLE()
60 {
61 //hetle((uint32_t *)RIFF_ID);
62 hetle(&fileSize);
63 //hetle((uint32_t *)WAVE_ID);
64 //hetle((uint32_t *)fmt_ID);
65 hetle(&fmtSize);
66 hetle(&dataType);
67 hetle(&channelCount);
68 hetle(&sampleRate);
69 hetle(&bytesPerSec);
70 hetle(&bytesPerSample);
71 hetle(&bitsPerSample);
72 //hetle((uint32_t *)data_ID);
73 hetle(&dataLength);
74 }
75
convertToLERWaveHeader76 void convertToLE()
77 {
78 //hetle((uint32_t *)RIFF_ID);
79 hetle(&fileSize);
80 //hetle((uint32_t *)WAVE_ID);
81 //hetle((uint32_t *)fmt_ID);
82 hetle(&fmtSize);
83 hetle(&dataType);
84 hetle(&channelCount);
85 hetle(&sampleRate);
86 hetle(&bytesPerSec);
87 hetle(&bytesPerSample);
88 hetle(&bitsPerSample);
89 //hetle((uint32_t *)data_ID);
90 hetle(&dataLength);
91 }
92
93
94 };
95
ClameSoundTranslator()96 ClameSoundTranslator::ClameSoundTranslator() :
97 ApipedSoundTranslator()
98 {
99 }
100
~ClameSoundTranslator()101 ClameSoundTranslator::~ClameSoundTranslator()
102 {
103 }
104
checkForApp()105 bool ClameSoundTranslator::checkForApp()
106 {
107 gPathToLame=findAppOnPath("lame");
108 if(gPathToLame=="")
109 fprintf(stderr,"'lame' executable not found in $PATH -- mp3 support will be disabled\n");
110 return gPathToLame!="";
111 }
112
113 // ??? could just return a CSound object an have used the one constructor that takes the meta info
114 // ??? but, then how would I be able to have createWorkingPoolFileIfExists
onLoadSound(const string filename,CSound * sound) const115 bool ClameSoundTranslator::onLoadSound(const string filename,CSound *sound) const
116 {
117 bool ret=true;
118
119 if(gPathToLame=="")
120 throw runtime_error(string(__func__)+" -- $PATH to 'lame' not set");
121
122 if(!checkThatFileExists(filename))
123 throw runtime_error(string(__func__)+" -- file not found, '"+filename+"'");
124
125 const string cmdLine=gPathToLame+" --decode "+escapeFilename(filename)+" -";
126
127 fprintf(stderr,"lame command line: '%s'\n",cmdLine.c_str());
128
129 FILE *errStream=NULL;
130 FILE *p=popen(cmdLine,"r",&errStream);
131
132 CRezPoolAccesser *accessers[MAX_CHANNELS]={0};
133 try
134 {
135 RWaveHeader waveHeader;
136 memset(&waveHeader,0,sizeof(waveHeader));
137
138
139 fread(&waveHeader,1,sizeof(waveHeader),p);
140 waveHeader.convertFromLE();
141
142 // verify some stuff about the output of lame
143 if(waveHeader.fmtSize!=16)
144 throw runtime_error(string(__func__)+" -- it looks as if either there is an error in the input file -- or lame was not compiled with decoding support (get latest at http://mp3dev.org) -- or an error has occuring executing lame -- or your version of lame has started to output a different wave file header when decoding MPEG Layer-1,2,3 files to wave files. Changes will have to be made to this source to handle the new wave file output -- check stderr for more information");
145 if(strncmp(waveHeader.RIFF_ID,"RIFF",4)!=0)
146 throw runtime_error(string(__func__)+" -- internal error -- 'RIFF' expected in lame output");
147 if(strncmp(waveHeader.WAVE_ID,"WAVE",4)!=0)
148 throw runtime_error(string(__func__)+" -- internal error -- 'WAVE' expected in lame output");
149 if(strncmp(waveHeader.fmt_ID,"fmt ",4)!=0)
150 throw runtime_error(string(__func__)+" -- internal error -- 'fmt ' expected in lame output");
151 if(strncmp(waveHeader.data_ID,"data",4)!=0)
152 throw runtime_error(string(__func__)+" -- internal error -- 'data' expected in lame output");
153
154 if(waveHeader.dataType!=1)
155 throw runtime_error(string(__func__)+" -- internal error -- it looks as if your version of lame has started to output non-PCM data when decoding mp3 files to wave files. Changes will have to be made to this source to handle the new wave file output");
156
157 unsigned channelCount=waveHeader.channelCount;
158 if(channelCount<=0 || channelCount>MAX_CHANNELS) // ??? could just ignore the extra channels
159 throw runtime_error(string(__func__)+" -- invalid number of channels in audio file: "+istring(channelCount)+" -- you could simply increase MAX_CHANNELS in CSound.h");
160
161 unsigned sampleRate=waveHeader.sampleRate;
162 if(sampleRate<100 || sampleRate>196000)
163 throw runtime_error(string(__func__)+" -- an unlikely sample rate of "+istring(sampleRate));
164
165 unsigned bits=waveHeader.bitsPerSample;
166 if(bits!=16 && bits!=8)
167 throw runtime_error(string(__func__)+" -- an unlikely/unhandled bit rate of "+istring(bits));
168
169 #define REALLOC_FILE_SIZE (1024*1024/4)
170
171 sound->createWorkingPoolFile(filename,sampleRate,channelCount,REALLOC_FILE_SIZE);
172
173 for(unsigned t=0;t<channelCount;t++)
174 accessers[t]=new CRezPoolAccesser(sound->getAudio(t));
175
176 #define BUFFER_SIZE 4096
177
178 // print initial stderr from lame
179 char errBuffer[BUFFER_SIZE+1];
180 while(fgets(errBuffer,BUFFER_SIZE,errStream)!=NULL) // non-blocking i/o set by mypopen on this stream
181 printf("%s",errBuffer);
182
183 TAutoBuffer<int8_t> mem_buffer((bits/8)*BUFFER_SIZE*channelCount); // set this up so it deallocates itself
184 void * const buffer=mem_buffer;
185
186 sample_pos_t pos=0;
187
188 CStatusBar statusBar("Loading Sound",0,100,true);
189 for(;;)
190 {
191 size_t chunkSize=fread(buffer,(bits/8)*channelCount,BUFFER_SIZE,p);
192 if(chunkSize<=0)
193 break;
194
195 if((pos+chunkSize)>sound->getLength())
196 sound->addSpace(sound->getLength(),REALLOC_FILE_SIZE);
197
198 if(bits==16)
199 {
200 for(unsigned c=0;c<channelCount;c++)
201 {
202 CRezPoolAccesser &accesser=*(accessers[c]);
203 for(unsigned i=0;i<chunkSize;i++)
204 accesser[pos+i]=convert_sample<int16_t,sample_t>(lethe(((int16_t *)buffer)[i*channelCount+c]));
205 }
206 }
207 else
208 throw runtime_error(string(__func__)+" -- an unhandled bit rate of "+istring(bits));
209
210 pos+=chunkSize;
211
212 // read and parse the stderr of 'lame' to determine the progress of the load
213 while(fgets(errBuffer,BUFFER_SIZE,errStream)!=NULL) // non-blocking i/o set by mypopen on this stream
214 {
215 int frameNumber,totalFrames;
216 sscanf(errBuffer,"%*s %d%*c%d ",&frameNumber,&totalFrames);
217 printf("%s",errBuffer);
218 if(statusBar.update(frameNumber*100/totalFrames))
219 { // cancelled
220 ret=false;
221 goto cancelled;
222 }
223 }
224
225 }
226 printf("\n"); // after lame stderr output
227
228 // remove any extra allocated space
229 if(sound->getLength()>pos)
230 sound->removeSpace(pos,sound->getLength()-pos);
231
232 cancelled:
233
234 for(unsigned t=0;t<MAX_CHANNELS;t++)
235 delete accessers[t];
236
237 pclose(p);
238 }
239 catch(...)
240 {
241 for(unsigned t=0;t<MAX_CHANNELS;t++)
242 delete accessers[t];
243
244 pclose(p);
245
246 throw;
247 }
248
249 return ret;
250 }
251
onSaveSound(const string filename,const CSound * sound,const sample_pos_t saveStart,const sample_pos_t saveLength,bool useLastUserPrefs) const252 bool ClameSoundTranslator::onSaveSound(const string filename,const CSound *sound,const sample_pos_t saveStart,const sample_pos_t saveLength,bool useLastUserPrefs) const
253 {
254 bool ret=true;
255
256 if(gPathToLame=="")
257 throw runtime_error(string(__func__)+" -- path to 'lame' not set");
258
259 if(CPath(filename).extension()!="mp3")
260 throw runtime_error(string(__func__)+" -- can only encode in MPEG Layer-3");
261
262 // get user preferences for saving the mp3
263 static bool parametersGotten=false;
264 static AFrontendHooks::Mp3CompressionParameters parameters;
265 useLastUserPrefs&=parametersGotten;
266 if(!useLastUserPrefs)
267 {
268 if(!gFrontendHooks->promptForMp3CompressionParameters(parameters))
269 return false;
270 parametersGotten=true;
271 }
272
273 if(sound->getCueCount()>0 || sound->getUserNotes()!="")
274 {
275 // don't prompt the user if they've already answered this question
276 if(!useLastUserPrefs)
277 {
278 if(Question(_("MPEG Layer-3 does not support saving user notes or cues\nDo you wish to continue?"),yesnoQues)!=yesAns)
279 return false;
280 }
281 }
282
283 removeExistingFile(filename);
284
285 string cmdLine=gPathToLame+" ";
286
287 if(!parameters.useFlagsOnly)
288 {
289 if(parameters.method==AFrontendHooks::Mp3CompressionParameters::brCBR)
290 {
291 cmdLine+=" -b "+istring(parameters.constantBitRate/1000)+" ";
292 }
293 else if(parameters.method==AFrontendHooks::Mp3CompressionParameters::brABR)
294 {
295 cmdLine+=" --abr "+istring(parameters.normBitRate/1000)+" -b "+istring(parameters.minBitRate/1000)+" -B "+istring(parameters.maxBitRate/1000)+" ";
296 }
297 else if(parameters.method==AFrontendHooks::Mp3CompressionParameters::brQuality)
298 {
299 cmdLine+=" -V "+istring(parameters.quality)+" ";
300 }
301 else
302 throw runtime_error(string(__func__)+" -- internal error -- unhandle bit rate method "+istring((int)parameters.method));
303 }
304
305 cmdLine+=" "+parameters.additionalFlags+" ";
306
307 cmdLine+=" - "+escapeFilename(filename);
308
309 fprintf(stderr,"lame command line: '%s'\n",cmdLine.c_str());
310
311 setupSIGPIPEHandler();
312
313 FILE *p=popen(cmdLine,"w",NULL);
314
315 CRezPoolAccesser *accessers[MAX_CHANNELS]={0};
316 try
317 {
318 const unsigned channelCount=sound->getChannelCount();
319
320
321 #define BITS 16 // has to go along with how we're writing it to the pipe below
322
323 if(saveLength>((0x7fffffff-4096)/((BITS/8)*channelCount)))
324 throw runtime_error(string(__func__)+" -- audio data is too large to be converted to mp3 (more than 2gigs of "+istring(BITS)+"bit/"+istring(channelCount)+"channels)");
325
326 RWaveHeader waveHeader;
327 strncpy(waveHeader.RIFF_ID,"RIFF",4);
328 waveHeader.fileSize=36+(saveLength*(channelCount*(BITS/8)));
329 strncpy(waveHeader.WAVE_ID,"WAVE",4);
330 strncpy(waveHeader.fmt_ID,"fmt ",4);
331 waveHeader.fmtSize=16;
332 waveHeader.dataType=1;
333 waveHeader.channelCount=channelCount;
334 waveHeader.sampleRate=sound->getSampleRate();
335 waveHeader.bytesPerSec=sound->getSampleRate()*channelCount*(BITS/8);
336 waveHeader.bitsPerSample=BITS;
337 strncpy(waveHeader.data_ID,"data",4);
338 waveHeader.dataLength=saveLength*(channelCount*(BITS/8));
339
340 if(SIGPIPECaught)
341 throw runtime_error(string(__func__)+" -- lame aborted -- check stderr for more information");
342
343 waveHeader.convertToLE();
344 fwrite(&waveHeader,1,sizeof(waveHeader),p);
345
346 for(unsigned t=0;t<channelCount;t++)
347 accessers[t]=new CRezPoolAccesser(sound->getAudio(t));
348
349 #define BUFFER_SIZE 4096
350
351 TAutoBuffer<int16_t> buffer(BUFFER_SIZE*channelCount);
352 sample_pos_t pos=0;
353
354 CStatusBar statusBar(_("Saving Sound"),0,saveLength,true);
355 while(pos<saveLength)
356 {
357 size_t chunkSize=BUFFER_SIZE;
358 if(pos+chunkSize>saveLength)
359 chunkSize=saveLength-pos;
360
361 for(unsigned c=0;c<channelCount;c++)
362 {
363 const CRezPoolAccesser &accesser=*(accessers[c]);
364 for(unsigned i=0;i<chunkSize;i++)
365 buffer[i*channelCount+c]=hetle( (convert_sample<sample_t,int16_t>(accesser[pos+i+saveStart])) );
366 }
367
368 pos+=chunkSize;
369
370 if(SIGPIPECaught)
371 throw runtime_error(string(__func__)+" -- lame aborted -- check stderr for more information");
372 if(fwrite(buffer,sizeof(int16_t)*channelCount,chunkSize,p)!=chunkSize)
373 fprintf(stderr,"%s -- dropped some data while writing\n",__func__);
374
375 if(statusBar.update(pos))
376 { // cancelled
377 ret=false;
378 goto cancelled;
379 }
380 }
381
382 cancelled:
383
384 for(unsigned t=0;t<MAX_CHANNELS;t++)
385 delete accessers[t];
386
387 pclose(p);
388
389 restoreOrigSIGPIPEHandler();
390 }
391 catch(...)
392 {
393 for(unsigned t=0;t<MAX_CHANNELS;t++)
394 delete accessers[t];
395
396 pclose(p);
397
398 restoreOrigSIGPIPEHandler();
399
400 throw;
401 }
402
403 if(!ret)
404 unlink(filename.c_str()); // remove the cancelled file
405
406 return ret;
407 }
408
409
handlesExtension(const string extension,const string filename) const410 bool ClameSoundTranslator::handlesExtension(const string extension,const string filename) const
411 {
412 return extension=="mp3" || extension=="mp2" || extension=="mp1";
413 }
414
supportsFormat(const string filename) const415 bool ClameSoundTranslator::supportsFormat(const string filename) const
416 {
417 return handlesExtension(CPath(filename).extension(),filename);
418 // I've tried and only can really get lame to know the format
419 // from the extension unless I can do some analysis on it myself
420 //return false;
421 }
422
getFormatNames() const423 const vector<string> ClameSoundTranslator::getFormatNames() const
424 {
425 vector<string> names;
426
427 names.push_back("MPEG Layer-3,2,1");
428
429 return names;
430 }
431
getFormatFileMasks() const432 const vector<vector<string> > ClameSoundTranslator::getFormatFileMasks() const
433 {
434 vector<vector<string> > list;
435 vector<string> fileMasks;
436
437 fileMasks.clear();
438 fileMasks.push_back("*.mp3");
439 fileMasks.push_back("*.mp2");
440 fileMasks.push_back("*.mp1");
441 list.push_back(fileMasks);
442
443 return list;
444 }
445
446