So erzeugt man eine Waveform mit PHP

Mit PHP eine Waveform erzeugen 5 von 5 mit 5 Stimmen
| Sick^

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

Werbung
Dieser Artikel erklärt wie man mit PHP eine Waveform erstellt. Als Waveform versteht man die garfische Darstellung eines beliebigen Audiosignals. In diesem Fall erstelle ich eine Waveform für Audiodateien die im Dateisystem hochgeladen werden.

Vorbereiten der Waveform

Angenommen wir haben 10 verschiedene Audioformate, welche wir alle unterstützen und mit einer Waveform visualisieren wollen. Nun wollen wir aber nicht 10 Codecs implementieren. Also benötigen wir ein einheitliches Format, welches es uns ermöglicht die Waveform zu generieren. Hier kommt das gute alte WAV Format ins Spiel. Da das WAV Format unkomprimiert und ausreichend dokumentiert ist, werden wir versuchen jede Audiodatei in dieses Format zu konvertieren. Somit umgehen wir das nervige Decoden der Audiodateien und wir müssen nur einen Standard unterstützen. In linux habe ich mir für das Konvertieren diverse Codecs und den MPlayer installiert.
sudo apt-get install faac faad flac lame libmad0 libmpcdec6 mppenc vorbis-tools wavpack mplayer
Der MPlayer konvertiert für uns so ziemlich jedes Format erst mal in eine MP3.

PHP-Quellcode: Konvertieren zum MP3 Format

  1. $commandPath = @exec('which "mplayer"');
  2. if ($commandPath) {
  3. // Prepare the convert command using mplayer
  4. $command = '%1$s -vo null -vc null -ao pcm:fast %2$s &&
  5. lame -m s audiodump.wav %3$s &&
  6. rm audiodump.wav';
  7. // Get the path to the new file
  8. $fileName = $this->getLocation('converted.mp3');
  9. // Execute the command
  10. @exec(sprintf($command, $commandPath, $this->getLocation(), $fileName));
  11. }
Alles anzeigen


Wir wandeln die Audiodateien erst in eine MP3, da man in der Regel eine Audiodatei auch wiedergeben möchte. Somit haben wir auf jeden Fall ein platzsparendes Format, welches so gut wie überall unterstützt wird.
Hier könnte man jetzt, wie in dem Artikel über die MPEG Frames beschrieben, die Spielzeit und andere Informationen auslesen bevor man anfängt die Waveform zu erzeugen.

Nun gebe ich euch hier meine BinaryReader Klasse mit, welche euch hilft die Audiodateien zu lesen.
BinaryReader.class.php

