Turn audio into a shareable video. forked from nypublicradio/audiogram

index.js 4.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. var path = require("path"),
  2. queue = require("d3").queue,
  3. mkdirp = require("mkdirp"),
  4. rimraf = require("rimraf"),
  5. serverSettings = require("../settings/"),
  6. transports = require("../lib/transports/"),
  7. logger = require("../lib/logger/"),
  8. getWaveform = require("./waveform.js"),
  9. initializeCanvas = require("./initialize-canvas.js"),
  10. drawFrames = require("./draw-frames.js"),
  11. combineFrames = require("./combine-frames.js"),
  12. trimAudio = require("./trim.js");
  13. function Audiogram(settings) {
  14. // Unique audiogram ID
  15. this.id = settings.id;
  16. this.settings = settings;
  17. // File locations to use
  18. this.dir = path.join(serverSettings.workingDirectory, this.id);
  19. this.audioPath = path.join(this.dir, "audio");
  20. this.videoPath = path.join(this.dir, "video.mp4");
  21. this.frameDir = path.join(this.dir, "frames");
  22. return this;
  23. }
  24. // Get the waveform data from the audio file, split into frames
  25. Audiogram.prototype.getWaveform = function(cb) {
  26. var self = this;
  27. this.status("waveform");
  28. getWaveform(this.audioPath, {
  29. samplesPerFrame: this.settings.samplesPerFrame,
  30. maxDuration: this.settings.maxDuration
  31. }, function(err, waveform){
  32. self.set("numFrames", self.numFrames = self.settings.waveform.length);
  33. return cb(err, self.settings.waveform = waveform);
  34. });
  35. };
  36. // Trim the audio by the start and end time specified
  37. Audiogram.prototype.trimAudio = function(start, end, cb) {
  38. var self = this;
  39. this.status("trim");
  40. // FFmpeg needs an extension to sniff
  41. var trimmedPath = this.audioPath + "-trimmed.mp3";
  42. trimAudio({
  43. origin: this.audioPath,
  44. destination: trimmedPath,
  45. startTime: start,
  46. endTime: end
  47. }, function(err){
  48. if (err) {
  49. return cb(err);
  50. }
  51. self.audioPath = trimmedPath;
  52. return cb(null);
  53. });
  54. };
  55. // Initialize the canvas and draw all the frames
  56. Audiogram.prototype.drawFrames = function(cb) {
  57. var self = this;
  58. this.status("renderer");
  59. initializeCanvas(this.settings, function(err, renderer){
  60. if (err) {
  61. return cb(err);
  62. }
  63. self.status("frames");
  64. drawFrames(renderer, {
  65. numFrames: self.numFrames,
  66. frameDir: self.frameDir,
  67. tick: function() {
  68. transports.incrementField(self.id, "framesComplete");
  69. }
  70. }, cb);
  71. });
  72. };
  73. // Combine the frames and audio into the final video with FFmpeg
  74. Audiogram.prototype.combineFrames = function(cb) {
  75. this.status("combine");
  76. combineFrames({
  77. framePath: path.join(this.frameDir, "%06d.png"),
  78. audioPath: this.audioPath,
  79. videoPath: this.videoPath,
  80. framesPerSecond: this.settings.framesPerSecond
  81. }, cb);
  82. };
  83. // Master render function, queue up steps in order
  84. Audiogram.prototype.render = function(cb) {
  85. var self = this,
  86. q = queue(1);
  87. this.status("audio-download");
  88. // Set up tmp directory
  89. q.defer(mkdirp, this.frameDir);
  90. // Download the stored audio file
  91. q.defer(transports.downloadAudio, "audio/" + this.id, this.audioPath);
  92. // If the audio needs to be clipped, clip it first and update the path
  93. if (this.settings.start || this.settings.end) {
  94. q.defer(this.trimAudio.bind(this), this.settings.start || 0, this.settings.end);
  95. }
  96. // Get the audio's duration for computing number of frames
  97. q.defer(this.probe.bind(this));
  98. // Get the audio waveform data
  99. q.defer(this.getWaveform.bind(this));
  100. // Draw all the frames
  101. q.defer(this.drawFrames.bind(this));
  102. // Combine audio and frames together with ffmpeg
  103. q.defer(this.combineFrames.bind(this));
  104. // Upload video to S3 or move to local storage
  105. q.defer(transports.uploadVideo, this.videoPath, "video/" + this.id + ".mp4");
  106. // Delete working directory
  107. q.defer(rimraf, this.dir);
  108. // Final callback, results in a URL where the finished video is accessible
  109. q.await(function(err){
  110. if (!err) {
  111. self.set("url", transports.getURL(self.id));
  112. }
  113. return cb(err);
  114. });
  115. return this;
  116. };
  117. Audiogram.prototype.set = function(field, value) {
  118. logger.debug(field + "=" + value);
  119. transports.setField(this.id, field, value);
  120. return this;
  121. };
  122. // Convenience method for .set("status")
  123. Audiogram.prototype.status = function(value) {
  124. return this.set("status", value);
  125. };
  126. module.exports = Audiogram;