|
|
|
<template>
|
|
|
|
<v-layout column justify-center align-left>
|
|
|
|
<v-flex xs12 sm8 md6>
|
|
|
|
<h3 class="display-3">Bytebeat Generator</h3>
|
|
|
|
<span class="subheading">This small tool can create music from a single JavaScript Expression using the Web Audio API.</span>
|
|
|
|
<v-divider class="my-3"></v-divider>
|
|
|
|
<v-select
|
|
|
|
v-model="exampleSelected"
|
|
|
|
prepend-icon="mdi-file-music"
|
|
|
|
:items="bytebeatSaves"
|
|
|
|
return-object
|
|
|
|
item-text="name"
|
|
|
|
>
|
|
|
|
</v-select>
|
|
|
|
<v-btn @click="load()">Load</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
|
|
|
|
v-model="liveByteBeat.frequency"
|
|
|
|
prepend-icon="mdi-waves"
|
|
|
|
:items="frequencies"
|
|
|
|
label="Frequency (Hz)"
|
|
|
|
>
|
|
|
|
</v-select>
|
|
|
|
<v-text-field
|
|
|
|
v-model="liveByteBeat.duration"
|
|
|
|
label="Duration (seconds)"
|
|
|
|
placeholder="30"
|
|
|
|
prepend-icon="timer"
|
|
|
|
></v-text-field>
|
|
|
|
<v-text-field
|
|
|
|
v-model="liveByteBeat.ft"
|
|
|
|
label="JavaScript function (t)="
|
|
|
|
value='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'
|
|
|
|
hint="The variable t determines how the function behaves"
|
|
|
|
></v-text-field>
|
|
|
|
<v-slider
|
|
|
|
v-model="volume"
|
|
|
|
v-on:input="setVolume"
|
|
|
|
:disabled="!isContextAvailable()"
|
|
|
|
label="Volume"
|
|
|
|
append-icon="volume_up"
|
|
|
|
prepend-icon="volume_down"
|
|
|
|
></v-slider>
|
|
|
|
<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-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"
|
|
|
|
max-width="290"
|
|
|
|
>
|
|
|
|
<v-card>
|
|
|
|
<v-card-title class="headline">Saving ByteBeat...</v-card-title>
|
|
|
|
<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
|
|
|
|
flat="flat"
|
|
|
|
@click="saveDialog = false"
|
|
|
|
>
|
|
|
|
Cancel
|
|
|
|
</v-btn>
|
|
|
|
<v-btn
|
|
|
|
color="green darken-1"
|
|
|
|
flat="flat"
|
|
|
|
@click="onSave()"
|
|
|
|
>
|
|
|
|
Save
|
|
|
|
</v-btn>
|
|
|
|
</v-card-actions>
|
|
|
|
</v-card>
|
|
|
|
</v-dialog>
|
|
|
|
</v-flex>
|
|
|
|
</v-layout>
|
|
|
|
</template>
|
|
|
|
<script>
|
|
|
|
import { mapGetters, mapState, mapMutations } from 'vuex'
|
|
|
|
|
|
|
|
/* eslint no-eval: 0 */
|
|
|
|
export default {
|
|
|
|
layout: 'phenomic',
|
|
|
|
data: () => ({
|
|
|
|
exampleSelected: {
|
|
|
|
id: 0,
|
|
|
|
isExample: true,
|
|
|
|
name: 'Default',
|
|
|
|
frequency: '8000',
|
|
|
|
duration: '30',
|
|
|
|
ft: 't * ((t>>12|t>>8)&63&t>>4)'
|
|
|
|
},
|
|
|
|
liveByteBeat: {
|
|
|
|
id: 0,
|
|
|
|
name: 'Default',
|
|
|
|
frequency: '8000',
|
|
|
|
duration: '30',
|
|
|
|
ft: 't * ((t>>12|t>>8)&63&t>>4)'
|
|
|
|
},
|
|
|
|
playBtn: [],
|
|
|
|
muteBtn: [],
|
|
|
|
frequencies: ['8000', '11025', '16000', '22050', '32000', '37800', '44056', '44100', '47250', '48000', '50000', '50400', '88200', '96000', '176400', '192000'],
|
|
|
|
audioCtx: null,
|
|
|
|
source: null,
|
|
|
|
gainNode: null,
|
|
|
|
volume: 50,
|
|
|
|
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: {
|
|
|
|
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)
|
|
|
|
},
|
|
|
|
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 + '}')
|
|
|
|
for (let t = 0; t < this.liveByteBeat.frequency * this.liveByteBeat.duration; t++) {
|
|
|
|
let sample = f(t)
|
|
|
|
sample = (sample & 0xff) * 256
|
|
|
|
if (sample < 0) sample = 0
|
|
|
|
if (sample > 65535) sample = 65535
|
|
|
|
this.sampleBuffer.push(sample / 65535)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
for (let i = 0; i < audioBuffer.length; i++) {
|
|
|
|
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)
|
|
|
|
},
|
|
|
|
playPause () {
|
|
|
|
if (this.isGenerated()) {
|
|
|
|
if (this.isPlaying()) {
|
|
|
|
this.audioCtx.suspend()
|
|
|
|
this.playBtn = []
|
|
|
|
} else {
|
|
|
|
this.audioCtx.resume()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
mute () {
|
|
|
|
if (this.muteBtn.length === 0) {
|
|
|
|
this.gainNode.gain.value = 0
|
|
|
|
} else {
|
|
|
|
this.gainNode.gain.value = this.volume / 100.0
|
|
|
|
}
|
|
|
|
},
|
|
|
|
setVolume (something) {
|
|
|
|
this.muteBtn = []
|
|
|
|
this.gainNode.gain.value = something / 100.0
|
|
|
|
},
|
|
|
|
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'
|
|
|
|
})
|
|
|
|
},
|
|
|
|
beforeRouteLeave (to, from, next) {
|
|
|
|
// Close Audio Context if already opened
|
|
|
|
if (this.isContextAvailable()) {
|
|
|
|
this.audioCtx.close()
|
|
|
|
}
|
|
|
|
next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|