Browse Source

Modified Bytebeat Generator

rebrush-2020
PHENOM 1 week ago
parent
commit
e5d498de89

+ 2
- 1
.gitignore View File

@@ -1 +1,2 @@
/vendor/
/vendor/
.vscode/

+ 36
- 0
web/modules/phenomic_net_bytebeat/assets/js/bytebeat-ui.js View File

@@ -0,0 +1,36 @@

$(() => {
var $ = jQuery
let prevVolume = $('#volume-slider').val();

function setVolume(val) {
$('#volume-slider').val(val)
$('#audio-player')[0].volume = ($('#volume-slider').val() / 100.00)
}

$('#edit-generate-btn').click((e) => {
console.log('Pressed!')
})

$('#volume-slider').on('input', () => {
prevVolume = $('#volume-slider').val()
$('#audio-player')[0].volume = ($('#volume-slider').val() / 100.00)
})

$('#play-btn').click((e) => {
e.preventDefault()
let player = $('#audio-player')[0]
player.paused ? player.play() : player.pause()
})

$('#mute-btn').click((e) => {
e.preventDefault()
let player = $('#audio-player')[0]
if (player.volume == 0) {
setVolume(prevVolume)
}
else {
setVolume(0)
}
})
})

+ 17
- 0
web/modules/phenomic_net_bytebeat/assets/js/bytebeat.js View File

@@ -0,0 +1,17 @@
function clamp(num, min, max) {
return num <= min ? min : num >= max ? max : num;
}

function generateSamples (oneLiner, frequency, duration, bitRate) {
let f;
let sampleBuffer = [];
eval("f = function (t) { return (" + oneLiner + "); }");
for (let t = 0; t < frequency * duration; t++) {
let sample = f(t);
sample = (sample & 0xff) * 256;
sample = clamp(sample, 0, 65535);

sampleBuffer.push(sample / 65535);
}
return sampleBuffer;
}

+ 2
- 0
web/modules/phenomic_net_bytebeat/config/install/phenomic_net_bytebeat.default.yml View File

@@ -0,0 +1,2 @@
phenomic_net_bytebeat:
test: "hello"

+ 7
- 0
web/modules/phenomic_net_bytebeat/phenomic_net_bytebeat.libraries.yml View File

@@ -0,0 +1,7 @@
bytebeat-ui:
version: 1.x
header: true
js:
assets/js/bytebeat-ui.js: {}
dependencies:
- core/jquery

+ 1
- 1
web/modules/phenomic_net_bytebeat/phenomic_net_bytebeat.module View File

