Browse Source

Added Save function to ByteBeat Generator

master
PHENOM 2 years ago
parent
commit
e17c7317c2
9 changed files with 300 additions and 149 deletions
  1. 2
    2
      layouts/phenomic.vue
  2. 22
    2
      nuxt.config.js
  3. 5
    2
      package.json
  4. 170
    94
      pages/bytebeat.vue
  5. 6
    0
      pages/index.vue
  6. 28
    33
      pages/projects.vue
  7. 9
    0
      plugins/localStorage.js
  8. 0
    12
      plugins/vue-chartjs.js
  9. 58
    4
      store/index.js

layouts/default.vue → layouts/phenomic.vue View File

@@ -162,13 +162,13 @@
drawer: false,
navMainItems: [
{ icon: 'home', title: 'Home', to: '/' },
{ icon: 'mdi-book', title: 'Featured Projects', to: '/projects' }
{ icon: 'mdi-book', title: 'Featured Projects', to: 'projects' }
],
navServiceItems: [
{ icon: 'mdi-git', title: 'Git Server', to: gitInfo.url }
],
navProjectsItems: [
{ icon: 'mdi-music', title: 'Bytebeat Generator', to: '/bytebeat' }
{ icon: 'mdi-music', title: 'Bytebeat Generator', to: 'bytebeat' }
],
footerItems: [
{ icon: 'mdi-git', title: gitInfo.user, to: gitInfo.url, color: 'grey' },

+ 22
- 2
nuxt.config.js View File

@@ -1,5 +1,6 @@
const nodeExternals = require('webpack-node-externals')
const resolve = (dir) => require('path').join(__dirname, dir)
const webpack = require('webpack')

module.exports = {
/*
@@ -22,10 +23,24 @@ module.exports = {
script: [
]
},
plugins: ['~/plugins/vuetify.js', { src: '~plugins/vue-chartjs.js', ssr: false }],
plugins: ['~/plugins/vuetify.js', { src: '~/plugins/localStorage.js', ssr: false }],
css: [
'~/assets/style/app.styl'
],
modules: [
// Simple usage
'nuxt-trailingslash-module',

// With options
['nuxt-trailingslash-module', {
/* module options */
methods: [
'GET',
'HEAD'
]
}]
],

/*
** Customize the progress bar color
*/
@@ -71,6 +86,11 @@ module.exports = {
})
]
}
}
},
plugins: [
new webpack.ProvidePlugin({
'_': 'lodash'
})
]
}
}

+ 5
- 2
package.json View File