PHP-Quellcode

  1. <?php
  2. namespace file\system\io;
  3. /**
  4. * This class helps to read files in binary format
  5. *
  6. * @author Thomas Schlage
  7. * @copyright 2013 Thomas Schlage
  8. * @license Commercial License <http://www.binpress.com/license/view/l/831258010c5e26599b815cfe8621d912>
  9. * @package com.stagetwo.file
  10. * @link http://www.stagetwo.eu
  11. */
  12. class BinaryReader {
  13. /**
  14. * The total file size
  15. */
  16. public $dataSize = 0;
  17. /**
  18. * The stream handle
  19. */
  20. protected $fileHandle = null;
  21. /**
  22. * Holds a sequence of bytes to read the bits from it
  23. */
  24. private $bitMemory = null;
  25. /**
  26. * The current bit position on the byte sequence
  27. */
  28. private $bitPositon = 0;
  29. /**
  30. * Initialize the file handle
  31. *
  32. * @param String Url to target file
  33. */
  34. public function __construct($file) {
  35. // Check if we have a url or a stream resource
  36. if (getType($file) == 'string') {
  37. $this->fileHandle = @fopen($file, 'r');
  38. $this->dataSize = filesize($file);
  39. }
  40. else if (getType($file) == 'resource') {
  41. // Use the stream resource as handle
  42. $this->fileHandle = $file;
  43. // Seek to end of file
  44. fseek($this->fileHandle, 0, SEEK_END);
  45. // The end of file is our dataSize
  46. $this->dataSize = $this->pos();
  47. // Go back to begin of file
  48. $this->seek(0);
  49. }
  50. }
  51. /**
  52. * Read a String until the next occurence of seperator or until length is reached
  53. *
  54. * @param Integer The maximum Length to read
  55. * @param Integer The string until we read
  56. * @return String The string between current offset and offset + length or offset + offsetOfSeperator
  57. */
  58. public function readString($length = null, $seperator = null) {
  59. if ($length == 0) return false;
  60. if (!$length) $length = $this->dataSize;
  61. return stream_get_line($this->fileHandle, $length, $seperator);
  62. }
  63. /**
  64. * Read a String in Unicode format until the next occurence of seperator or until length is reached
  65. *
  66. * @param Integer The maximum Length to read
  67. * @param Integer The string until we read
  68. * @param String The encoding the string is expected in
  69. * @return String The string in UTF-8 Format between current offset and offset + length or offset + offsetOfSeperator
  70. */
  71. public function readUnicodeString($length = null, $seperator = null, $encoding = 'UTF-16') {
  72. $string = $this->readString($length, $seperator);
  73. if (strlen($string) > 1) {
  74. // Determine the unicode type lo first or hi first
  75. if ($string[0] > $string[1]) {
  76. $encoding = 'UTF-16LE';
  77. }
  78. else {
  79. $encoding = 'UTF-16BE';
  80. }
  81. // Check if there are indicator bytes and remove them if yes
  82. if (($string[0] == "\xff" && $string[1] == "\xfe") || ($string[0] == "\xfe" && $string[1] == "\xff")) {
  83. $string = substr($string, 2);
  84. }
  85. }
  86. try {
  87. return iconv($encoding, 'UTF-8', $string);
  88. }
  89. catch (\Exception $e) {
  90. return iconv('UTF-16', 'UTF-8', $string);
  91. }
  92. finally {
  93. return $string;
  94. }
  95. }
  96. /**
  97. * Reads a synchronize save 32 bit (4 byte) Integer
  98. *
  99. * @return Integer The 4 bytes as parsed Integer
  100. */
  101. public function readSyncSaveInt32() {
  102. $int = fread($this->fileHandle, 4);
  103. $int = ord($int[3]) | ord($int[2]) << 8 | ord($int[1]) << 16 | ord($int[0]) << 24;
  104. return ($int & 0x0000007f) |
  105. ($int & 0x00007f00) >> 1 |
  106. ($int & 0x007f0000) >> 2 |
  107. ($int & 0x7f000000) >> 3;
  108. }
  109. /**
  110. * Reads a 32 bit (4 byte) Integer
  111. *
  112. * @return Integer The 4 bytes inversed as Integer
  113. */
  114. public function readInt32() {
  115. $int = fread($this->fileHandle, 4);
  116. return ord($int[3]) | ord($int[2]) << 8 | ord($int[1]) << 16 | ord($int[0]) << 24;
  117. }
  118. /**
  119. * Reads a 16 bit (2 byte) Integer
  120. *
  121. * @return Integer The 2 bytes inversed as Integer
  122. */
  123. public function readInt16() {
  124. $short = fread($this->fileHandle, 2);
  125. return ord($short[1]) | ord($short[0]) << 8;
  126. }
  127. /**
  128. * Reads a 8 bit (1 byte) Integer
  129. *
  130. * @return Integer The byte as Integer
  131. */
  132. public function readInt8() {
  133. return ord(fread($this->fileHandle, 1));
  134. }
  135. /**
  136. * Reads a number with dynamic length
  137. *
  138. * @param Integer the length of the number in bytes
  139. * @return Integer The parsed number
  140. */
  141. public function readNumber($bytes) {
  142. $int = fread($this->fileHandle, $bytes);
  143. $newInt = 0;
  144. $len = strlen($int) - 1;
  145. for ($i = $len; $i >= 0; $i--) {
  146. $newInt |= ord($int[$i]) << (($len - $i) * 8);
  147. }
  148. return $newInt;
  149. }
  150. /**
  151. * Reads a number with dynamic length by inversing the bytes
  152. *
  153. * @param Integer the length of the number in bytes
  154. * @return Integer The parsed number
  155. */
  156. public function readInversedNumber($bytes) {
  157. $int = fread($this->fileHandle, $bytes);
  158. $newInt = 0;
  159. for ($i = 0, $l = strlen($int); $i < $l; $i++) {
  160. $newInt |= ord($int[$i]) << ($i * 8);
  161. }
  162. return $newInt;
  163. }
  164. /**
  165. * Returns a amount of bits. Started bytes are skipped by other functions.
  166. *
  167. * @param Integer The amount of bits you wish to read
  168. * @return String The bits in binary string format
  169. */
  170. public function readBits($length) {
  171. // Check if we have enough bits in memory
  172. if ($this->bitPositon + $length > strlen($this->bitMemory)) {
  173. // Read bits we have left in memory
  174. $bits = substr($this->bitMemory, $this->bitPositon);
  175. // Calculate bits we need to reed excluding bits we have in memory
  176. $bytesToRead = ceil(($length - (strlen($this->bitMemory) - $this->bitPositon)) / 8);
  177. // Shorten bitMemory by strip readed bytes. Keep every time at least 7 bits to have 1 byte complete
  178. $this->bitMemory = substr($this->bitMemory, floor($this->bitPositon / 8) * 8);
  179. // Set new bitPositon
  180. $this->bitPositon = $this->bitPositon % 8;
  181. // Read new bytes into bitMemory
  182. $bytes = $this->read($bytesToRead);
  183. // Get bits from ascii chars.
  184. for ($i = 0, $l = strlen($bytes); $i < $l; $i++) {
  185. // Get unsigned int from unreadable binary
  186. $decimal = unpack('C', $bytes[$i])[1];
  187. // Make ascii char to binary string
  188. $bits = decbin($decimal);
  189. // Fill missing bits to have full bytes
  190. $this->bitMemory .= str_repeat('0', 8 - strlen($bits)) . $bits;
  191. }
  192. }
  193. // Get amount of bits from memory
  194. $bits = substr($this->bitMemory, $this->bitPositon, $length);
  195. $this->bitPositon += $length;
  196. return $bits;
  197. }
  198. /**
  199. * Reads a string until end of file or length is reached
  200. *
  201. * @param Integer The maximum length to read
  202. * @return String The string between current offset and end of file or offset + length
  203. */
  204. public function read($length) {
  205. return fread($this->fileHandle, $length);
  206. }
  207. /**
  208. * Search for a String in a stream and return its index
  209. *
  210. * @param String The string to search for
  211. * @param Boolean True if you want to stay witht the reader at the current position
  212. * @return Integer The index of the occurrence or -1 if not found
  213. */
  214. public function indexOf($string, $keepPosition = false) {
  215. // Remind the current position if we need to jump back
  216. if ($keepPosition) {
  217. $currentPosition = $this->pos();
  218. }
  219. // Search for the string
  220. stream_get_line($this->fileHandle, $this->dataSize, $seperator);
  221. // Get the position where we stopped
  222. $index = ftell($this->fileHandle);
  223. // Jump back to the position befor searching if needed
  224. if ($keepPosition) {
  225. fseek($this->fileHandle, $currentPosition);
  226. }
  227. // If we are at file end we didn't found anything
  228. return $index == $this->dataSize ? -1 : $index;
  229. }
  230. /**
  231. * Jump with the pointer to a wished position
  232. *
  233. * @param Integer the position to seek to
  234. */
  235. public function seek($position) {
  236. fseek($this->fileHandle, $position);
  237. }
  238. /**
  239. * Get the current stream pointer position
  240. *
  241. * @return Integer The current pointer position
  242. */
  243. public function pos() {
  244. return ftell($this->fileHandle);
  245. }
  246. /**
  247. * Check if we are at the end of the stream
  248. *
  249. * @return Boolean True if we are the the end
  250. */
  251. public function end() {
  252. return feof($this->fileHandle);
  253. }
  254. }
