I have a bin and a cue file (CD image) and I am making a website to be able to listen to my music. I managed to split the tracks and make a (44 bytes) wav header so my browser can read the audio but the wav files are too big to be read on an unstable connection (it's 1411kbps where an mp3 would be 320kbps or lower!).
At first, is there a way to convert the audio to mp3 without making a temporary file in php? Maybe ffmpeg or sox could help?
Secondly, I would like to be able to satisfy partial content requests (so I can skip without loading the whole file). I have already succeeded it for the wav files and I would need to know a few things to do it with mp3:
I tried searching online but I did not find what I was hoping for. I hope you can help me.
For my first question, to convert wav to mp3 in php without creating a temporary file, I used:
passthru(dd if="$bin_path" bs=1 skip=$skip_bytes count=$count_bytes | ffmpeg -ss 0 -f s16le -ar 44100 -ac 2 -i pipe:0 -codec:a libmp3lame -b:a 320k -ac 2 -joint_stereo 0 -compression_level 0 -write_id3v1 0 -id3v2_version none -f mp3 -);
where $bin_path
is the path to your binary file (you need to escape the "
if there is any in the path).
$skip_bytes
is 44100 * 4 * song_start
(s)
$count_bytes
is 44100 * 4 * song_duration
(s)
Here is a breakdown of the FFmpeg command:
For the input:
-ss 0
start from 0.0s even if there is no audio (silence)
-f s16le -ar 44100 -ac 2
tell that the input is pcm_s16le (-f s16le
) stereo (-ac 2
) with a sampling rate of 44100 Hz (-ar 44100
)
-i pipe:0
the input is comming from the pipe
For the output:
-codec:a libmp3lame
mp3 encoding
-b:a 320k
bitrate (320kbps)
-ac 2 -joint_stereo 0
2-channels stereo
-compression_level 0
use constant bit rate
-id3v2_version none
skip the ID3 header
-
output to sdin
For the second part of your question,
mp3 are nothing like wav files and predicting their size is nearly impossible. However, I managed to do it anyway by making the song lasting a multiple of 1.28 seconds (mp3 (version 1, layer III) have an internal structure that repeats every 1.28 seconds).
Here is how I've done it:
$bin_path = "/path/to/binary.bin";
$song_start = "182.12"; // song starts after 3m0.12s
$song_duration = "176.14"; // and lasts 2m56.02s
$skip_bytes = 44100 * 4 * $song_start;
$count_bytes = 44100 * 4 * $song_duration;
$chunk_size_mp3 = 51200; // Size (in bytes) for 1.28s of 320kbps MP3
$chunk_size_wav = 225792; // Size (in bytes) for 1.28s of WAV
$file_size = (floor($song_duration/1.28)+1) * $chunk_size_mp3;
$need_exit = false;
//--------------------HTTP HEADERS--------------------
ob_get_clean();
header('Content-Type: audio/mpeg');
header('Cache-Control: no-cache', true);
header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); // Expire now!
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', @filemtime($file_path)) . ' GMT' );
$start_index = 0;
$end_index = $file_size - 1;
header('Accept-Ranges: bytes');
if(isset($_SERVER['HTTP_RANGE'])){
$c_start = $start_index;
$c_end = $end_index;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if(strpos($range, ',') !== false){
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start_index-$end_index/$file_size");
$need_exit = true;
}
if(!$need_exit){
if($range == '-'){
$c_start = $file_size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $c_end;
}
$c_end = ($c_end > $end_index) ? $end_index : $c_end;
if($c_start > $c_end || $c_start > $file_size - 1 || $c_end >= $file_size){
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start_index-$end_index/$file_size");
$need_exit = true;
}
if(!$need_exit){
if($c_start <= $chunk_size_mp3){
$start_index = 0;
}else{
$start_index = $c_start;
}
if($c_end <= $chunk_size_mp3){
$end_index = $chunk_size_mp3 - 1;
}else{
$end_index = $c_end;
}
$length = $end_index - $start_index + 1;
header('HTTP/1.1 206 Partial Content');
header('Content-Length: ' . $length);
header("Content-Range: bytes $start_index-$end_index/".$file_size);
}
}
}else{
header('Content-Length: ' . $file_size);
}
//--------------------HTTP HEADERS--------------------
//-----------------------STREAM-----------------------
if(!$need_exit){
$i = $start_index;
if($start_index === 0){
$mp3_data = shell_exec("php /path/to/dd.php \"$bin_path\" $skip_bytes ".($chunk_size_wav*2)." 0 | ffmpeg -ss 0 -f s16le -ar 44100 -ac 2 -i pipe:0 -codec:a libmp3lame -b:a 320k -ac 2 -joint_stereo 0 -compression_level 0 -write_id3v1 0 -id3v2_version none -f mp3 -");
for($j = 0; $j < $chunk_size_mp3; $j++){
echo $mp3_data[(int)$j];
}
$i += $chunk_size_mp3;
}
while($i <= $end_index){
$start_chunk_number = floor($i/$chunk_size_mp3)-1;
$end_chunk_number = $start_chunk_number + 2;
$start_wav_byte = $skip_bytes + $start_chunk_number * $chunk_size_wav;
$end_wav_byte = $start_wav_byte + 3 * $chunk_size_wav - 1;
if($end_wav_byte > $skip_bytes + $count_bytes - 1){
$empty_bytes = $end_wav_byte - ($skip_bytes + $count_bytes - 1);
$end_wav_byte = $skip_bytes + $count_bytes - 1;
}else{
$empty_bytes = 0;
}
$mp3_data = shell_exec("php /path/to/dd.php \"$bin_path\" $start_wav_byte ".($end_wav_byte - $start_wav_byte + 1)." $empty_bytes | ffmpeg -ss 0 -f s16le -ar 44100 -ac 2 -i pipe:0 -codec:a libmp3lame -b:a 320k -ac 2 -joint_stereo 0 -compression_level 0 -write_id3v1 0 -id3v2_version none -f mp3 -");
$ign_mp3_bytes = $i - $start_chunk_number * $chunk_size_mp3;
$j = $ign_mp3_bytes;
while($i <= $end_index && $j < 2*$chunk_size_mp3){
echo $mp3_data[(int)$j];
$i++;
$j++;
}
}
}
//-----------------------STREAM-----------------------
?>
dd.php:
<?php
if(isset($argv[3])){
$bin_file = $argv[1];
$skip_bytes = (int)$argv[2];
$count_bytes = (int)$argv[3];
if(!isset($argv[4])){
$empty_bytes = 0;
}else{
$empty_bytes = (int)$argv[4];
}
$file_size = $count_bytes;
$file_stream = fopen($bin_file, 'rb');
fseek($file_stream, $skip_bytes);
$i = 0;
$buffer = 102400;
set_time_limit(0);
while(!feof($file_stream) && $i < $count_bytes){
$bytesToRead = $buffer;
if(($i+$bytesToRead) > $count_bytes){
$bytesToRead = $count_bytes - $i;
}
echo fread($file_stream, $bytesToRead);
flush();
$i += $bytesToRead;
}
if($empty_bytes > 0){
for($k = 0; $k < $empty_bytes; $k++){
echo "\x00";
}
}
}
?>
A few notes to make here: