Pencil2D  ff90c0872e88be3bf81c548cd60f01983012ec49
Pencil2D is an animation software for both bitmap and vector graphics. It is free, multi-platform, and open source.
 All Classes Functions
movieexporter.cpp
1 /*
2 
3 Pencil - Traditional Animation Software
4 Copyright (C) 2012-2017 Matthew Chiawen Chang
5 
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; version 2 of the License.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14 
15 */
16 
17 #include "movieexporter.h"
18 
19 #include <vector>
20 #include <cstdint>
21 #include <QDir>
22 #include <QDebug>
23 #include <QProcess>
24 #include <QApplication>
25 #include <QStandardPaths>
26 #include "object.h"
27 #include "layercamera.h"
28 #include "layersound.h"
29 #include "soundclip.h"
30 
31 #define IMAGE_FILENAME "/test_img_%05d.png"
32 
33 // refs
34 // http://www.topherlee.com/software/pcm-tut-wavformat.html
35 // http://soundfile.sapp.org/doc/WaveFormat/
36 //
38 {
39  char riff[ 4 ];
40  int32_t chuckSize;
41  char format[ 4 ];
42  char fmtID[ 4 ];
43  int32_t fmtChuckSize;
44  int16_t audioFormat;
45  int16_t numChannels;
46  int32_t sampleRate;
47  int32_t byteRate;
48  int16_t blockAlign;
49  int16_t bitsPerSample;
50  char dataChuckID[ 4 ];
51  int32_t dataSize;
52 
53  void InitWithDefaultValues()
54  {
55  strncpy( riff, "RIFF", 4 );
56  chuckSize = 0;
57  strncpy( format, "WAVE", 4 );
58  strncpy( fmtID, "fmt ", 4 );
59  fmtChuckSize = 16;
60  audioFormat = 1; // 1 means PCM
61  numChannels = 2; // stereo
62  sampleRate = 44100;
63  bitsPerSample = 16;
64  blockAlign = ( bitsPerSample * numChannels ) / 8;
65  byteRate = ( sampleRate * bitsPerSample * numChannels ) / 8;
66 
67  strncpy( dataChuckID, "data", 4 );
68  dataSize = 0;
69  }
70 };
71 
72 int16_t safeSumInt16( int16_t a, int16_t b )
73 {
74  int32_t a32 = static_cast<int32_t>( a );
75  int32_t b32 = static_cast<int32_t>( b );
76 
77  if ( ( a32 + b32 ) > INT16_MAX )
78  {
79  return INT16_MAX;
80  }
81  else if ( ( a32 + b32 ) < INT16_MIN )
82  {
83  return INT16_MIN;
84  }
85  return a + b;
86 }
87 
88 void skipUselessChucks( WavFileHeader& header, QFile& file )
89 {
90  // We only care about the 'data' chuck
91  while ( memcmp( header.dataChuckID, "data", 4 ) != 0 )
92  {
93  int skipByteCount = header.dataSize;
94  std::vector<char> skipData( skipByteCount );
95  file.read( skipData.data(), skipByteCount );
96 
97  file.read( (char*)&header.dataChuckID, 4 );
98  file.read( (char*)&header.dataSize, 4 );
99  }
100 }
101 
102 QString ffmpegLocation()
103 {
104 #ifdef _WIN32
105  return QApplication::applicationDirPath() + "/plugins/ffmpeg.exe";
106 #elif __APPLE__
107  return QApplication::applicationDirPath() + "/plugins/ffmpeg";
108 #else
109  QString ffmpegPath = QStandardPaths::findExecutable(
110  "ffmpeg",
111  QStringList()
112  << QApplication::applicationDirPath() + "/plugins"
113  << QApplication::applicationDirPath() + "/../plugins" // linuxdeployqt in FHS-like mode
114  );
115  if ( !ffmpegPath.isEmpty() )
116  {
117  return ffmpegPath;
118  }
119  return QStandardPaths::findExecutable( "ffmpeg" ); // ffmpeg is a standalone project.
120 #endif
121 }
122 
123 MovieExporter::MovieExporter()
124 {
125 }
126 
127 MovieExporter::~MovieExporter()
128 {
129 }
130 
131 Status MovieExporter::run(const Object* obj,
132  const ExportMovieDesc& desc,
133  std::function<void( float )> progress )
134 {
135  progress( 0.f );
136 
137  QString ffmpegPath = ffmpegLocation();
138  qDebug() << ffmpegPath;
139  if ( !QFile::exists( ffmpegPath ) )
140  {
141  #ifdef _WIN32
142  qDebug() << "Please place ffmpeg.exe in " << ffmpegPath << " directory";
143  #elif __APPLE__
144  qDebug() << "Please place ffmpeg in " << ffmpegPath << " directory";
145  #else
146  qDebug() << "Please place ffmpeg in " << ffmpegPath << " directory";
147  #endif
148  return Status::ERROR_FFMPEG_NOT_FOUND;
149  }
150 
151  STATUS_CHECK( checkInputParameters( desc ) );
152  mDesc = desc;
153 
154  qDebug() << "OutFile: " << mDesc.strFileName;
155 
156  // Setup temporary folder
157  if ( !mTempDir.isValid() )
158  {
159  Q_ASSERT( false && "Cannot create temp folder." );
160  return Status::FAIL;
161  }
162 
163  mTempWorkDir = mTempDir.path();
164  progress( 0.03f );
165 
166  if ( !desc.strFileName.endsWith( "gif" ) )
167  {
168  STATUS_CHECK( assembleAudio( obj, ffmpegPath, progress ) );
169  }
170  progress( 0.10f );
171 
172  STATUS_CHECK( generateImageSequence( obj, progress ) );
173  progress( 0.99f );
174 
175  twoPassEncoding( ffmpegPath, desc.strFileName );
176 
177  progress( 1.0f );
178 
179  return Status::OK;
180 }
181 
182 QString MovieExporter::error()
183 {
184  return QString();
185 }
186 
187 Status MovieExporter::assembleAudio( const Object* obj,
188  QString ffmpegPath,
189  std::function<void( float )> progress )
190 {
191  // Quicktime assemble call
192  int startFrame = mDesc.startFrame;
193  int endFrame = mDesc.endFrame;
194  int fps = mDesc.fps;
195 
196  Q_ASSERT( startFrame >= 0 );
197  Q_ASSERT( endFrame >= startFrame );
198 
199  float lengthInSec = ( endFrame - startFrame + 1 ) / (float)fps;
200  qDebug() << "Audio Length = " << lengthInSec << " seconds";
201 
202  int32_t audioDataSize = 44100 * 2 * 2 * lengthInSec;
203 
204  std::vector<int16_t> audioData( audioDataSize / sizeof( int16_t ) );
205 
206  bool audioDataValid = false;
207 
208  QDir dir( mTempWorkDir );
209  Q_ASSERT( dir.exists() );
210 
211  QString tempAudioPath = mTempWorkDir + "/tmpaudio0.wav";
212  qDebug() << "TempAudio=" << tempAudioPath;
213 
214  std::vector< SoundClip* > allSoundClips;
215 
216  std::vector< LayerSound* > allSoundLayers = obj->getLayersByType<LayerSound>();
217  for ( LayerSound* layer : allSoundLayers )
218  {
219  layer->foreachKeyFrame( [&allSoundClips]( KeyFrame* key )
220  {
221  allSoundClips.push_back( static_cast<SoundClip*>( key ) );
222  } );
223  }
224 
225  int clipCount = 0;
226 
227  for ( SoundClip* clip : allSoundClips )
228  {
229  if ( mCanceled )
230  {
231  return Status::CANCELED;
232  }
233 
234  // convert audio file: 44100Hz sampling rate, stereo, signed 16 bit little endian
235  // supported audio file types: wav, mp3, ogg... ( all file types supported by ffmpeg )
236  QString strCmd;
237  strCmd += QString("\"%1\"").arg( ffmpegPath );
238  strCmd += QString( " -i \"%1\" " ).arg( clip->fileName() );
239  strCmd += "-ar 44100 -acodec pcm_s16le -ac 2 -y ";
240  strCmd += QString( "\"%1\"" ).arg( tempAudioPath );
241 
242  executeFFMpegCommand( strCmd );
243  qDebug() << "audio file: " + tempAudioPath;
244 
245  // Read wav file header
246  WavFileHeader header;
247  QFile file( tempAudioPath );
248  file.open( QIODevice::ReadOnly );
249  file.read( (char*)&header, sizeof( WavFileHeader ) );
250 
251  skipUselessChucks( header, file );
252 
253  int32_t audioSize = header.dataSize;
254 
255  qDebug() << "audio len " << audioSize;
256 
257  // before calling malloc should check: audioSize < max credible value
258  std::vector< int16_t > data( audioSize / sizeof( int16_t ) );
259  file.read( (char*)data.data(), audioSize );
260  audioDataValid = true;
261 
262  float fframe = (float)clip->pos() / (float)fps;
263  int delta = fframe * 44100 * 2;
264  qDebug() << "audio delta " << delta;
265 
266  int indexMax = std::min( audioSize / 2, audioDataSize / 2 - delta );
267 
268  // audio files 'mixing': 'higher' sound layers overwrite 'lower' sound layers
269  for ( int i = 0; i < indexMax; i++ )
270  {
271  audioData[ i + delta ] = safeSumInt16( audioData[ i + delta ], data[ i ] );
272  }
273 
274  file.close();
275 
276  float p = ( (float)clipCount / allSoundClips.size() );
277  progress( p * 0.1f );
278  clipCount++;
279  }
280 
281  if ( !audioDataValid )
282  {
283  return Status::SAFE;
284  }
285 
286  // save mixed audio file ( will be used as audio stream )
287  QFile file( mTempWorkDir + "/tmpaudio.wav" );
288  file.open( QIODevice::WriteOnly );
289 
290  WavFileHeader outputHeader;
291  outputHeader.InitWithDefaultValues();
292  outputHeader.dataSize = audioDataSize;
293  outputHeader.chuckSize = 36 + audioDataSize;
294 
295  file.write( (char*)&outputHeader, sizeof( outputHeader ) );
296  file.write( (char*)audioData.data(), audioDataSize );
297  file.close();
298 
299  return Status::OK;
300 }
301 
302 Status MovieExporter::generateImageSequence(
303  const Object* obj,
304  std::function<void(float)> progress )
305 {
306  int frameStart = mDesc.startFrame;
307  int frameEnd = mDesc.endFrame;
308  QSize exportSize = mDesc.exportSize;
309  bool transparency = false;
310  QString strCameraName = mDesc.strCameraName;
311 
312  auto cameraLayer = (LayerCamera*)obj->findLayerByName( strCameraName, Layer::CAMERA );
313  if ( cameraLayer == nullptr )
314  {
315  cameraLayer = obj->getLayersByType< LayerCamera >().front();
316  }
317 
318  for ( int currentFrame = frameStart; currentFrame <= frameEnd; currentFrame++ )
319  {
320  if ( mCanceled )
321  {
322  return Status::CANCELED;
323  }
324 
325  QImage imageToExport( exportSize, QImage::Format_ARGB32_Premultiplied );
326  QColor bgColor = Qt::white;
327  if ( transparency )
328  {
329  bgColor.setAlpha( 0 );
330  }
331  imageToExport.fill( bgColor );
332 
333  QPainter painter( &imageToExport );
334 
335  QTransform view = cameraLayer->getViewAtFrame( currentFrame );
336 
337  QSize camSize = cameraLayer->getViewSize();
338  QTransform centralizeCamera;
339  centralizeCamera.translate( camSize.width() / 2, camSize.height() / 2 );
340 
341  painter.setWorldTransform( view * centralizeCamera );
342  painter.setWindow( QRect( 0, 0, camSize.width(), camSize.height() ) );
343 
344  obj->paintImage( painter, currentFrame, false, true );
345 
346  QString imageFileWithFrameNumber = QString().sprintf( IMAGE_FILENAME, currentFrame );
347 
348  QString strImgPath = mTempWorkDir + imageFileWithFrameNumber;
349  bool bSave = imageToExport.save( strImgPath );
350  Q_ASSERT(bSave);
351  qDebug() << "Save img to: " << strImgPath << ", Success=" << bSave;
352 
353  float fProgressValue = ( currentFrame / (float)( frameEnd - frameStart ) );
354  progress( 0.1f + ( fProgressValue * 0.99f ) );
355  }
356 
357  return Status::OK;
358 }
359 
360 Status MovieExporter::combineVideoAndAudio( QString ffmpegPath, QString strOutputFile )
361 {
362  if ( mCanceled )
363  {
364  return Status::CANCELED;
365  }
366 
367  //int exportFps = mDesc.videoFps;
368  const QString imgPath = mTempWorkDir + IMAGE_FILENAME;
369  const QString tempAudioPath = mTempWorkDir + "/tmpaudio.wav";
370  const QSize exportSize = mDesc.exportSize;
371 
372  QString strCmd = QString("\"%1\"").arg( ffmpegPath );
373  strCmd += QString( " -f image2");
374  strCmd += QString( " -framerate %1" ).arg( mDesc.fps );
375  strCmd += QString( " -pix_fmt yuv420p" );
376  strCmd += QString( " -start_number %1" ).arg( mDesc.startFrame );
377  //strCmd += QString( " -r %1" ).arg( exportFps );
378  strCmd += QString( " -i \"%1\" " ).arg( imgPath );
379 
380  if ( QFile::exists( tempAudioPath ) )
381  {
382  strCmd += QString( " -i \"%1\" " ).arg( tempAudioPath );
383  }
384 
385  strCmd += QString( " -s %1x%2" ).arg( exportSize.width() ).arg( exportSize.height() );
386  strCmd += " -y";
387  strCmd += QString(" \"%1\"" ).arg( strOutputFile );
388 
389  STATUS_CHECK( executeFFMpegCommand( strCmd ) );
390 
391  return Status::OK;
392 }
393 
394 Status MovieExporter::twoPassEncoding( QString ffmpeg, QString strOutputFile )
395 {
396  QString strTempVideo = mTempWorkDir + "/Temp1.mp4";
397  qDebug() << "TempVideo=" << strTempVideo;
398 
399  combineVideoAndAudio( ffmpeg, strTempVideo );
400 
401  if ( strOutputFile.endsWith( "gif" ) )
402  {
403  STATUS_CHECK( convertToGif( ffmpeg, strTempVideo, strOutputFile ) );
404  }
405  else
406  {
407  STATUS_CHECK( convertVideoAgain( ffmpeg, strTempVideo, strOutputFile ) );
408  }
409 
410  return Status::OK;
411 }
412 
413 Status MovieExporter::convertVideoAgain( QString ffmpegPath, QString strIn, QString strOut )
414 {
415  QString strCmd = QString("\"%1\"").arg( ffmpegPath );
416  strCmd += QString( " -i \"%1\" " ).arg( strIn );
417  strCmd += QString( " -pix_fmt yuv420p" );
418  strCmd += " -y";
419  strCmd += QString(" \"%1\"" ).arg( strOut );
420 
421  STATUS_CHECK( executeFFMpegCommand( strCmd ) );
422  return Status::OK;
423 }
424 
425 Status MovieExporter::convertToGif( QString ffmpeg, QString strIn, QString strOut )
426 {
427  // http://superuser.com/questions/556029/
428  // generate a palette
429  QString strGifPalette = mTempWorkDir + "/palette.png";
430  QString strCmd1 = QString( "\"%1\"" ).arg( ffmpeg );
431  strCmd1 += " -y";
432  strCmd1 += QString( " -i \"%1\"" ).arg( strIn );
433  strCmd1 += " -vf scale=320:-1:flags=lanczos,palettegen";
434  strCmd1 += QString( " \"%1\"" ).arg( strGifPalette );
435 
436  STATUS_CHECK( executeFFMpegCommand( strCmd1 ) );
437 
438  // Output the GIF using the palette:
439  QString strCmd2 = QString( "\"%1\"" ).arg( ffmpeg );
440  strCmd2 += " -y";
441  strCmd2 += QString( " -i \"%1\"" ).arg( strIn );
442  strCmd2 += QString( " -i \"%1\"" ).arg( strGifPalette );
443  strCmd2 += " -filter_complex \"scale=-1:-1:flags=lanczos[x];[x][1:v]paletteuse\"";
444  strCmd2 += QString( " \"%1\"" ).arg( strOut );
445 
446  STATUS_CHECK( executeFFMpegCommand( strCmd2 ) );
447 
448  return Status::OK;
449 }
450 
451 Status MovieExporter::executeFFMpegCommand( QString strCmd )
452 {
453  qDebug() << strCmd;
454 
455  QProcess ffmpeg;
456  ffmpeg.start( strCmd );
457  if ( ffmpeg.waitForStarted() == true )
458  {
459  if ( ffmpeg.waitForFinished() == true )
460  {
461  qDebug() << "stdout: " + ffmpeg.readAllStandardOutput();
462  qDebug() << "stderr: " + ffmpeg.readAllStandardError();
463  }
464  else
465  {
466  qDebug() << "ERROR: FFmpeg did not finish executing.";
467  return Status::FAIL;
468  }
469  }
470  else
471  {
472  qDebug() << "ERROR: Could not execute FFmpeg.";
473  return Status::FAIL;
474  }
475  return Status::OK;
476 }
477 
478 Status MovieExporter::checkInputParameters( const ExportMovieDesc& desc )
479 {
480  bool b = true;
481  b &= ( !desc.strFileName.isEmpty() );
482  b &= ( desc.startFrame > 0 );
483  b &= ( desc.endFrame >= desc.startFrame );
484  b &= ( desc.fps > 0 );
485  b &= ( !desc.strCameraName.isEmpty() );
486 
487  return b ? Status::OK : Status::INVALID_ARGUMENT;
488 }
Definition: object.h:71