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