I've tried to find answers online, but couldn't find anything that helped me...
I am trying to convert a PCM stream into a WAV file using PHP (7.2) and save it on the server.
Specifically, I am generating speech via Amazon Polly with the below code:
try {
$result = $client->synthesizeSpeech([
'Text' => 'Dies ist ein Test.',
'OutputFormat' => 'pcm',
'SampleRate' => '8000',
'VoiceId' => 'Hans'
]);
$resultData = $result->get('AudioStream')->getContents();
}
I need a WAV file for use with different code later on.
Many thanks for your help!
You just need to add a header and append the PCM data.
http://soundfile.sapp.org/doc/WaveFormat/
I couldn't find any PHP library for this, so I wrote a simple PHP program to do so:
<?php
$pcm = file_get_contents('polly.raw');
//$pcm = $result->get('AudioStream')->getContents();
//Output file
$fp = fopen('file.wav', 'wb');
$pcm_size = strlen($pcm);
$size = 36 + $pcm_size;
$chunk_size = 16;
$audio_format = 1;
$channels = 1; //mono
/**From the AWS Polly documentation: Valid values for pcm are "8000" and "16000" The default value is "16000".
* https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html#polly-SynthesizeSpeech-request-OutputFormat
**/
$sample_rate = 16000; //Hz
$bits_per_sample = 16;
$block_align = $channels * $bits_per_sample / 8;
$byte_rate = $sample_rate * $channels * $bits_per_sample / 8;
/**
* http://soundfile.sapp.org/doc/WaveFormat/
* https://github.com/jwhu1024/pcm-to-wav/blob/master/inc/wave.h
* https://jun711.github.io/aws/convert-aws-polly-synthesized-speech-from-pcm-to-wav-format/
**/
//RIFF chunk descriptor
fwrite($fp, 'RIFF');
fwrite($fp,pack('I', $size));
fwrite($fp, 'WAVE');
//fmt sub-chunk
fwrite($fp, 'fmt ');
fwrite($fp,pack('I', $chunk_size));
fwrite($fp,pack('v', $audio_format));
fwrite($fp,pack('v', $channels));
fwrite($fp,pack('I', $sample_rate));
fwrite($fp,pack('I', $byte_rate));
fwrite($fp,pack('v', $block_align));
fwrite($fp,pack('v', $bits_per_sample));
//data sub-chunk
fwrite($fp, 'data');
fwrite($fp,pack('i', $pcm_size));
fwrite($fp, $pcm);
fclose($fp);
You can use FFmpeg as well to achieve this, but my solution is purely written in PHP.
I hope I could help you!
PHP extension functions and/or classes that can be called from PHP scripts
https://www.php-cpp.com/documentation/functions
Simple Native wave file writer example:
https://www3.nd.edu/~dthain/courses/cse20211/fall2013/wavfile/
Related
I want to read the csv file content using php, google drive api v3
I got the fileid and file name but I am not sure how I can read the file content?
$service = new Drive($client);
$results = $service->files->listFiles();
$fileId="1I****************";
$file = $service->files->get($fileId);
The google drive api is a file storage api. It allows you to upload, download and manage storage of files.
It does not give you access to the contents of the file.
To do this you would need to download the file and open it locally.
Alternately since its a csv file you may want to consider converting it to a google sheet then you could use the google sheets api to access the data within the file programmatically.
Code for downloading a file from Google drive api would look something like this
Full sample can be found here large-file-download.php
// If this is a POST, download the file
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Determine the file's size and ID
$fileId = $files[0]->id;
$fileSize = intval($files[0]->size);
// Get the authorized Guzzle HTTP client
$http = $client->authorize();
// Open a file for writing
$fp = fopen('Big File (downloaded)', 'w');
// Download in 1 MB chunks
$chunkSizeBytes = 1 * 1024 * 1024;
$chunkStart = 0;
// Iterate over each chunk and write it to our file
while ($chunkStart < $fileSize) {
$chunkEnd = $chunkStart + $chunkSizeBytes;
$response = $http->request(
'GET',
sprintf('/drive/v3/files/%s', $fileId),
[
'query' => ['alt' => 'media'],
'headers' => [
'Range' => sprintf('bytes=%s-%s', $chunkStart, $chunkEnd)
]
]
);
$chunkStart = $chunkEnd + 1;
fwrite($fp, $response->getBody()->getContents());
}
// close the file pointer
fclose($fp);
For Windows Chrome (and probably many other browsers), this code works for serving an mp3 in an audio element:
/**
*
* #param string $filename
* #return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
*/
public function getMp3($filename) {
$fileContents = Storage::disk(\App\Helpers\CoachingCallsHelper::DISK)->get($filename);
$fileSize = Storage::disk(\App\Helpers\CoachingCallsHelper::DISK)->size($filename);
$shortlen = $fileSize - 1;
$headers = [
'Accept-Ranges' => 'bytes',
'Content-Range' => 'bytes 0-' . $shortlen . '/' . $fileSize,
'Content-Type' => "audio/mpeg"
];
Log::debug('$headers=' . json_encode($headers));
$response = response($fileContents, 200, $headers);
return $response;
}
But when I use an iPhone to browse to the same page, the mp3 file does not show the total duration, and when I play it, it says "Live broadcast".
I've tried to follow suggestions from various answers of this question (HTML5 <audio> Safari live broadcast vs not) and other articles I've read, but none seem to have an effect.
No matter how I change the headers, the mp3 seems to function as desired on Windows and does not work on iOS.
How can I debug what I'm doing wrong?
Here is HTML:
<audio controls preload="auto">
<source src="{{$coachingCall->getMp3Url()}}" type="audio/mpeg"/>
<p>Your browser doesnt support embedded HTML5 audio. Here is a link to the audio instead.</p>
</audio>
MP3 files don't have timestamps, and therefore no inherent length that can be known ahead of time. Chrome is just guessing, based on the bitrate at the beginning of the file and the byte size of the file. It doesn't really know.
Some players don't bother guessing.
Also, all browsers on iOS are Safari under the hood, thanks to some incredibly restrictive policies by Apple. Therefore, Chrome on iOS is really just a wrapper for a Safari web view.
Whoa, that was a very difficult problem to solve. (It took me days.)
And I learned that it wasn't just iOS that was having problems: Safari on Mac hadn't been working either.
Now I think everything works on every browser I've tested.
I'm really glad I found this example to follow.
Here is my answer:
/**
*
* #param string $disk
* #param string $filename
* #return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function getMediaFile($disk, $filename) {
$rangeHeader = request()->header('Range');
$fileContents = Storage::disk($disk)->get($filename);
$fullFilePath = Storage::disk($disk)->path($filename); //https://stackoverflow.com/a/49532280/470749
$headers = ['Content-Type' => Storage::disk($disk)->mimeType($fullFilePath)];
if ($rangeHeader) {
return self::getResponseStream($disk, $fullFilePath, $fileContents, $rangeHeader, $headers);
} else {
$httpStatusCode = 200;
return response($fileContents, $httpStatusCode, $headers);
}
}
/**
*
* #param string $disk
* #param string $fullFilePath
* #param string $fileContents
* #param string $rangeRequestHeader
* #param array $responseHeaders
* #return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function getResponseStream($disk, $fullFilePath, $fileContents, $rangeRequestHeader, $responseHeaders) {
$stream = Storage::disk($disk)->readStream($fullFilePath);
$fileSize = strlen($fileContents);
$fileSizeMinusOneByte = $fileSize - 1; //because it is 0-indexed. https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
list($param, $rangeHeader) = explode('=', $rangeRequestHeader);
if (strtolower(trim($param)) !== 'bytes') {
abort(400, "Invalid byte range request"); //Note, this is not how https://stackoverflow.com/a/29997555/470749 did it
}
list($from, $to) = explode('-', $rangeHeader);
if ($from === '') {
$end = $fileSizeMinusOneByte;
$start = $end - intval($from);
} elseif ($to === '') {
$start = intval($from);
$end = $fileSizeMinusOneByte;
} else {
$start = intval($from);
$end = intval($to);
}
$length = $end - $start + 1;
$httpStatusCode = 206;
$responseHeaders['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $fileSize);
$responseStream = response()->stream(function() use ($stream, $start, $length) {
fseek($stream, $start, SEEK_SET);
echo fread($stream, $length);
fclose($stream);
}, $httpStatusCode, $responseHeaders);
return $responseStream;
}
I can't comment since I just made my account, so... complementing RYAN's
Just found out that you can save some loading time removing the
$fileContents = Storage::disk($disk)->get($filename);
And replacing it with
$fileSize = Storage::disk($disk)->size($filename);
Passing the size directly to the getResponseStream function, instead of downloading the whole content into a variable and then measuring the length.
Thank you Ryan, saved me a lot of precious time with the stinky safari.
I am currently using microsoft-php-sdk and it has been pretty good. I have managed to upload small files from the server to OneDrive. But when I tried to upload a 38MB Powerpoint file it was failing. The Microsoft Graph API documentation suggests creating an upload session. I thought it would just be as easy as just updating the URI from /content to /createUploadSession but it was still failing.
$response = $graph->createRequest('POST', '/me/drive/root/children/'.basename($path).'/createUploadSession')
->setReturnType(Model\DriveItem::class)
->upload($path);
My code looks something like this. I have difficulty figuring out the PHP SDK documentation and there was no example for upload session. Has anyone used the PHP SDK for this scenario before?
i used the similar approach with https://github.com/microsoftgraph/msgraph-sdk-php/wiki and laravel framework. here is the code that worked for me finally.
public function test()
{
//initialization
$viewData = $this->loadViewData();
// Get the access token from the cache
$tokenCache = new TokenCache();
$accessToken = $tokenCache->getAccessToken();
// Create a Graph client
$graph = new Graph();
$graph->setAccessToken($accessToken);
//upload larger files
// 1. create upload session
$fileLocation = 'S:\ebooks\myLargeEbook.pdf';
$file = \File::get($fileLocation);
$reqBody=array(
"#microsoft.graph.conflictBehavior"=> "rename | fail | replace",
"description"=> "description",
"fileSystemInfo"=> ["#odata.type"=> "microsoft.graph.fileSystemInfo"] ,
"name"=> "ebook.pdf",
);
$uploadsession=$graph->createRequest("POST", "/drive/root:/test/ebook.pdf:/createUploadSession")
->attachBody($reqBody)
->setReturnType(Model\UploadSession::class)
->execute();
//2. upload bytes
$fragSize =320 * 1024;// 1024 * 1024 * 4;
$fileLocation = 'S:\ebooks\myLargeEbook.pdf';
// check if exists file if not make one
if (\File::exists($fileLocation)) {
$graph_url = $uploadsession->getUploadUrl();
$fileSize = filesize($fileLocation);
$numFragments = ceil($fileSize / $fragSize);
$bytesRemaining = $fileSize;
$i = 0;
while ($i < $numFragments) {
$chunkSize = $numBytes = $fragSize;
$start = $i * $fragSize;
$end = $i * $fragSize + $chunkSize - 1;
$offset = $i * $fragSize;
if ($bytesRemaining < $chunkSize) {
$chunkSize = $numBytes = $bytesRemaining;
$end = $fileSize - 1;
}
if ($stream = fopen($fileLocation, 'r')) {
// get contents using offset
$data = stream_get_contents($stream, $chunkSize, $offset);
fclose($stream);
}
$content_range = "bytes " . $start . "-" . $end . "/" . $fileSize;
$headers = array(
"Content-Length"=> $numBytes,
"Content-Range"=> $content_range
);
$uploadByte = $graph->createRequest("PUT", $graph_url)
->addHeaders($headers)
->attachBody($data)
->setReturnType(Model\UploadSession::class)
->setTimeout("1000")
->execute();
$bytesRemaining = $bytesRemaining - $chunkSize;
$i++;
}
}
dd($uploadByte);
}
}
I have developed a similar library for oneDrive based on microsoft graph rest api. This problem is also solved here : tarask/oneDrive
look at the documentation in the Upload large files section
I'm not familiar with PHP, but I am familiar with the upload API. Hopefully this will help.
The /content endpoint you were using before allows you to write binary contents to a file directly and returns a DriveItem as your code expects. The /createUploadSession method works differently. The Graph documentation for resumable upload details this, but I'll summarize here.
Instead of sending the binary contents in the CreateUploadSession request, you either send an empty body or you send a JSON payload with metadata like the filename or conflict-resolution behavior.
The response from CreateUploadSession is an UploadSession object, not a DriveItem. The object has an uploadUrl property that you use to send the binary data.
Upload your binary data over multiple requests using the HTTP Content-Range header to indicate which byte range you're uploading.
Once the server receives the last bytes of the file, the upload automatically finishes.
While this overview illustrates the basics, there are some more concepts you should code around. For example, if one of your byte ranges fails to upload, you need to ask the server which byte ranges it already has and where to resume. That and other things are detailed in the docs. https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/driveitem_createuploadsession
<?php
require __DIR__.'/path/to/vendor/autoload.php';
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model;
$graph = new Graph();
$graph->setAccessToken('YOUR_TOKEN_HERE');
/** #var Model\UploadSession $uploadSession */
$uploadSession = $graph->createRequest("POST", "/me/drive/items/root:/doc-test2.docx:/createUploadSession")
->addHeaders(["Content-Type" => "application/json"])
->attachBody([
"item" => [
"#microsoft.graph.conflictBehavior" => "rename",
"description" => 'File description here'
]
])
->setReturnType(Model\UploadSession::class)
->execute();
$file = __DIR__.'/path/to/large-file.avi';
$handle = fopen($file, 'r');
$fileSize = fileSize($file);
$fileNbByte = $fileSize - 1;
$chunkSize = 1024*1024*4;
$fgetsLength = $chunkSize + 1;
$start = 0;
while (!feof($handle)) {
$bytes = fread($handle, $fgetsLength);
$end = $chunkSize + $start;
if ($end > $fileNbByte) {
$end = $fileNbByte;
}
/* or use stream
$stream = \GuzzleHttp\Psr7\stream_for($bytes);
*/
$res = $graph->createRequest("PUT", $uploadSession->getUploadUrl())
->addHeaders([
'Content-Length' => ($end - 1) - $start,
'Content-Range' => "bytes " . $start . "-" . $end . "/" . $fileSize
])
->setReturnType(Model\UploadSession::class)
->attachBody($bytes)
->execute();
$start = $end + 1;
}
It's work for Me !
I am using flysystem with IRON IO queue and I am attempting to run a DB query that will be taking ~1.8 million records and while doing 5000 at at time. Here is the error message I am receiving with file sizes of 50+ MB:
PHP Fatal error: Allowed memory size of ########## bytes exhausted
Here are the steps I would like to take:
1) Get the data
2) Turn it into a CSV appropriate string (i.e. implode(',', $dataArray) . "\r\n")
3) Get the file from the server (in this case S3)
4) Read that files' contents and append this new string to it and re-write that content to the S3 file
Here is a brief run down of the code I have:
public function fire($job, $data)
{
// First set the headers and write the initial file to server
$this->filesystem->write($this->filename, implode(',', $this->setHeaders($parameters)) . "\r\n", [
'visibility' => 'public',
'mimetype' => 'text/csv',
]);
// Loop to get new sets of data
$offset = 0;
while ($this->exportResult) {
$this->exportResult = $this->getData($parameters, $offset);
if ($this->exportResult) {
$this->writeToFile($this->exportResult);
$offset += 5000;
}
}
}
private function writeToFile($contentToBeAdded = '')
{
$content = $this->filesystem->read($this->filename);
// Append new data
$content .= $contentToBeAdded;
$this->filesystem->update($this->filename, $content, [
'visibility' => 'public'
]);
}
I'm assuming this is NOT the most efficient? I am going off of these docs:
PHPLeague Flysystem
If anyone can point me in a more appropriate direction, that would be awesome!
Flysystem supports read/write/update stream
Please check latest API https://flysystem.thephpleague.com/api/
$stream = fopen('/path/to/database.backup', 'r+');
$filesystem->writeStream('backups/'.strftime('%G-%m-%d').'.backup', $stream);
// Using write you can also directly set the visibility
$filesystem->writeStream('backups/'.strftime('%G-%m-%d').'.backup', $stream, [
'visibility' => AdapterInterface::VISIBILITY_PRIVATE
]);
if (is_resource($stream)) {
fclose($stream);
}
// Or update a file with stream contents
$filesystem->updateStream('backups/'.strftime('%G-%m-%d').'.backup', $stream);
// Retrieve a read-stream
$stream = $filesystem->readStream('something/is/here.ext');
$contents = stream_get_contents($stream);
fclose($stream);
// Create or overwrite using a stream.
$putStream = tmpfile();
fwrite($putStream, $contents);
rewind($putStream);
$filesystem->putStream('somewhere/here.txt', $putStream);
if (is_resource($putStream)) {
fclose($putStream);
}
If you are working with S3, I would use the AWS SDK for PHP directly to solve this particular problem. Appending to a file is actually very easy using the SDK's S3 streamwrapper, and doesn't force you to read the entire file into memory.
$s3 = \Aws\S3\S3Client::factory($clientConfig);
$s3->registerStreamWrapper();
$appendHandle = fopen("s3://{$bucket}/{$key}", 'a');
fwrite($appendHandle, $data);
fclose($appendHandle);
I want to pass an image as a byte array from php to a .NET web serice.
The php client is as follows:
<?php
class Image{
public $ImgIn = array();
}
$file = file_get_contents('chathura.jpg');
$ImgIn = str_split($file);
foreach ($ImgIn as $key=>$val) { $ImgIn[$key] = ord($val); }
$client = new SoapClient('http://localhost:64226/Service1.asmx?wsdl');
$result = $client->PutImage(new Image());
echo $result->PutImageResult;
//print_r($ImgIn);
?>
Here is the web method in ASP.NET web service:
[WebMethod]
public string PutImage(byte[] ImgIn)
{
System.IO.MemoryStream ms =
new System.IO.MemoryStream(ImgIn);
System.Drawing.Bitmap b =
(System.Drawing.Bitmap)Image.FromStream(ms);
b.Save("imageTest", System.Drawing.Imaging.ImageFormat.Jpeg);
return "test";
}
When I run this the image content is correctly read to ImgIm array in php client. (In this instance the image had 16992 elements.) However when the array is passed to the web service method it contains only 5 elements (the first 5 elements of the image)
Can I know what is the reason for the loss of data ? How can I avoid it ?
Thanks
file_get_contents returns the file contents as string which is not useful for binary files such as images. Try this:
$handle = fopen("chathura.jpg", "r");
$contents = fread($handle, filesize("chathura.jpg"));
fclose($handle);
$client = new SoapClient('http://localhost:64226/Service1.asmx?wsdl');
$result = $client->PutImage($contents);
Guys, it seems that it is not going to be any use to try and pass data as a byte array as PHP anyway converts it to a string when sending. This conversion seems to introduce control characters to the string, making it only send a part of the byte array. I got this to work by sending a base64 encoded string and decoding inside the server.
My client side code:
<?php
class Image{
public $ImgIn = '';
}
//ini_set("memory_limit","20M");
$imageData = file_get_contents('chathura.jpg');
$encodedData = base64_encode($imageData);
$Img = new Image();
$Img->ImgIn = $encodedData;
$client = new SoapClient('http://localhost:64226/Service1.asmx?wsdl');
$result = $client->PutImage($Img);
echo($result->PutImageResult);
?>
ASP .NET web service code:
[WebMethod]
public string PutImage(String ImgIn)
{
byte[] ImgInBytes = Convert.FromBase64String(ImgIn);
System.IO.MemoryStream ms =
new System.IO.MemoryStream(ImgInBytes);
System.Drawing.Bitmap b =
(System.Drawing.Bitmap)Image.FromStream(ms);
b.Save("C:\\imageTest.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);
return "success";
}