@@ -29,7 +29,7 @@ function phenomic_net_bytebeat_help($route_name, RouteMatchInterface $route_matc
function phenomic_net_bytebeat_theme() {
return [
'phenomic_net_bytebeat' => [
'variables' => ['test_var' => NULL],
'variables' => ['bytebeats' => NULL],
],
];
}

+ 19
- 1
web/modules/phenomic_net_bytebeat/phenomic_net_bytebeat.routing.yml View File

@@ -1,8 +1,26 @@

phenomic_net_bytebeat.bytebeat_controller_show:
path: '/bytebeat'
path: '/bytebeat/old'
defaults:
_controller: '\Drupal\phenomic_net_bytebeat\Controller\BytebeatController::show'
_title: 'Bytebeat Generator'
requirements:
_permission: 'access content'

phenomic_net_bytebeat.bytebeat_controller_generate:
path: '/bytebeat/generate'
defaults:
_controller: '\Drupal\phenomic_net_bytebeat\Controller\BytebeatController::generate'
requirements:
_permission: 'access content'
methods: [POST,GET]


phenomic_net_bytebeat.bytebeat_form:
path: '/bytebeat'
defaults:
_form: '\Drupal\phenomic_net_bytebeat\Form\BytebeatForm'
_title: 'Bytebeat Generator'
requirements:
_access: 'TRUE'


+ 91
- 3
web/modules/phenomic_net_bytebeat/src/Controller/BytebeatController.php View File

@@ -3,23 +3,111 @@
namespace Drupal\phenomic_net_bytebeat\Controller;

use Drupal\Core\Controller\ControllerBase;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\node\Entity\Node;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Drupal\phenomic_net_bytebeat\Utility\WavGenerator;
use \V8Js;
use \V8JsException;
/**
* Class BytebeatController.
*/
class BytebeatController extends ControllerBase {

/**
* Show.
* {@inheritdoc}
*/
public static function create(ContainerInterface $container)
{
return new static();
}

/**
* Constructs a BytebeatController object
*/
public function __construct()
{}

/**
* Show Bytebeats.
*
* @return string
* Return Show string.
*/
public function show() {
$nids = Drupal::entityQuery('node')
->condition('status', 1)
->condition('type', 'bytebeat')
->execute();
$bytebeatsArray = [];
$bytebeats = Node::loadMultiple($nids);
foreach ($bytebeats as $bytebeat) {
$bytebeatArray = [
'id' => $bytebeat->id(),
'title' => $bytebeat->getTitle(),
'frequency' => $bytebeat->get('field_frequency')->getString(),
'duration' => $bytebeat->get('field_duration')->getString(),
'function' => $bytebeat->get('field_function')->getString(),
];
array_push($bytebeatsArray, $bytebeatArray);
}

return [
'#theme' => 'phenomic_net_bytebeat',
'#test_var' => $this->t('Test Value'),
'#bytebeats' => $bytebeatsArray,
];
}

/**
* Generate Bytebeat.
*
* @return string
* Return Show string.
*/
public function generate(Request $request) {
$v8 = new V8Js();
$requestMethod = $request->getMethod();
$function = 't * ((t>>12|t>>8)&63&t>>4)';
$frequency = '8000';
$duration = '30';

if ($requestMethod === "POST") {
$frequency = $request->request->get('frequency', $frequency);
$duration = $request->request->get('duration', $duration);
$function = $request->request->get('function', $function);
}
else if($requestMethod === "GET") {
$frequency = $request->query->get('frequency', $frequency);
$duration = $request->query->get('duration', $duration);
$function = $request->query->get('function', $function);
}

// bytebeat.js
$JS = trim(file_get_contents($this->moduleHandler()->getModule('phenomic_net_bytebeat')->getPath() .'/assets/js/bytebeat.js')) .
sprintf("\ngenerateSamples('%s',%s,%s);", $function, $frequency, $duration);

try {
$samples = $v8->executeString($JS, 'bytebeat.js');

$func = function ($sample) { return (int) (255 * $sample); };
$samples = array_map($func, $samples);
$wavGen = new WavGenerator();
$wavGen->setSampleRate($frequency);

$wavFileContent = $wavGen->create($samples);
$date = date("d_m_Y_H_i");
$response = new Response($wavFileContent);
$response->headers->set('Content-Type', 'audio/wav');
return $response;
} catch (V8JsException $error) {
$response = new Response('<div style="background-color: red;">V8Js: ' . $error->getMessage() . '</div>', Response::HTTP_INTERNAL_SERVER_ERROR);
}

return $response;
}

}

+ 254
- 0
web/modules/phenomic_net_bytebeat/src/Form/BytebeatForm.php View File

@@ -0,0 +1,254 @@
<?php

namespace Drupal\phenomic_net_bytebeat\Form;

use Drupal;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\Entity\Node;

/**
* Provides a bytebeat form.
*/
class BytebeatForm extends FormBase {

/**
* {@inheritdoc}
*/
public function getFormId() {
return 'bytebeat_form';
}

public function loadBytebeat(array &$form, FormStateInterface $form_state) {
$bytebeat = Node::load($form_state->getValue('bytebeat-example-select'));
$frequency = $bytebeat->get('field_frequency')->getString();
$duration = $bytebeat->get('field_duration')->getString();
$function = $bytebeat->get('field_function')->getString();
$form['fields']['frequency']['#value'] = $frequency;
$form['fields']['duration']['#value'] = $duration;
$form['fields']['function']['#value'] = $function;
$frequency = urlencode($frequency);
$duration = urlencode($duration);
$function = urlencode($function);

$form['fields']['audio-player']['audio-src']['#attributes']['src'] = "https://drupal.phenomic.net/bytebeat/generate?frequency=$frequency&duration=$duration&function=$function";
return $form['fields'];
}

public function generateBytebeat(array &$form, FormStateInterface $form_state) {

$frequency = urlencode($form_state->getValue('frequency'));
$duration = urlencode($form_state->getValue('duration'));
$function = urlencode($form_state->getValue('function'));

$form['fields']['audio-player']['audio-src']['#attributes']['src'] = "https://drupal.phenomic.net/bytebeat/generate?frequency=$frequency&duration=$duration&function=$function";
return $form['fields']['audio-player'];
}

/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$nids = Drupal::entityQuery('node')
->condition('status', 1)
->condition('type', 'bytebeat')
->execute();
$bytebeatTitleArray = [];
$bytebeats = Node::loadMultiple($nids);
/** @var Drupal\node\Entity\Node */
$defaultBytebeat = NULL;
foreach ($bytebeats as $bytebeat) {
if ($bytebeat->getTitle() === 'Default') {
$defaultBytebeat = $bytebeat;
}
$bytebeatTitleArray[$bytebeat->id()] = $bytebeat->getTitle();
}
$form = [
'#attributes' => [
'id' => 'bytebeat-form',
],
'#attached' => [
'library' => [
'phenomic_net_bytebeat/bytebeat-ui'
],
],
];

$form['headline'] = [
'#type' => 'html_tag',
'#tag' => 'h1',
'#attributes' => [
'class' => [
'display-4'
],
],
'#value' => 'Bytebeat Generator',
];

$form['sub-headline'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('This small tool can create music from a single JavaScript Expression which gets evaluated via server-side using the V8js PHP Extension.'),
];

$form['bytebeat-example-select'] = [
'#type' => 'select',
'#title' => $this->t('Bytebeat Examples'),
'#options' => $bytebeatTitleArray,
];

$form['load-btn'] = [
'#type' => 'button',
'#value' => $this->t('Load'),
'#ajax' => [
'callback' => [$this, 'loadBytebeat'],
'disable-refocus' => FALSE,
'wrapper' => 'fields',
'progress' => [
'type' => 'throbber',
'message' => $this->t('Loading Bytebeat'),
],
],
];

$form['save-as-btn'] = [
'#type' => 'button',
'#value' => $this->t('Save as...'),
'#attributes' => [
'disabled' => ''
],
];

$form['delete-btn'] = [
'#type' => 'button',
'#value' => $this->t('Delete'),
'#attributes' => [
'disabled' => ''
],
];

$form['fields'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'fields',
],
];

$form['fields']['frequency'] = [
'#type' => 'select',
'#title' => $this->t('Frequency'),
'#options' => [
2000 => '2000',
4000 => '4000',
6000 => '6000',
8000 => '8000',
11025 => '11025',
16000 => '16000',
22050 => '22050',
32000 => '32000',
37800 => '37800',
44100 => '44100',
48000 => '48000',
],
];

$form['fields']['duration'] = [
'#type' => 'number',
'#title' => $this->t('Duration'),
'#default_value' => $defaultBytebeat->get('field_duration')->getString(),
'#required' => TRUE,
'#min' => 1,
'#max' => 120,
'#step' => 1,
];

$form['fields']['function'] = [
'#type' => 'textfield',
'#title' => $this->t('JavaScript Function f(t)'),
'#default_value' => $defaultBytebeat->get('field_function')->getString(),
'#maxlength' => 99999,
'#required' => TRUE,
];

$form['fields']['audio-player'] = [
'#type' => 'html_tag',
'#tag' => 'audio',
'#attributes' => [
'controls' => '',
'id' => 'audio-player',
],
];

$frequency = urlencode($defaultBytebeat->get('field_frequency')->getString());
$duration = urlencode($defaultBytebeat->get('field_duration')->getString());
$function = urlencode($defaultBytebeat->get('field_function')->getString());
$form['fields']['audio-player']['audio-src'] = [
'#type' => 'html_tag',
'#tag' => 'source',
'#attributes' => [
'id' => 'audio-src',
'src' => "https://drupal.phenomic.net/bytebeat/generate?frequency=$frequency&duration=$duration&function=$function",
],
];

$form['volume-slider'] = [
'#type' => 'range',
'#title' => $this->t('Volume'),
'#attributes' => [
'id' => 'volume-slider'
],
];

$form['generate-btn'] = array(
'#type' => 'button',
'#value' => $this->t('Generate'),
'#ajax' => [
'callback' => [$this, 'generateBytebeat'],
'wrapper' => 'audio-player',
'disable-refocus' => FALSE,
'progress' => [
'type' => 'throbber',
'message' => $this->t('Generating Bytebeat'),
],
]
);

$form['play-btn'] = [
'#type' => 'button',
'#value' => $this->t('Play'),
'#attributes' => [
'id' => 'play-btn'
],
];

$form['mute-btn'] = [
'#type' => 'button',
'#value' => $this->t('Mute'),
'#attributes' => [
'id' => 'mute-btn'
],
];

return $form;
}

