The main website of PHENOM
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

bytebeat.vue 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <template>
  2. <v-layout column justify-center align-left>
  3. <v-flex xs12 sm8 md6>
  4. <h3 class="display-3">Bytebeat Generator</h3>
  5. <span class="subheading">This small tool can create music from a single JavaScript Expression using the Web Audio API.</span>
  6. <v-divider class="my-3"></v-divider>
  7. <v-select
  8. v-model="exampleSelected"
  9. prepend-icon="mdi-file-music"
  10. :items="bytebeatSaves"
  11. return-object
  12. item-text="name"
  13. >
  14. </v-select>
  15. <v-btn @click="load()">Load</v-btn>
  16. <v-btn @click="saveDialog = true" :disabled="!changed">Save As...</v-btn>
  17. <v-btn color="red" :disabled="exampleSelected.isExample" @click="deleteSave()">Delete</v-btn>
  18. <v-divider class="my-3"></v-divider>
  19. <v-form>
  20. <v-select
  21. v-model="liveByteBeat.frequency"
  22. prepend-icon="mdi-waves"
  23. :items="frequencies"
  24. label="Frequency (Hz)"
  25. >
  26. </v-select>
  27. <v-text-field
  28. v-model="liveByteBeat.duration"
  29. label="Duration (seconds)"
  30. placeholder="30"
  31. prepend-icon="timer"
  32. ></v-text-field>
  33. <v-text-field
  34. v-model="liveByteBeat.ft"
  35. label="JavaScript function (t)="
  36. 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'
  37. hint="The variable t determines how the function behaves"
  38. ></v-text-field>
  39. <v-slider
  40. v-model="volume"
  41. v-on:input="setVolume"
  42. :disabled="!isContextAvailable()"
  43. label="Volume"
  44. append-icon="volume_up"
  45. prepend-icon="volume_down"
  46. ></v-slider>
  47. <p color="red" v-if="changed">Changes were made. Press on Generate to recompile the expression.</p>
  48. <v-layout row wrap align-center>
  49. <v-btn color="green" @click="regenerate()">
  50. <v-icon>mdi-sync</v-icon>
  51. <span>Generate</span>
  52. </v-btn>
  53. <v-btn-toggle v-model="playBtn" multiple>
  54. <v-btn flat @click="playPause()" :disabled="!isGenerated()">
  55. <v-icon>mdi-play-pause</v-icon>
  56. </v-btn>
  57. </v-btn-toggle>
  58. <v-btn-toggle v-model="muteBtn" multiple>
  59. <v-btn flat @click="mute()" :disabled="!isContextAvailable()">
  60. <v-icon>mdi-volume-off</v-icon>
  61. </v-btn>
  62. </v-btn-toggle>
  63. </v-layout>
  64. </v-form>
  65. <v-dialog
  66. v-model="saveDialog"
  67. max-width="290"
  68. >
  69. <v-card>
  70. <v-card-title class="headline">Saving ByteBeat...</v-card-title>
  71. <v-layout align-content-space-between>
  72. <v-form>
  73. <v-text-field
  74. v-model="newName"
  75. placeholder="Enter a Name"
  76. label="Name"
  77. ></v-text-field>
  78. </v-form>
  79. </v-layout>
  80. <v-card-actions>
  81. <v-spacer></v-spacer>
  82. <v-btn
  83. flat="flat"
  84. @click="saveDialog = false"
  85. >
  86. Cancel
  87. </v-btn>
  88. <v-btn
  89. color="green darken-1"
  90. flat="flat"
  91. @click="onSave()"
  92. >
  93. Save
  94. </v-btn>
  95. </v-card-actions>
  96. </v-card>
  97. </v-dialog>
  98. </v-flex>
  99. </v-layout>
  100. </template>
  101. <script>
  102. import { mapGetters, mapState, mapMutations } from 'vuex'
  103. /* eslint no-eval: 0 */
  104. export default {
  105. layout: 'phenomic',
  106. data: () => ({
  107. exampleSelected: {
  108. id: 0,
  109. isExample: true,
  110. name: 'Default',
  111. frequency: '8000',
  112. duration: '30',
  113. ft: 't * ((t>>12|t>>8)&63&t>>4)'
  114. },
  115. liveByteBeat: {
  116. id: 0,
  117. name: 'Default',
  118. frequency: '8000',
  119. duration: '30',
  120. ft: 't * ((t>>12|t>>8)&63&t>>4)'
  121. },
  122. playBtn: [],
  123. muteBtn: [],
  124. frequencies: ['8000', '11025', '16000', '22050', '32000', '37800', '44056', '44100', '47250', '48000', '50000', '50400', '88200', '96000', '176400', '192000'],
  125. audioCtx: null,
  126. source: null,
  127. gainNode: null,
  128. volume: 50,
  129. sampleBuffer: null,
  130. saveDialog: false,
  131. changed: false,
  132. isLoading: false,
  133. newName: ''
  134. }),
  135. watch: {
  136. liveByteBeat: {
  137. handler: function (newVal, oldVal) {
  138. if (!this.isLoading) {
  139. this.changed = true
  140. } else {
  141. this.changed = false
  142. this.isLoading = false
  143. }
  144. },
  145. deep: true
  146. }
  147. },
  148. errorCaptured (err, vm, info) {
  149. console.log(err)
  150. },
  151. computed: {
  152. ...mapState({
  153. bytebeatSaves: 'bytebeatSaves'
  154. }),
  155. ...mapGetters({
  156. getNewId: 'getNewId'
  157. })
  158. },
  159. methods: {
  160. onChange (newVal) {
  161. console.log('Change: ' + newVal)
  162. this.changed = true
  163. this.liveByteBeat.isExample = false
  164. },
  165. isGenerated () {
  166. if (!(this.source === undefined || this.source === null)) {
  167. return !(this.source.buffer === undefined || this.source.buffer === null)
  168. }
  169. return false
  170. },
  171. isContextAvailable () {
  172. return !(this.audioCtx === undefined || this.audioCtx === null)
  173. },
  174. isExample () {
  175. return this.liveByteBeat.isExample
  176. },
  177. isPlaying () {
  178. if (this.audioCtx.state === 'running') {
  179. return true
  180. } else if (this.audioCtx.state === 'suspended') {
  181. return false
  182. }
  183. },
  184. regenerate () {
  185. // Reset change indicator
  186. this.changed = false
  187. // Reset Playbutton
  188. this.playBtn = []
  189. if (process.browser) {
  190. // Close Audio Context if already opened
  191. if (this.isContextAvailable()) {
  192. this.audioCtx.close()
  193. }
  194. // Create new Audio Context
  195. this.audioCtx = new (window.AudioContext || window.webkitAudioContext)()
  196. this.audioCtx.suspend() // Stop instant playback
  197. this.gainNode = this.audioCtx.createGain()
  198. this.gainNode.connect(this.audioCtx.destination)
  199. } else {
  200. console.error("This is not a browser! Can't continue!")
  201. return
  202. }
  203. // Create a new Audio Source
  204. this.source = this.audioCtx.createBufferSource()
  205. // Determines the behaviour when the Audio has finished playing
  206. this.source.onended = (event) => {
  207. this.audioCtx.suspend()
  208. this.playBtn = []
  209. }
  210. // Here is the main clear on compare match linear function of the bytebeat algorithm
  211. this.sampleBuffer = []
  212. let f
  213. eval('f = function (t) { return ' + this.liveByteBeat.ft + '}')
  214. for (let t = 0; t < this.liveByteBeat.frequency * this.liveByteBeat.duration; t++) {
  215. let sample = f(t)
  216. sample = (sample & 0xff) * 256
  217. if (sample < 0) sample = 0
  218. if (sample > 65535) sample = 65535
  219. this.sampleBuffer.push(sample / 65535)
  220. }
  221. // Fill Audio Buffer with Data
  222. let audioBuffer = this.audioCtx.createBuffer(2, this.liveByteBeat.frequency * this.liveByteBeat.duration, this.liveByteBeat.frequency)
  223. for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
  224. let channelBuffer = audioBuffer.getChannelData(channel)
  225. for (let i = 0; i < audioBuffer.length; i++) {
  226. channelBuffer[i] = this.sampleBuffer[i]
  227. }
  228. }
  229. // Apply Audio Buffer to source
  230. this.source.buffer = audioBuffer
  231. // Apply Gain connection for Volume control
  232. this.source.connect(this.gainNode)
  233. // Start the source
  234. this.source.start(0)
  235. },
  236. playPause () {
  237. if (this.isGenerated()) {
  238. if (this.isPlaying()) {
  239. this.audioCtx.suspend()
  240. this.playBtn = []
  241. } else {
  242. this.audioCtx.resume()
  243. }
  244. }
  245. },
  246. mute () {
  247. if (this.muteBtn.length === 0) {
  248. this.gainNode.gain.value = 0
  249. } else {
  250. this.gainNode.gain.value = this.volume / 100.0
  251. }
  252. },
  253. setVolume (something) {
  254. this.muteBtn = []
  255. this.gainNode.gain.value = something / 100.0
  256. },
  257. onSave () {
  258. this.saveDialog = false
  259. this.liveByteBeat.name = this.newName
  260. this.liveByteBeat.id = this.getNewId
  261. this.saveBeat(this.liveByteBeat)
  262. this.exampleSelected = this.liveByteBeat
  263. this.isCurrentExample = false
  264. },
  265. load () {
  266. this.isLoading = true
  267. this.liveByteBeat.id = this.exampleSelected.id
  268. this.liveByteBeat.duration = this.exampleSelected.duration
  269. this.liveByteBeat.frequency = this.exampleSelected.frequency
  270. this.liveByteBeat.ft = this.exampleSelected.ft
  271. this.liveByteBeat.name = this.exampleSelected.name
  272. this.isCurrentExample = this.exampleSelected.isExample
  273. },
  274. deleteSave () {
  275. if (!this.isCurrentExample) {
  276. this.deleteBeat(this.exampleSelected.id)
  277. }
  278. },
  279. ...mapMutations({
  280. saveBeat: 'saveByteBeat',
  281. deleteBeat: 'deleteByteBeatById'
  282. })
  283. },
  284. beforeRouteLeave (to, from, next) {
  285. // Close Audio Context if already opened
  286. if (this.isContextAvailable()) {
  287. this.audioCtx.close()
  288. }
  289. next()
  290. }
  291. }
  292. </script>