@@ -15,10 +15,13 @@
"babel-plugin-transform-remove-strict-mode": "0.0.2",
"chart.js": "^2.7.2",
"chartjs-plugin-zoom": "^0.6.5",
"lodash": "^4.17.11",
"nuxt": "^1.4.1",
"nuxt-trailingslash-module": "^1.1.0",
"vue-chartjs": "^3.4.0",
"vue-clipboard2": "^0.1.0",
"vuetify": "^1.0.0"
"vue-clipboard2": "^0.1.1",
"vuetify": "^1.0.0",
"vuex-persistedstate": "^2.5.4"
},
"devDependencies": {
"@babel/plugin-transform-strict-mode": "^7.0.0",

+ 170
- 94
pages/bytebeat.vue View File

@@ -7,13 +7,14 @@
<v-select
v-model="exampleSelected"
prepend-icon="mdi-file-music"
:items="examples"
item-value="payload"
:items="bytebeatSaves"
return-object
item-text="name"
>
</v-select>
<v-btn @click="load()">Load</v-btn>
<v-btn @click="saveAs()">Save As...</v-btn>
<v-btn @click="saveDialog = true" :disabled="!changed">Save As...</v-btn>
<v-btn color="red" :disabled="exampleSelected.isExample" @click="deleteSave()">Delete</v-btn>
<v-divider class="my-3"></v-divider>
<v-form>
<v-select
@@ -38,20 +39,28 @@
<v-slider
v-model="volume"
v-on:input="setVolume"
:disabled="!isContextAvailable()"
label="Volume"
append-icon="volume_up"
prepend-icon="volume_down"
></v-slider>
<v-btn-toggle v-model="playBtn" multiple>
<v-btn flat @click="play()">
<v-icon>mdi-play-pause</v-icon>
<p color="red" v-if="changed">Changes were made. Press on Generate to recompile the expression.</p>
<v-layout row wrap align-center>
<v-btn color="green" @click="regenerate()">
<v-icon>mdi-sync</v-icon>
<span>Generate</span>
</v-btn>
</v-btn-toggle>
<v-btn-toggle v-model="muteBtn" multiple>
<v-btn flat @click="mute()">
<v-icon>mdi-volume-off</v-icon>
</v-btn>
</v-btn-toggle>
<v-btn-toggle v-model="playBtn" multiple>
<v-btn flat @click="playPause()" :disabled="!isGenerated()">
<v-icon>mdi-play-pause</v-icon>
</v-btn>
</v-btn-toggle>
<v-btn-toggle v-model="muteBtn" multiple>
<v-btn flat @click="mute()" :disabled="!isContextAvailable()">
<v-icon>mdi-volume-off</v-icon>
</v-btn>
</v-btn-toggle>
</v-layout>
</v-form>
<v-dialog
v-model="saveDialog"
@@ -59,15 +68,18 @@
>
<v-card>
<v-card-title class="headline">Saving ByteBeat...</v-card-title>
<v-form>
<v-text-field
></v-text-field>
</v-form>

<v-layout align-content-space-between>
<v-form>
<v-text-field
v-model="newName"
placeholder="Enter a Name"
label="Name"
></v-text-field>
</v-form>
</v-layout>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="green darken-1"
flat="flat"
@click="saveDialog = false"
>
@@ -76,7 +88,7 @@
<v-btn
color="green darken-1"
flat="flat"
@click="saveDialog = false"
@click="onSave()"
>
Save
</v-btn>
@@ -87,45 +99,22 @@
</v-layout>
</template>
<script>
import { mapGetters, mapState, mapMutations } from 'vuex'

/* eslint no-eval: 0 */
/* eslint no-debugger: 0 */
export default {
layout: 'phenomic',
data: () => ({
examples: [
{
name: 'Default',
payload: {
frequency: '8000',
duration: '30',
ft: 't * ((t>>12|t>>8)&63&t>>4)'
}
},
{
name: 'Chaos Theory',
payload: {
frequency: '8000',
duration: '30',
ft: 'w=t>>9,k=32,m=2048,a=1-t/m%1,d=(14*t*t^t)%m*a,y=[3,3,4.7,2][p=w/k&3]*t/4,h="IQNNNN!!]]!Q!IW]WQNN??!!W]WQNNN?".charCodeAt(w/2&15|p/3<<4)/33*t-t,s=y*.98%80+y%80+(w>>7&&a*((5*t%m*a&128)*(0x53232323>>w/4&1)+(d&127)*(0xa444c444>>w/4&1)*1.5+(d*w&1)+(h%k+h*1.99%k+h*.49%k+h*.97%k-64)*(4-a-a))),s*s>>14?127:s'
}
},
{
name: 'Dante\'s Inferno',
payload: {
frequency: '8000',
duration: '30',
ft: '((t*(t>>12)&(201*t/100)&(199*t/100))&(t*(t>>14)&(t*301/100)&(t*399/100)))+((t*(t>>16)&(t*202/100)&(t*198/100))-(t*(t>>17)&(t*302/100)&(t*298/100)))'
}
}
],
exampleSelected: {
id: 0,
isExample: true,
name: 'Default',
payload: {
frequency: '8000',
duration: '30',
ft: 't * ((t>>12|t>>8)&63&t>>4)'
}
frequency: '8000',
duration: '30',
ft: 't * ((t>>12|t>>8)&63&t>>4)'
},
liveByteBeat: {
id: 0,
name: 'Default',
frequency: '8000',
duration: '30',
@@ -134,18 +123,98 @@ export default {
playBtn: [],
muteBtn: [],
frequencies: ['8000', '11025', '16000', '22050', '32000', '37800', '44056', '44100', '47250', '48000', '50000', '50400', '88200', '96000', '176400', '192000'],
audioCtx: '',
source: '',
gainNode: '',
audioCtx: null,
source: null,
gainNode: null,
volume: 50,
sampleBuffer: [],
saveDialog: false
sampleBuffer: null,
saveDialog: false,
changed: false,
isLoading: false,
newName: ''
}),
watch: {
liveByteBeat: {
handler: function (newVal, oldVal) {
if (!this.isLoading) {
this.changed = true
} else {
this.changed = false
this.isLoading = false
}
},
deep: true
}
},
errorCaptured (err, vm, info) {
console.log(err)
},
computed: {
...mapState({
bytebeatSaves: 'bytebeatSaves'
}),
...mapGetters({
getNewId: 'getNewId'
})
},
methods: {
loadExample () {

onChange (newVal) {
console.log('Change: ' + newVal)
this.changed = true
this.liveByteBeat.isExample = false
},
isGenerated () {
if (!(this.source === undefined || this.source === null)) {
return !(this.source.buffer === undefined || this.source.buffer === null)
}
return false
},
isContextAvailable () {
return !(this.audioCtx === undefined || this.audioCtx === null)
},
generateMusicArray () {
isExample () {
return this.liveByteBeat.isExample
},
isPlaying () {
if (this.audioCtx.state === 'running') {
return true
} else if (this.audioCtx.state === 'suspended') {
return false
}
},
regenerate () {
// Reset change indicator
this.changed = false

// Reset Playbutton
this.playBtn = []

if (process.browser) {
// Close Audio Context if already opened
if (this.isContextAvailable()) {
this.audioCtx.close()
}

// Create new Audio Context
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
this.audioCtx.suspend() // Stop instant playback
this.gainNode = this.audioCtx.createGain()
this.gainNode.connect(this.audioCtx.destination)
} else {
console.error("This is not a browser! Can't continue!")
return
}

// Create a new Audio Source
this.source = this.audioCtx.createBufferSource()

// Determines the behaviour when the Audio has finished playing
this.source.onended = (event) => {
this.audioCtx.suspend()
this.playBtn = []
}

// Here is the main clear on compare match linear function of the bytebeat algorithm
this.sampleBuffer = []
let f
eval('f = function (t) { return ' + this.liveByteBeat.ft + '}')
@@ -156,20 +225,8 @@ export default {
if (sample > 65535) sample = 65535
this.sampleBuffer.push(sample / 65535)
}
},
generate () {
this.audioCtx.suspend()
if (this.playBtn.length === 1) {
this.pause()
this.playBtn = []
}
this.source = this.audioCtx.createBufferSource()
this.source.onended = (event) => {
this.audioCtx.suspend()
this.playBtn = []
this.generate()
}
this.generateMusicArray()

// Fill Audio Buffer with Data
let audioBuffer = this.audioCtx.createBuffer(2, this.liveByteBeat.frequency * this.liveByteBeat.duration, this.liveByteBeat.frequency)
for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
let channelBuffer = audioBuffer.getChannelData(channel)
@@ -177,21 +234,24 @@ export default {
channelBuffer[i] = this.sampleBuffer[i]
}
}

// Apply Audio Buffer to source
this.source.buffer = audioBuffer

// Apply Gain connection for Volume control
this.source.connect(this.gainNode)

// Start the source
this.source.start(0)
},
play () {
if (!this.source.buffer) {
this.generate()
}
this.pause()
},
pause () {
if (this.audioCtx.state === 'running') {
this.audioCtx.suspend()
} else if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume()
playPause () {
if (this.isGenerated()) {
if (this.isPlaying()) {
this.audioCtx.suspend()
this.playBtn = []
} else {
this.audioCtx.resume()
}
}
},
mute () {
@@ -205,23 +265,39 @@ export default {
this.muteBtn = []
this.gainNode.gain.value = something / 100.0
},
saveAs () {
this.saveDialog = true
onSave () {
this.saveDialog = false
this.liveByteBeat.name = this.newName
this.liveByteBeat.id = this.getNewId
this.saveBeat(this.liveByteBeat)
this.exampleSelected = this.liveByteBeat
this.isCurrentExample = false
},
load () {
this.isLoading = true
this.liveByteBeat.id = this.exampleSelected.id
this.liveByteBeat.duration = this.exampleSelected.duration
this.liveByteBeat.frequency = this.exampleSelected.frequency
this.liveByteBeat.ft = this.exampleSelected.ft
}

this.liveByteBeat.name = this.exampleSelected.name
this.isCurrentExample = this.exampleSelected.isExample
},
deleteSave () {
if (!this.isCurrentExample) {
this.deleteBeat(this.exampleSelected.id)
}
},
...mapMutations({
saveBeat: 'saveByteBeat',
deleteBeat: 'deleteByteBeatById'
})
},
mounted () {
if (process.browser) {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
this.audioCtx.suspend()
this.gainNode = this.audioCtx.createGain()
this.gainNode.connect(this.audioCtx.destination)
beforeRouteLeave (to, from, next) {
// Close Audio Context if already opened
if (this.isContextAvailable()) {
this.audioCtx.close()
}
next()
}
}
</script>

+ 6
- 0
pages/index.vue View File

@@ -12,3 +12,9 @@
</v-responsive>
</v-layout>
</template>

<script>
export default {
layout: 'phenomic'
}
</script>

+ 28
- 33
pages/projects.vue View File

@@ -4,22 +4,22 @@
<h3 class="display-3">Featured Projects</h3>
<v-container
fluid grid-list-md
:key="i"
v-for="(project, i) in getImageProjects"
:key="i-50"
v-for="(imageProject, i) in imageProjects"
>
<v-layout
row
wrap
>
<v-flex d-flex xs12 sm6 md4>
<v-card :color="project.color" dark>
<v-card :color="imageProject.color" dark>
<v-card-title
primary
class="title"
>{{ project.name }}</v-card-title>
<v-card-text>{{ project.description }}</v-card-text>
>{{ imageProject.name }}</v-card-title>
<v-card-text>{{ imageProject.description }}</v-card-text>
<v-btn
:href="project.url"
:href="imageProject.url"
>
Go to Project
</v-btn>
@@ -30,7 +30,7 @@
<v-card>
<v-carousel>
<v-carousel-item
v-for="(imageUrl,i) in getCompleteImageUrls(project)"
v-for="(imageUrl, i) in getCompleteImageUrls(imageProject)"
:key="i"
:src="imageUrl"
></v-carousel-item>
@@ -43,32 +43,33 @@
<v-container
fluid grid-list-md
:key="i"
v-for="(project, i) in getNoImageProjects"
>
<v-layout
column
wrap
v-for="(textProject, i) in textProjects"
>
<v-card :color="project.color" dark>
<v-card-title
primary
class="title"
>
{{ project.name }}
</v-card-title>
<v-card-text>{{ project.description }}</v-card-text>
<v-btn :href="project.url">Go to Project</v-btn>
</v-card>
</v-layout>
<v-layout
column
wrap
>
<v-card :color="textProject.color" dark>
<v-card-title
primary
class="title"
>
{{ textProject.name }}
</v-card-title>
<v-card-text>{{ textProject.description }}</v-card-text>
<v-btn :href="textProject.url">Go to Project</v-btn>
</v-card>
</v-layout>
</v-container>
</v-layout>
</template>

<script>
export default {
layout: 'phenomic',
data: () => ({
imageBaseUrl: 'images/projects',
projects: [
imageProjects: [
{
name: 'Propeller Parallax P8X32A DemoBoard',
color: 'red darken-3',
@@ -100,7 +101,9 @@ export default {
url: 'https://git.phenomic.net/PHENOM/GameBoyEmulator',
imageBaseUrl: 'gameboyemulator',
imageUrls: ['1.png']
},
}
],
textProjects: [
{
name: 'PHENOMIC.net',
color: 'blue-grey darken-3',
@@ -127,14 +130,6 @@ export default {
}
]
}),
computed: {
getImageProjects () {
return this.projects.filter(project => project.imageUrls !== undefined)
},
getNoImageProjects () {
return this.projects.filter(project => project.imageUrls === undefined)
}
},
methods: {
getCompleteImageUrls (project) {
return project.imageUrls.map(image => {

+ 9
- 0
plugins/localStorage.js View File

@@ -0,0 +1,9 @@
import createPersistedState from 'vuex-persistedstate'

export default ({store}) => {
window.onNuxtReady(() => {
createPersistedState({
key: 'phenomic'
})(store)
})
}

+ 0
- 12
plugins/vue-chartjs.js View File

@@ -1,12 +0,0 @@
import Vue from 'vue'
import { Line } from 'vue-chartjs'
import zoomPlugin from 'chartjs-plugin-zoom'

Vue.component('LineChart', {
extends: Line,
props: ['data', 'options'],
mounted () {
this.addPlugin(zoomPlugin)
this.renderChart(this.data, this.options)
}
})

+ 58
- 4
store/index.js View File

@@ -1,9 +1,63 @@
import _ from 'lodash'

export const state = () => ({
sidebar: false
bytebeatSaves: [
{
id: 0,
isExample: true,
name: 'Default',
frequency: '8000',
duration: '30',
ft: 't * ((t>>12|t>>8)&63&t>>4)'
},
{
id: 1,
isExample: true,
name: 'Chaos Theory',
frequency: '8000',
duration: '30',
ft: 'w=t>>9,k=32,m=2048,a=1-t/m%1,d=(14*t*t^t)%m*a,y=[3,3,4.7,2][p=w/k&3]*t/4,h="IQNNNN!!]]!Q!IW]WQNN??!!W]WQNNN?".charCodeAt(w/2&15|p/3<<4)/33*t-t,s=y*.98%80+y%80+(w>>7&&a*((5*t%m*a&128)*(0x53232323>>w/4&1)+(d&127)*(0xa444c444>>w/4&1)*1.5+(d*w&1)+(h%k+h*1.99%k+h*.49%k+h*.97%k-64)*(4-a-a))),s*s>>14?127:s'
},
{
id: 2,
isExample: true,
name: 'Dante\'s Inferno',
frequency: '8000',
duration: '30',
ft: '((t*(t>>12)&(201*t/100)&(199*t/100))&(t*(t>>14)&(t*301/100)&(t*399/100)))+((t*(t>>16)&(t*202/100)&(t*198/100))-(t*(t>>17)&(t*302/100)&(t*298/100)))'
},
{
id: 3,
isExample: true,
name: 'Rick Roll\'d',
frequency: '8000',
duration: '30',
ft: '(t<<3)*[8/9,1,9/8,6/5,4/3,3/2,0][[0xd2d2c8,0xce4088,0xca32c8,0x8e4009][t>>14&3]>>(0x3dbe4688>>((t>>10&15)>9?18:t>>10&15)*3&7)*3&7]'
}
]
})

export const mutations = {
toggleSidebar (state) {
state.sidebar = !state.sidebar
saveByteBeat (state, payload) {
let clone = _.clone(payload, true)
state.bytebeatSaves.push(clone)
},
deleteByteBeatById (state, payload) {
state.bytebeatSaves = _.remove(state.bytebeatSaves, (element) => {
return element.id !== payload
})
}
}
export const getters = {
getNewId (state) {
let previousId = 0
let highestId = 0
state.bytebeatSaves.forEach(save => {
if (save.id > previousId) {
highestId = save.id
}
previousId = save.id
})
highestId++
return highestId
}
}

Loading…
Cancel
Save