Alles anzeigen

Lets Wave ... Wir generieren die Waveform

Nun konvertieren wir die zuvor erstellte MP3 Datei in eine WAV Datei. Um den gesamten Prozess zu beschleunigen können wir hier ein paar Daten fallen lassen. Sprich wir nehmen nur einen Kanal und Resamplen auf 8 bit herunter. Würden wir das nicht machen wäre unsere WAV Datei ziemlich groß und das Erzeugen der Waveform würde, da wir uns bis zum Ende durch kämpfen, ewig dauern.

Nun validieren wir die Existenz der Datei und ob diese auch wirklich im WAV Format ist. Indem wir die ersten 4 bytes auf ihren Inhalt prüfen, welcher den Text RIFF darstellen sollte, stellen wir fest ob die Datei im WAV Format ist. Nun beginnen wir mit dem Analysieren der Datei.
Die ersten 22 bytes sind für uns nicht relevant. Um den Samples die richtige Größe zuordnen zu können, benötigen wir die Anzahl der Kanäle. Vom Header benötigen wir nun nur noch die Bitrate um die Waveform generieren zu können.

WAV Header verarbeiten

PHP-Quellcode

  1. // Convert mp3 to reduced wav file
  2. $command = 'lame %1$s -m m -S -f -b 16 --resample 8 %2$s.mp3 && lame %2$s.mp3 -S --decode %2$s.wav';
  3. @exec(sprintf($command, $convertedFileName, $tempName));
  4. $fileHandle = fopen($tempName . '.wav', 'r');
  5. // Check if we have a valid wav file
  6. if ($fileHandle && fread($fileHandle, 4) == 'RIFF') {
  7. $binary = new BinaryReader($fileHandle);
  8. // Skip 22 bytes of header
  9. $binary->seek(22);
  10. // Read 2 bytes inversed as integer
  11. $channels = $binary->readInversedNumber(2);
  12. // Skip again unnessesary bytes
  13. $binary->seek(34);
  14. // Read 2 bytes inversed as integer
  15. $bitRate = $binary->readInversedNumber(2);
  16. // Seek to end of header
  17. $binary->seek(44);
Alles anzeigen


Für das analysieren müssen wir die Länge eines Samples kennen.
Also berechnen wir die Bytes pro Frame und das Verhältnis um die Streuung zu erhöhen und die Leistung zu verbessern.
Sprich wir analysieren, obwohl wir die Datei bereits verlustbehaftet konvertiert haben, immer noch nicht jedes Sample.

PHP-Quellcode: Ermitteln der Frameanzahl und Framelänge

  1. // Calculate the amount of bytes per frame
  2. $bytesPerFrame = $bitRate / 8;
  3. // Analyze per channel every 16th byte
  4. $ratio = $channels == 2 ? 32 : 16;
  5. // Remove header size and divide by bytesPerFrame + channelRatio
  6. $frameAmount = ($binary->dataSize - 44) / ($ratio + $bytesPerFrame) + 1;

Lets paint it - Wir Zeichen die Waveform

Wir definieren nun unsere Variablen und Initialisieren das Bild worin wir Zeichnen wollen. Das Bild wird mit der Höhe von 255 Pixeln und der Breite eines Fünftels der Frameanzahl erstellt. Die Höhe wird auf 255 gesetzt da dies hier der maximale Wert eines Samples ist und wir so unsere Werte nicht in ein Verhältnis setzen müssen. Nun iterieren wir so lange bis wir entweder das Ende des Streams oder die Anzahl der Frames erreicht haben. Auch jetzt versuchen wir die Performance zu verbessern indem wir nur jedes 5te Frame analysieren. Jetzt merkt ihr vielleicht wieso unser Bild die Breite von einem Fünftel der Frameanzahl hat. Wenn ihr eine detailliertere Waveform haben wollt, müsst ihr diesen Wert runter setzen. Wenn ihr das macht, wird das Erzeugen der Waveform aber auch dementsprechend länger dauern.

Bei jeder Iteration ermitteln wir den Wert des aktuellen Samples um ihn später in die Waveform zeichnen zu können. Dafür bilden wir aus den Bytes des Samples einen Wert welchen wir dann auf ein byte reduzieren. Bei einem 16 Bit Sample (2 byte) bilden wir aus 2 bytes einen unsigned short int Wert und reduzieren diesen wieder auf ein byte. Nun haben wir einen Wert zwischen 0 und 255, also die Größe eines unsigned char oder auch byte gennant, welcher den Endpunkt einer vertikalen Linie in der Waveform darstellt. Dieser Wert wird nun umgekehrt und schon haben wir die zweite Y Koordinate für unsere Waveform. Als Beispiel haben wir bei einem ursprünglichen Wert von 249, die Y Koordinate 6 als Startpunkt und die Y Koordinate 249 als Endpunkt. Nun zeichnen wir diese Linie auf unser Bild, überspringen 5 Frames und fangen wieder an den Wert des Samples zu ermitteln.
Bei jeder Iteration wird die X Koordinate inkrementiert und irgendwann sind wir am Ende der Audiodatei.

Das Ergebnis

Nun haben wir von Anfang bis Ende viele Samples gelesen und unsere Waveform gezeichnet. Diese muss nur noch gespeichert werden und wir sind fertig. Das ganze kann man natürlich auch nach belieben anpassen. Z.B. kann man die Performance verbessern in dem man die $ratio Variable erhöht und weniger Samples (z.B. jedes 10te) analysiert. Im Gegenzug könnte man die Waveform detaillierter zeichnen wenn man mehr Samples analysiert. Man könnte die Darstellung der Waveform ändern in dem man nicht von einer zentrierten Linie, sondern von z.B. einem Grafen ausgeht. Wenn ihr hier im Dateisystem eine Audio Datei hoch ladet, werdet ihr das Ergebnis dieser Funktion sehen.

Pfad
/
Dateigröße
50,28 KiB
Mime Typ
audio/mpeg
Erstellt
Bitrate
128 Kbit/s
Dauer
0 Minuten 2 Sekunden
Sampelrate
44100 Hz
Titel
Test Titel
Album
StageTwo Album
Interpret
Sick^
Herstellung
1389298380
Genre
52


function generateWaveform

PHP-Quellcode

  1. /**
  2. * Generate a waveForm image from the current audio object
  3. *
  4. * @return String The webaccessible URL to the waveForm or false
  5. */
  6. protected function generateWaveform() {
  7. $waveFormName = $this->getLocation('waveform.png');
  8. $convertedFileName = $this->getLocation('converted.mp3');
  9. $tempName = FILE_DIR . 'files/wcf' . rand(0, 9999);
  10. // Check if we already generated the waveform
  11. if (is_file($waveFormName)) {
  12. return $this->getUrl('waveform.png');
  13. }
  14. // Check if we have something to generate the waveForm
  15. if (!is_file($convertedFileName)) {
  16. return false;
  17. }
  18. // Convert mp3 to reduced wav file
  19. $command = 'lame %1$s -m m -S -f -b 16 --resample 8 %2$s.mp3 && lame %2$s.mp3 -S --decode %2$s.wav';
  20. //die(var_dump(sprintf($command, $convertedFileName, $tempName)));
  21. @exec(sprintf($command, $convertedFileName, $tempName));
  22. $fileHandle = fopen($tempName . '.wav', 'r');
  23. // Check if we have a valid wav file
  24. if ($fileHandle && fread($fileHandle, 4) == 'RIFF') {
  25. $binary = new BinaryReader($fileHandle);
  26. // Skip 22 bytes of header
  27. $binary->seek(22);
  28. // Read 2 bytes inversed as integer
  29. $channels = $binary->readInversedNumber(2);
  30. // Skip again unnessesary bytes
  31. $binary->seek(34);
  32. // Read 2 bytes inversed as integer
  33. $bitRate = $binary->readInversedNumber(2);
  34. // Seek to end of header
  35. $binary->seek(44);
  36. // Calculate the amount of bytes per frame
  37. $bytesPerFrame = $bitRate / 8;
  38. $ratio = $channels == 2 ? 40 : 80;
  39. // Remove header size and divide by bytesPerFrame + channelRatio
  40. $frameAmount = ($binary->dataSize - 44) / ($ratio + $bytesPerFrame) + 1;
  41. // Initialize the waveForm image
  42. $adapter = ImageHandler::getInstance()->getAdapter();
  43. $adapter->createEmptyImage($frameAmount / 5, 255);
  44. $adapter->setColor(255, 255, 255);
  45. $adapter->drawRectangle(0, 0, $adapter->getWidth(), $adapter->getHeight());
  46. $adapter->setColor(0, 0, 0);
  47. $x = $frame = 0;
  48. while (!$binary->end() && $x < $frameAmount) {
  49. // Just handle every 5th frame
  50. if ($frame++ % 5 != 0) {
  51. $binary->seek($binary->pos() + $ratio + $bytesPerFrame);
  52. continue;
  53. }
  54. $bytes = $binary->readString($bytesPerFrame);
  55. // Get the value for a 8-bit sample
  56. $value = $bytes[0];
  57. if ($bytesPerFrame != 1) {
  58. // Check if the value is signed or not
  59. $temp = ord($bytes[1]) & 128 ? 0 : 128;
  60. // Fulfill the byte if it was signed
  61. $temp = (ord($bytes[1]) & 127) + $temp;
  62. // Get 16 bit value and ensure it not exceed the value 255
  63. $value = floor(($bytes[0] + ($temp * 256)) / 256);
  64. }
  65. // Invert value
  66. $y = 255 - $value;
  67. $x++;
  68. // Draw line on waveForm
  69. $adapter->drawRectangle($x, $y, $x, $value);
  70. }
  71. // Write image to file
  72. $adapter->setTransparentColor(0, 0, 0);
  73. $adapter->writeImage($adapter->getImage(), $waveFormName);
  74. @unlink($tempName . '.wav', $tempName . '.mp3');
  75. @fclose($fileHandle);
  76. return $this->getUrl('waveform.png');
  77. }
  78. @unlink($tempName . '.wav', $tempName . '.mp3');
  79. @fclose($fileHandle);
  80. return false;
  81. }
  82. /**
  83. * @see file\data\file\type\IFileType->getAttributes()
  84. */
  85. public function getAttributes() {
  86. // Detect if we have mplayer installed
  87. $commandPath = @exec('which "mplayer"');
  88. if ($commandPath) {
  89. // Prepare the convert command using mplayer
  90. $command = '%1$s -vo null -vc null -ao pcm:fast %2$s &&
  91. lame -m s audiodump.wav %3$s &&
  92. rm audiodump.wav';
  93. // Get the path to the new file
  94. $fileName = $this->getLocation('converted.mp3');
  95. // Execute the command
  96. @exec(sprintf($command, $commandPath, $this->getLocation(), $fileName));
  97. }
  98. return array();
  99. }
Alles anzeigen



Werbung
  • Es wurden noch keine Einträge an der Pinnwand verfasst.