waveform.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. <?php
  2. /**
  3. *
  4. *
  5. * @author MaximAL
  6. * @since 2019-02-13 Added `$onePhase` parameters to get only positive waveform data and image
  7. * @since 2018-10-22 Added `getWaveformData()` method and `$soxCommand` configuration
  8. * @since 2016-11-21
  9. * @date 2016-11-21
  10. * @time 19:08
  11. * @link http://maximals.ru
  12. * @link http://sijeko.ru
  13. * @link https://github.com/maximal/audio-waveform-php
  14. * @copyright © MaximAL, Sijeko 2016-2019
  15. *
  16. * @modified fusionate
  17. * @since 2024-02-07 Added option to return image in base64 format by setting $filename to 'base64'
  18. * @since 2024-02-08 Added `$singleAxis` parameter to combine channels (if stereo) into single axis
  19. * @since 2024-02-08 Added `$colorA` and `$colorB` parameters to allow different colors for each channel
  20. * @since 2024-02-08 Rename `$onePhase` parameter to `$singlePhase` and change to public static variable for class
  21. * @since 2024-02-08 Modified singleAxis so channel 2 would display as negative waveform data when singlePhase enabled
  22. *
  23. *
  24. */
  25. namespace maximal\audio;
  26. /**
  27. * Waveform class allows you to get waveform data and images from audio files
  28. * @package maximal\audio
  29. */
  30. class Waveform
  31. {
  32. protected $filename;
  33. protected $info;
  34. protected $channels;
  35. protected $samples;
  36. protected $sampleRate;
  37. protected $duration;
  38. public static $linesPerPixel = 8;
  39. public static $samplesPerLine = 512;
  40. public static $singlePhase; // set `true` to get positive waveform phase only, `false` to get both positive and negative waveform phases
  41. public static $singleAxis; // combine double or single phases to use same axis
  42. // Colors in CSS `rgba(red, green, blue, opacity)` format
  43. public static $color = [95, 95, 95, 0.5];
  44. public static $colorA; // color of left channel (1)
  45. public static $colorB; // color of right channel (2)
  46. public static $backgroundColor = [245, 245, 245, 1];
  47. public static $axisColor = [0, 0, 0, 0.1];
  48. // SoX command: 'sox', '/usr/local/bin/sox' etc
  49. public static $soxCommand = 'sox';
  50. public function __construct($filename)
  51. {
  52. $this->filename = $filename;
  53. }
  54. public function getInfo()
  55. {
  56. $out = null;
  57. $ret = null;
  58. exec(self::$soxCommand . ' --i ' . escapeshellarg($this->filename) . ' 2>&1', $out, $ret);
  59. $str = implode('|', $out);
  60. $match = null;
  61. if (preg_match('/Channels?\s*\:\s*(\d+)/ui', $str, $match)) {
  62. $this->channels = intval($match[1]);
  63. }
  64. $match = null;
  65. if (preg_match('/Sample\s*Rate\s*\:\s*(\d+)/ui', $str, $match)) {
  66. $this->sampleRate = intval($match[1]);
  67. }
  68. $match = null;
  69. if (preg_match('/Duration.*[^\d](\d+)\s*samples?/ui', $str, $match)) {
  70. $this->samples = intval($match[1]);
  71. }
  72. if ($this->samples && $this->sampleRate) {
  73. $this->duration = 1.0 * $this->samples / $this->sampleRate;
  74. }
  75. if ($ret !== 0) {
  76. throw new \Exception('Failed to get audio info.' . PHP_EOL . 'Error: ' . implode(PHP_EOL, $out) . PHP_EOL);
  77. }
  78. }
  79. public function getSampleRate()
  80. {
  81. if (!$this->sampleRate) {
  82. $this->getInfo();
  83. }
  84. return $this->sampleRate;
  85. }
  86. public function getChannels()
  87. {
  88. if (!$this->channels) {
  89. $this->getInfo();
  90. }
  91. return $this->channels;
  92. }
  93. public function getSamples()
  94. {
  95. if (!$this->samples) {
  96. $this->getInfo();
  97. }
  98. return $this->samples;
  99. }
  100. public function getDuration()
  101. {
  102. if (!$this->duration) {
  103. $this->getInfo();
  104. }
  105. return $this->duration;
  106. }
  107. /**
  108. * Get waveform from the audio file.
  109. * @param string $filename Image file name
  110. * @param int $width Width of the image file in pixels
  111. * @param int $height Height of the image file in pixels
  112. * @return bool Returns `true` on success or `false` on failure, when generating an image file, or a base64 string.
  113. * @throws \Exception
  114. */
  115. public function getWaveform($filename, $width, $height)
  116. {
  117. // Calculating parameters
  118. $needChannels = $this->getChannels() > 1 ? 2 : 1;
  119. $data = $this->getWaveformData($width, self::$singlePhase ?? false);
  120. $lines1 = $data['lines1'];
  121. $lines2 = $data['lines2'];
  122. // Creating image
  123. $img = imagecreatetruecolor($width, $height);
  124. imagesavealpha($img, true);
  125. //if (function_exists('imageantialias')) {
  126. // imageantialias($img, true);
  127. //}
  128. // Colors
  129. $back = self::rgbaToColor($img, self::$backgroundColor);
  130. $color = self::rgbaToColor($img, self::$color);
  131. $colorA = self::$colorA ? self::rgbaToColor($img, self::$colorA) : null;
  132. $colorB = self::$colorB ? self::rgbaToColor($img, self::$colorB) : null;
  133. $axis = self::rgbaToColor($img, self::$axisColor);
  134. $singleAxis = self::$singleAxis ?? false;
  135. imagefill($img, 0, 0, $back);
  136. // Center Ys
  137. if ($singleAxis) {
  138. $center1 = $center2 = $height / 2;
  139. } else {
  140. if (self::$singlePhase ?? false) {
  141. $center1 = $needChannels === 2 ? $height / 2 - 1: $height - 1;
  142. $center2 = $needChannels === 2 ? $height - 1 : null;
  143. } else {
  144. $center1 = $needChannels === 2 ? ($height / 2 - 1) / 2 : $height / 2;
  145. $center2 = $needChannels === 2 ? $height - $center1 : null;
  146. }
  147. }
  148. // Drawing channel 1
  149. for ($i = 0; $i < count($lines1); $i += 2) {
  150. $x = $i / 2 / self::$linesPerPixel;
  151. if (self::$singlePhase ?? false) {
  152. $max = max($lines1[$i], $lines1[$i + 1]);
  153. @imageline($img, $x, $center1, $x, $center1 - $max * $center1, $colorA ?? $color);
  154. } else {
  155. $min = $lines1[$i];
  156. $max = $lines1[$i + 1];
  157. @imageline($img, $x, $center1 - $min * $center1, $x, $center1 - $max * $center1, $colorA ?? $color);
  158. }
  159. }
  160. // Drawing channel 2
  161. for ($i = 0; $i < count($lines2); $i += 2) {
  162. $x = $i / 2 / self::$linesPerPixel;
  163. if (self::$singlePhase ?? false) {
  164. $max = max($lines2[$i], $lines2[$i + 1]);
  165. if ($singleAxis) {
  166. @imageline($img, $x, $center1, $x, $center1 + $max * $center2, $colorB ?? $color);
  167. } else {
  168. @imageline($img, $x, $center2, $x, $center2 - $max * $center1, $colorB ?? $color);
  169. }
  170. } else {
  171. if ($singleAxis) {
  172. $min = $lines2[$i];
  173. $max = $lines2[$i + 1];
  174. @imageline($img, $x, $center1 - $min * $center1, $x, $center1 - $max * $center1, $colorB ?? $color);
  175. } else {
  176. $min = $lines2[$i];
  177. $max = $lines2[$i + 1];
  178. @imageline($img, $x, $center2 - $min * $center1, $x, $center2 - $max * $center1, $colorB ?? $color);
  179. }
  180. }
  181. }
  182. // Axis
  183. @imageline($img, 0, $center1, $width - 1, $center1, $axis);
  184. if ($center2 !== null) {
  185. @imageline($img, 0, $center2, $width - 1, $center2, $axis);
  186. }
  187. if ($filename == 'base64') {
  188. ob_start();
  189. imagepng($img);
  190. $image_data = ob_get_clean();
  191. return base64_encode($image_data);
  192. } else {
  193. return imagepng($img, $filename);
  194. }
  195. }
  196. /**
  197. * Get waveform data from the audio file.
  198. * @param int $width Desired width of the image file in pixels
  199. * @return array
  200. * @throws \Exception
  201. */
  202. public function getWaveformData($width)
  203. {
  204. // Calculating parameters
  205. $needChannels = $this->getChannels() > 1 ? 2 : 1;
  206. $samplesPerPixel = self::$samplesPerLine * self::$linesPerPixel;
  207. $needRate = 1.0 * $width * $samplesPerPixel * $this->getSampleRate() / $this->getSamples();
  208. //if ($needRate > 4000) {
  209. // $needRate = 4000;
  210. //}
  211. // Command text
  212. $command = self::$soxCommand . ' ' . escapeshellarg($this->filename) .
  213. ' -c ' . $needChannels .
  214. ' -r ' . $needRate . ' -e floating-point -t raw -';
  215. //var_dump($command);
  216. $outputs = [
  217. 1 => ['pipe', 'w'], // stdout
  218. 2 => ['pipe', 'w'], // stderr
  219. ];
  220. $pipes = null;
  221. $proc = proc_open($command, $outputs, $pipes);
  222. if (!$proc) {
  223. throw new \Exception('Failed to run `sox` command');
  224. }
  225. $lines1 = [];
  226. $lines2 = [];
  227. while ($chunk = fread($pipes[1], 4 * $needChannels * self::$samplesPerLine)) {
  228. $data = unpack('f*', $chunk);
  229. $channel1 = [];
  230. $channel2 = [];
  231. foreach ($data as $index => $sample) {
  232. if ($needChannels === 2 && $index % 2 === 0) {
  233. $channel2 []= $sample;
  234. } else {
  235. $channel1 []= $sample;
  236. }
  237. }
  238. if (self::$singlePhase ?? false) {
  239. // Rectifying to get positive values only
  240. $lines1 []= abs(min($channel1));
  241. $lines1 []= abs(max($channel1));
  242. if ($needChannels === 2) {
  243. $lines2 []= abs(min($channel2));
  244. $lines2 []= abs(max($channel2));
  245. }
  246. } else {
  247. // Two phases
  248. $lines1 []= min($channel1);
  249. $lines1 []= max($channel1);
  250. if ($needChannels === 2) {
  251. $lines2 []= min($channel2);
  252. $lines2 []= max($channel2);
  253. }
  254. }
  255. }
  256. $err = stream_get_contents($pipes[2]);
  257. $ret = proc_close($proc);
  258. if ($ret !== 0) {
  259. throw new \Exception('Failed to run `sox` command. Error:' . PHP_EOL . $err);
  260. }
  261. return ['lines1' => $lines1, 'lines2' => $lines2];
  262. }
  263. public static function rgbaToColor($img, $rgba)
  264. {
  265. return imagecolorallocatealpha($img, $rgba[0], $rgba[1], $rgba[2], round((1 - $rgba[3]) * 127));
  266. }
  267. }