/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValues() as $key => $value) {
// @TODO: Validate fields.
}
parent::validateForm($form, $form_state);
}

/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Display result.
foreach ($form_state->getValues() as $key => $value) {
Drupal::messenger()->addMessage($key . ': ' . ($key === 'text_format'?$value['value']:$value));
}
}
}

+ 111
- 0
web/modules/phenomic_net_bytebeat/src/Utility/WavGenerator.php View File

@@ -0,0 +1,111 @@
<?php

namespace Drupal\phenomic_net_bytebeat\Utility;

class WavGenerator {

const FIELD_FORMAT_MAP = [
'sGroupID' => 'A4',
'dwFileLength'=> 'V',
'sRiffType' => 'A4',
'dwChunkSize' => 'V',
'wFormatTag' => 'v',
'wChannels' => 'v',
'dwSamplesPerSec' => 'V',
'dwAvgBytesPerSec' => 'V',
'wBlockAlign' => 'v',
'dwBitsPerSample' => 'v'
];

private $wavHeader = [
'sGroupID' => 'RIFF',
'dwFileLength' => 0,
'sRiffType' => 'WAVE'
];

const MAX_AMPLITUDE = 127;

private $channels = 1;
private $bitDepth = 8;
private $sampleRate = 8000;

private function getBlockAlign () {
return $this->channels * ($this->bitDepth/8);
}

private function getAverageBytesPerSecond () {
return $this->sampleRate * $this->getBlockAlign();
}

private function getFmtChunk () {
return [
'sGroupID' => 'fmt',
'dwChunkSize' => 16,
'wFormatTag' => 1,
'wChannels' => $this->channels,
'dwSamplesPerSec' => $this->sampleRate,
'dwAvgBytesPerSec' => $this->getAverageBytesPerSecond(),
'wBlockAlign' => $this->getBlockAlign(),
'dwBitsPerSample' => $this->bitDepth
];
}

public function getChannels () {
return $this->channels;
}

public function setChannels ($channels) {
$this->channels = $channels;
}

public function getBitDepth () {
return $this->bitDepth;
}

public function setBitDepth ($bitDepth) {
$this->bitDepth = $bitDepth;
}

public function getSampleRate () {
return $this->sampleRate;
}

public function setSampleRate ($sampleRate) {
$this->sampleRate = $sampleRate;
}

private function mergeFieldFormatMap ($array) {
$result = NULL;
foreach($array as $currKey => $currValue) {
if(!array_key_exists($currKey, self::FIELD_FORMAT_MAP))
{
die('Unrecognized field '.$currKey);
}

$currPackFlag = self::FIELD_FORMAT_MAP[$currKey];
$result .= pack($currPackFlag, $currValue);
}
return $result;
}

public function create ($samples) {


$dwFileLength = 0;
$result = '';
$result .= $this->mergeFieldFormatMap($this->wavHeader);
$result .= $this->mergeFieldFormatMap($this->getFmtChunk());

$dataChunk = [
'sGroupID' => 'data',
'dwChunkSize' => count($samples)
];

$result .= pack(self::FIELD_FORMAT_MAP['sGroupID'], $dataChunk['sGroupID']);
$dataChunkSizePosition = strlen($result);
$result .= pack(self::FIELD_FORMAT_MAP['dwChunkSize'], $dataChunk['dwChunkSize']);

$result .= pack('C*', ...$samples);
return $result;
}
}

+ 74
- 52
web/modules/phenomic_net_bytebeat/templates/phenomic-net-bytebeat.html.twig View File

@@ -1,62 +1,84 @@
<h1 class="display-4">Bytebeat Generator</h1>
<h4>This small tool can create music from a single JavaScript Expression using the Web Audio API.</h4>
<hr>
<div class="input-group mb-2 mt-2">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01"><i class="fas fa-file-audio"></i>&nbsp;Example</label>
<form>
<div class="input-group mb-2 mt-2">
<div class="input-group-prepend">
<label class="input-group-text" for="exampleSelect"><i class="fas fa-file-audio"></i>&nbsp;Example</label>
</div>
<select class="custom-select" id="exampleSelect">
{% for bytebeat in bytebeats %}
{% if bytebeat.title == "Default" %}
<option value="{{ bytebeat.id }}" selected>{{ bytebeat.title }}</option>
{% else %}
<option value="{{ bytebeat.id }}">{{ bytebeat.title }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<select class="custom-select" id="inputGroupSelect01">
<option selected>Choose...</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
</div>
<div class="mb-2">
<button type="button" class="btn btn-primary"><i class="fas fa-truck-loading"></i>&nbsp;Load</button>
<button type="button" class="btn btn-success"><i class="fas fa-save"></i>&nbsp;Save As...</button>
<button type="button" class="btn btn-danger"><i class="fas fa-trash"></i>&nbsp;Delete</button>
</div>
<hr>
<div class="input-group mb-2 mt-2">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect01"><i class="fas fa-wave-square"></i>&nbsp;Frequency</label>
<div class="mb-2">
<button type="button" class="btn btn-primary"><i class="fas fa-truck-loading"></i>&nbsp;Load</button>
<button type="button" class="btn btn-success"><i class="fas fa-save"></i>&nbsp;Save As...</button>
<button type="button" class="btn btn-danger"><i class="fas fa-trash"></i>&nbsp;Delete</button>
</div>
<hr>
<div class="input-group mb-2 mt-2">
<div class="input-group-prepend">
<label class="input-group-text" for="frequencySelect"><i class="fas fa-wave-square"></i>&nbsp;Frequency</label>
</div>
<select class="custom-select" id="frequencySelect">
<option value="2000">2000</option>
<option value="4000">4000</option>
<option value="6000">6000</option>
<option value="8000" selected>8000</option>
<option value="11025">11025</option>
<option value="16000">16000</option>
<option value="22050">22050</option>
<option value="32000">32000</option>
<option value="37800">37800</option>
<option value="44100">44100</option>
<option value="44100">44100</option>
<option value="48000">48000</option>
<option value="50000">50000</option>
<option value="50400">50400</option>
<option value="88200">88200</option>
<option value="96000">96000</option>
<option value="176400">176400</option>
<option value="192000">192000</option>
</select>
</div>
<select class="custom-select" id="inputGroupSelect01">
<option selected>Choose...</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
</div>

<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1"><i class="fas fa-stopwatch"></i>&nbsp;Duration</span>
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text" id="duration-addon"><i class="fas fa-stopwatch"></i>&nbsp;Duration</span>
</div>
<input type="text" class="form-control" placeholder="Duration" aria-label="Duration" aria-describedby="duration-addon" value="30">
</div>
<input type="text" class="form-control" placeholder="Duration" aria-label="Duration" aria-describedby="basic-addon1">
</div>
<hr>
<label for="customRange1"><i class="fas fa-volume-up"></i>&nbsp;Volume</label>
<input type="range" class="custom-range" id="customRange1" aria-describedby="basic-addon1">
<hr>
<hr>
<label for="volumeSlider"><i class="fas fa-volume-up"></i>&nbsp;Volume</label>
<input type="range" class="custom-range" id="volumeSlider">
<hr>

<label for="formcontrol1">JavaScript Function</label>
<div id="formcontrol1" class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">f(t)</span>
<label for="jsFunction">JavaScript Function</label>
<div id="jsFunction" class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text" id="function-addon">f(t)</span>
</div>
<input type="text" class="form-control" placeholder="JavaScript Function" aria-label="JavaScript Function" aria-describedby="function-addon" value="t * ((t>>12|t>>8)&63&t>>4)">
</div>
<input type="text" class="form-control" placeholder="JavaScript Function" aria-label="JavaScript Function" aria-describedby="basic-addon1">
</div>

<div>
<button type="button" class="btn btn-success"><i class="fas fa-save"></i>&nbsp;Generate</button>
<div class="btn-group btn-group-toggle" data-toggle="buttons" role="group" aria-label="Basic example">
<label class="btn btn-success">
<input type="checkbox" autocomplete="off"> <i class="fas fa-play-circle"></i>
</label>
<label class="btn btn-danger">
<input type="checkbox" autocomplete="off"> <i class="fas fa-volume-mute"></i>
</label>
</div>
</div>
<div>
<button type="button" class="btn btn-success">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
&nbsp;Generate
</button>
<div class="btn-group btn-group-toggle" data-toggle="buttons" role="group" aria-label="Play Controls" disabled>
<label class="btn btn-success">
<input type="checkbox" autocomplete="off"> <i class="fas fa-play-circle"></i>
</label>
<label class="btn btn-danger">
<input type="checkbox" autocomplete="off"> <i class="fas fa-volume-mute"></i>
</label>
</div>
</div>
</form>

+ 22
- 22
web/themes/phenomic_net/src/components/spinner/_spinner.scss View File

@@ -1,22 +1,22 @@
@mixin spinnerFullscreen() {
position: fixed;
z-index: 5000;
top: 50%;
left: 50%;
}
.spinner-border {
@include spinnerFullscreen();
}
.spinner-border-sm {
@include spinnerFullscreen();
}
.spinner-grow {
@include spinnerFullscreen();
}
.spinner-grow-sm {
@include spinnerFullscreen();
}
@mixin spinnerFullscreen() {
position: fixed;
z-index: 5000;
top: 50%;
left: 50%;
}
.spinner-border {
// @include spinnerFullscreen();
}
.spinner-border-sm {
// @include spinnerFullscreen();
}
.spinner-grow {
// @include spinnerFullscreen();
}
.spinner-grow-sm {
// @include spinnerFullscreen();
}

+ 1
- 1
web/themes/phenomic_net/src/js/phenomic_net.script.js View File

@@ -25,4 +25,4 @@ import 'bootstrap';
}
});
});
})(jQuery, Drupal);
})(jQuery, Drupal);

+ 2
- 2
web/themes/phenomic_net/webpack.mix.js View File

@@ -7,7 +7,7 @@
| for your application. See https://github.com/JeffreyWay/laravel-mix.
|
*/
const proxy = 'http://phenomic.lvh.me';
const proxy = 'https://drupal.phenomic.net';
const mix = require('laravel-mix');

/*
@@ -31,7 +31,7 @@ mix.browserSync({
open: false,
proxy: proxy,
files: ['assets/js/**/*.js', 'assets/css/**/*.css'],
stream: true,
stream: false,
});

/*

Loading…
Cancel
Save