1387 lines
44 KiB
HTML
1387 lines
44 KiB
HTML
<!--
|
||
|
||
ZzFX - Zuper Zmall Zound Zynth by Frank Force
|
||
https://github.com/KilledByAPixel/ZzFX
|
||
|
||
ZzFX UI Features
|
||
|
||
- Generates random sounds from presets.
|
||
- Sound list is saved automatically.
|
||
- Each parameter can be modified with constraints.
|
||
- Lock and mutate buttons for each parameter.
|
||
- Sound name can be changed for easier workflow.
|
||
- Shortens code for zzfx sound calls.
|
||
- Displays image of sound wave when played.
|
||
- Sounds can be download as a wave file.
|
||
- Sounds can be marked as favorites to prevent removal.
|
||
- Sounds can be loaded by pasting zzfx code for easy sharing.
|
||
- List of sounds can be exported and imported.
|
||
- Supports drag-and-drop of exported files into sound list.
|
||
|
||
GitHub Corner is Copyright (c) 2016 Tim Holman - http://tholman.com
|
||
|
||
-->
|
||
<!--
|
||
|
||
ZzFX MIT License
|
||
|
||
Copyright (c) 2019 - Frank Force
|
||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
of this software and associated documentation files (the "Software"), to deal
|
||
in the Software without restriction, including without limitation the rights
|
||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
copies of the Software, and to permit persons to whom the Software is
|
||
furnished to do so, subject to the following conditions:
|
||
|
||
The above copyright notice and this permission notice shall be included in all
|
||
copies or substantial portions of the Software.
|
||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
SOFTWARE.
|
||
|
||
-->
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<title>ZzFX - Zuper Zmall Zound Zynth</title>
|
||
<meta charset="utf-8">
|
||
<meta name="google" content="notranslate">
|
||
<meta property="og:title" content="ZzFX - Zuper Zmall Zound Zynth">
|
||
<meta property="og:description" content="A tiny sound effects generator for games and websites!">
|
||
<meta name="twitter:image" content="screenshot.jpg">
|
||
<meta property="og:image" content="screenshot.jpg">
|
||
<link rel="shortcut icon" type="image/x-icon" href="favicon.png"/>
|
||
<style>
|
||
* { font-family:courier; }
|
||
a { color:#5AF; }
|
||
button { margin:3px; font-size:18px;}
|
||
button.large { height:26px; }
|
||
button.rand { width:15px; height:15px; background:#3ff; }
|
||
button.reset { width:15px; height:15px; background:#3F3; }
|
||
.soundSelect { overflow-x:hidden; height:400px; }
|
||
.slider { cursor: pointer; }
|
||
input.lock { width:15px; height:15px; }
|
||
.settingRow { width:200px; display:flex }
|
||
.setting { width:100%; }
|
||
textarea { height:50px; cursor:text; resize:none; }
|
||
body,input,select,textarea { color:#FFF; font-size:18px;}
|
||
select,input,textarea,canvas { background:#000; border:1px solid #FFF; }
|
||
option:hover { background:#888; }
|
||
option:checked { background:#444; }
|
||
body { user-select:none; }
|
||
</style>
|
||
</head>
|
||
<body bgcolor=#222>
|
||
<div id=UI style=display:none>
|
||
<center>
|
||
<table>
|
||
<tr>
|
||
<td style=text-align:right;>
|
||
<font size=7><b><div id=div_logo style='display:inline'>ℤ𝕫𝔽𝕏</div></b></font>
|
||
</td>
|
||
<td style=text-align:left>
|
||
<div style=display:inline;font-size:22px id=div_logo2>
|
||
<i>Zuper Zmall Zound Zynth</i>
|
||
</div>
|
||
</td>
|
||
<td style=text-align:center;width:380px rowspan=2>
|
||
<div id=div_masterVolume></div>
|
||
<input id=slider_masterVolume title='Volume to scale all sounds by in percent' type=range min=0 max=100 value=25 style=width:90% oninput=UpdateSettings();SaveLocalStorage()>
|
||
<canvas title='Image of sound wave, click to play the sound [SPACE]' id=canvas_soundWave width=350 height=80 style=width:100%></canvas>
|
||
<button class=large title='Create new random sound [INS]' onclick=AddPresetSound()>Random</button><button class=large title='Make a new sound with mutated parameters [M]' onclick=CopySelected(1)>Mutate</button><button class=large title='Make copy of selected sound [C]' onclick=CopySelected()>Copy</button>
|
||
<br>
|
||
<button class=large title='Generate random pickup sound' onclick="AddPresetSound('Pickup')">Pickup</button><button class=large title='Generate random powerup sound' onclick="AddPresetSound('Powerup')">Powerup</button><button class=large title='Generate random shoot sound' onclick="AddPresetSound('Shoot')">Shoot</button><button class=large title='Generate a tonal sound for music' onclick="AddPresetSound('Music')">Note</button>
|
||
<br>
|
||
<button class=large title='Generate random jump sound' onclick="AddPresetSound('Jump')">Jump</button><button class=large title='Generate random blip sound' onclick="AddPresetSound('Blip')">Blip</button><button class=large title='Generate random hit sound' onclick="AddPresetSound('Hit')">Hit</button><button class=large title='Generate random explosion sound' onclick="AddPresetSound('Explosion')">Explosion</button>
|
||
<br>
|
||
<button class=large title='Toggle favorite on current sound [F]' onclick=FavoriteSelected()>Favorite</button><button class=large title='Move selected to top of the list' onclick="SelectedToTop()">Top</button><button title='Remove selected sound [DEL]' class=large onclick=RemoveSelected()>Remove</button>
|
||
<br>
|
||
<select id=select_soundList class=soundSelect onclick='loadedSound=0;LoadSelected(1)' onchange=LoadSelected(1) size=2 style=margin:5px;width:100%></select>
|
||
</td></tr>
|
||
<tr>
|
||
<td colspan=2>
|
||
<div id=div_settingsTable></div>
|
||
</td>
|
||
</tr>
|
||
<tr><td colspan=3 style=padding:5px>
|
||
<center>
|
||
<button title='Download the selected wave file' onclick=SaveWave()>Save Wav</button>
|
||
<button title='Load sound from zzfx code' onclick=LoadSound()>Load Sound</button>
|
||
<button title='Copy ZzFX code to clipboard' onclick=if(CopyToCliboard(textarea_code.value))eval(textarea_code.value)>Copy Sound</button>
|
||
<button title='Import all sounds from a text file.' onclick=Import()>Import</button>
|
||
<button title='Export all sounds to a text file.' onclick=Export()>Export</button>
|
||
<button title='Clear non-favorite sounds' onclick=ClearSoundsButton()>Clear</button>
|
||
<button title='Clear all sounds' onclick=ClearSoundsButton(1)>Clear All</button>
|
||
</center>
|
||
</td></tr>
|
||
<tr><td colspan=3 style=padding:5px>
|
||
<center>ZzFX JavaScript
|
||
<input type=radio name=radio_codeStyle id=input_codeStyleCompact checked onchange="SaveLocalStorage();textarea_code.value=GetCode(GetSelectedSound())">Compact
|
||
<input type=radio name=radio_codeStyle id=input_codeStyleFull onchange="SaveLocalStorage();textarea_code.value=GetCode(GetSelectedSound())">Full
|
||
<input type=radio name=radio_codeStyle id=input_codeStyleLittleJS onchange="SaveLocalStorage();textarea_code.value=GetCode(GetSelectedSound())">LittleJS
|
||
|
||
<br><textarea title='Use this code to play the selected sound' id=textarea_code style=width:90% readonly></textarea>
|
||
</center>
|
||
</td></tr>
|
||
</table>
|
||
ZzFX © <a href=https://www.frankforce.com target=_blank>Frank Force</a> 2019 ☮♥☻␌
|
||
<a hidden id=a_downloadLink></a>
|
||
<input hidden id=input_importFile type=file accept=.txt>
|
||
</center>
|
||
<script src=ZzFXMicro.js></script>
|
||
<script type=module>
|
||
|
||
// load zzfx module into global scope
|
||
import {ZZFX} from './ZzFX.js';
|
||
window.ZZFX = ZZFX;
|
||
window.zzfx = zzfx; // uncomment to test zzfx micro
|
||
|
||
import {buildWavBlob} from './wav.js';
|
||
window.buildWavBlob = buildWavBlob;
|
||
|
||
Init();
|
||
|
||
</script>
|
||
<script>
|
||
|
||
'use strict'; // strict mode
|
||
let sounds = [];
|
||
let generatedSoundCount = 0;
|
||
let loadedSound;
|
||
let lastPlayedSound;
|
||
let defaultSound;
|
||
|
||
const favoriteColor = '#f00';
|
||
|
||
const SeededRandom=_=>
|
||
(
|
||
randomSeed ^= randomSeed << 13,
|
||
randomSeed ^= randomSeed >> 17,
|
||
randomSeed ^= randomSeed << 5,
|
||
(randomSeed%1e9)/1e9
|
||
)
|
||
let randomSeed = Date.now();
|
||
let loadedSoundSeed = 0;
|
||
|
||
// build a sound object with same defaults as zzfx
|
||
function BuildSound
|
||
(
|
||
volume = 1,
|
||
randomness = .05,
|
||
frequency = 220,
|
||
attack = 0,
|
||
sustain = 0,
|
||
release = .1,
|
||
shape = 0,
|
||
shapeCurve = 1,
|
||
slide = 0,
|
||
deltaSlide = 0,
|
||
pitchJump = 0,
|
||
pitchJumpTime = 0,
|
||
repeatTime = 0,
|
||
noise = 0,
|
||
modulation = 0,
|
||
bitCrush = 0,
|
||
delay = 0,
|
||
sustainVolume = 1,
|
||
decay = 0,
|
||
tremolo = 0
|
||
)
|
||
{
|
||
const sound =
|
||
{
|
||
volume,
|
||
randomness,
|
||
frequency,
|
||
attack,
|
||
sustain,
|
||
release,
|
||
shape,
|
||
shapeCurve,
|
||
slide,
|
||
deltaSlide,
|
||
pitchJump,
|
||
pitchJumpTime,
|
||
repeatTime,
|
||
noise,
|
||
modulation,
|
||
bitCrush,
|
||
delay,
|
||
sustainVolume,
|
||
decay,
|
||
tremolo
|
||
};
|
||
|
||
return sound;
|
||
}
|
||
|
||
// convert sound parameters object to array
|
||
function SoundToArray(sound)
|
||
{
|
||
// use default sound for keys and order
|
||
const array = [];
|
||
for(const key in defaultSound)
|
||
array.push(sound[key]);
|
||
return array;
|
||
}
|
||
|
||
function Init()
|
||
{
|
||
defaultSound = BuildSound();
|
||
UI.style.display = '';
|
||
|
||
BuildSettingsTable();
|
||
RandomizeLogo();
|
||
LoadLocalStorage();
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// settings
|
||
|
||
const SETTING_TYPE_NAME = 1;
|
||
const SETTING_TYPE_SHAPE = 2;
|
||
|
||
function BuildSetting(step, min, max, name, niceName, help, canMutate=1, type=0)
|
||
{ return {name, step, min, max, niceName, help, type, canMutate}; }
|
||
|
||
const settings =
|
||
[
|
||
BuildSetting(0,0,0,'name','Name','Name of sound',0,SETTING_TYPE_NAME),
|
||
BuildSetting(.1,-1e9,1e9,'volume','Volume','Volume scale (percent)',0),
|
||
BuildSetting(.05,-1e9,1e9,'randomness','Randomness','How much to randomize frequency (percent Hz)',0),
|
||
BuildSetting(1,-1e9,1e9,'frequency','Frequency','Frequency of sound (Hz)'),
|
||
BuildSetting(1,0,0,'shape','Wave Shape','Shape of the sound wave',1,SETTING_TYPE_SHAPE),
|
||
BuildSetting(.1,0,1e9,'shapeCurve','Shape Curve','Squarenes of wave (0=square, 1=normal, 2=pointy)'),
|
||
BuildSetting(.01,0,3,'attack','Attack','Attack time, how fast sound starts (seconds)'),
|
||
BuildSetting(.01,0,1,'decay','Decay','Decay time, how long to reach sustain after attack'),
|
||
BuildSetting(.01,0,3,'sustain','Sustain','Sustain time, how long sound holds (seconds)'),
|
||
BuildSetting(.01,0,3,'release','Release','Release time, how fast sound fades out (seconds)'),
|
||
BuildSetting(.1,-1e9,1e9,'sustainVolume','Sustain Volume','Volume level for sustain (percent)'),
|
||
BuildSetting(.1,-1e9,1e9,'slide','Slide','How much to slide frequency (kHz/s)'),
|
||
BuildSetting(.1,-1e9,1e9,'deltaSlide','Delta Slide','How much to change slide (kHz/s/s)'),
|
||
BuildSetting(50,-1e9,1e9,'pitchJump','Pitch Jump','Frequency of pitch jump (Hz)'),
|
||
BuildSetting(.01,-1e9,1e9,'pitchJumpTime','Pitch Jump Time','Time of pitch jump (seconds)'),
|
||
BuildSetting(.01,-1e9,1e9,'repeatTime','Repeat Time','Resets some parameters periodically (seconds)'),
|
||
BuildSetting(1,-1e9,1e9,'modulation','Modulation','Frequency of modulation wave, negative flips phase (Hz)'),
|
||
BuildSetting(.1,-1e9,1e9,'noise','Noise','How much random noise to add (percent)'),
|
||
BuildSetting(.1,-1e9,1e9,'bitCrush','Bit Crush','Resamples at a lower frequency in (samples*100)'),
|
||
BuildSetting(.01,0,1e9,'delay','Delay','Overlap sound with itself for reverb and flanger effects (seconds)'),
|
||
BuildSetting(.01,0,1,'tremolo','Tremolo','Trembling effect, rate controlled by repeat time (precent)'),
|
||
]
|
||
|
||
let noteScale;
|
||
|
||
function BuildSettingsTable()
|
||
{
|
||
const NoteName = n=>"CCDDEFFGGAAB"[n%12|0] + ('02579'.indexOf(n%12-1) < 0 ? '' : '#') + (n/12|0);
|
||
|
||
noteScale = [];
|
||
for(let i = 0; i < 37; i++)
|
||
{
|
||
// skip sharps
|
||
if ([1,3,6,8,10].includes(i%12))
|
||
continue;
|
||
|
||
noteScale.push([ZZFX.getNote(i+3-36).toPrecision(7), NoteName(i)]);
|
||
}
|
||
|
||
const s = settings[0];
|
||
let html = '<center><table style=text-align:center>';
|
||
html += '<tr><td style=text-align:right>' + s.niceName + ' </td><td class=settingRow>'
|
||
html += `<input title='${s.help}' class=setting step=${s.step} id=input_${s.name} oninput=SelectedWasChanged('${s.name}')>`
|
||
|
||
html += '</td><td>';
|
||
html += '<button title="Toggle lock all parameters" onclick=ToggleLockAll()>🔒</button>'
|
||
|
||
html += '</td><td>';
|
||
html += '<button title="Sets all unlocked parameters to their default values." onclick=if(ResetAllSettings())PlaySelected()>♻️</button>';
|
||
|
||
html += '</td><td>';
|
||
html += '<button title="Mutate all unlocked parameters" onclick=if(MutateAllSettings())PlaySelected()>🎲</button>';
|
||
|
||
html += '</td></tr>';
|
||
|
||
for(const i in settings)
|
||
{
|
||
const s = settings[i];
|
||
if (s.name == 'name')
|
||
continue;
|
||
|
||
html += '<tr>';
|
||
html += '<td style=text-align:right>';
|
||
html += s.niceName + ' ';
|
||
html += '</td><td class=settingRow>';
|
||
|
||
const isFrequency = s.name == 'frequency';
|
||
|
||
if (!s.type)
|
||
html += `<input id=input_${s.name} class=setting ${isFrequency?'style=width:140px':''} title='${s.help}' type=number step=${s.step} oninput=SelectedWasChanged('${s.name}') min=${s.min} max=${s.max} onfocusout=UpdateSettings()>`
|
||
else if (s.type == SETTING_TYPE_SHAPE)
|
||
{
|
||
html += `<select id=input_${s.name} class=setting title='${s.help}' oninput=SelectedWasChanged('${s.name}')>`;
|
||
html += `<option value=0>sin</option>`;
|
||
html += `<option value=1>triangle</option>`;
|
||
html += `<option value=2>saw</option>`;
|
||
html += `<option value=3>tan</option>`;
|
||
html += `<option value=4>noise</option>`;
|
||
html += `</select>`;
|
||
}
|
||
|
||
if (isFrequency)
|
||
{
|
||
html += `<select id=input_note class=setting style=width:100px title='Set frequency to note' oninput=SetFrequencyToNote()>`;
|
||
html += `<option value=-1></option>`;
|
||
noteScale.map( (note, i)=>html += `<option value=${i}>${note[1]}</option>`);
|
||
html += `</select>`;
|
||
}
|
||
|
||
html += '</td><td>';
|
||
html += `<input id=input_lock_${s.name} class=lock title='Lock ${s.niceName}' oninput=SelectedWasChanged('${s.name}') type=checkbox>`;
|
||
|
||
html += '</td><td>';
|
||
html += `<button id=input_reset_${s.name} class=reset title='Reset ${s.niceName}' onclick=ResetSetting(settings[${i}]);PlaySelected()></button>`;
|
||
|
||
html += '</td><td>';
|
||
html += `<button id=input_mutate_${s.name} class=rand title='Mutate ${s.niceName}' onclick=MutateSetting(settings[${i}]);PlaySelected()></button>`;
|
||
|
||
html += '</td></tr>';
|
||
}
|
||
|
||
html += '</table></center>'
|
||
div_settingsTable.innerHTML = html;
|
||
}
|
||
|
||
function GetSelectedSound()
|
||
{
|
||
if (select_soundList.selectedIndex < 0)
|
||
return;
|
||
const index = select_soundList.options[select_soundList.selectedIndex].value;
|
||
return sounds[index];
|
||
}
|
||
|
||
function PlaySelected()
|
||
{
|
||
if (lastPlayedSound)
|
||
{
|
||
try
|
||
{
|
||
const context = lastPlayedSound.context;
|
||
const gain = context.createGain();
|
||
|
||
lastPlayedSound.disconnect();
|
||
lastPlayedSound.connect(gain);
|
||
gain.connect(context.destination);
|
||
|
||
const t = lastPlayedSound.context.currentTime + .02;
|
||
gain.gain.linearRampToValueAtTime(1, t);
|
||
gain.gain.linearRampToValueAtTime(0, t + .1);
|
||
}
|
||
catch (e) { lastPlayedSound.stop(); }
|
||
|
||
lastPlayedSound = 0;
|
||
}
|
||
|
||
const sound = BuildSoundFromSettings();
|
||
const params = SoundToArray(sound);
|
||
|
||
if (ZZFX.volume > 0)
|
||
{
|
||
const samples = ZZFX.buildSamples(...params);
|
||
lastPlayedSound = ZZFX.play(...params);
|
||
DrawSoundWave(samples, ZZFX.volume, sound);
|
||
}
|
||
else
|
||
{
|
||
// just build samples without playing
|
||
const saveVolume = ZZFX.volume;
|
||
ZZFX.volume = 1;
|
||
const samples = ZZFX.buildSamples(...params);
|
||
DrawSoundWave(samples, 1, sound);
|
||
ZZFX.volume = saveVolume;
|
||
}
|
||
RandomizeLogo();
|
||
}
|
||
|
||
function BuildSoundFromSettings()
|
||
{
|
||
const sound = {};
|
||
for(const s of settings)
|
||
{
|
||
let v = document.getElementById("input_"+s.name).value;
|
||
if (s.type != SETTING_TYPE_NAME)
|
||
v = parseFloat(v) || 0;
|
||
|
||
sound[s.name] = v;
|
||
}
|
||
|
||
return sound;
|
||
}
|
||
|
||
function UpdateSettings()
|
||
{
|
||
const v = slider_masterVolume.value;
|
||
div_masterVolume.innerHTML = 'Master Volume ' + v + '%';
|
||
ZZFX.volume = v / 100;
|
||
|
||
const sound = GetSelectedSound();
|
||
if (sound)
|
||
{
|
||
// copy settings to selected
|
||
const option = select_soundList.options[select_soundList.selectedIndex];
|
||
const index = parseInt(option.value);
|
||
|
||
for(const s of settings)
|
||
{
|
||
const element = document.getElementById("input_"+s.name);
|
||
let v = element.value;
|
||
if (!s.type && element != document.activeElement)
|
||
{
|
||
// sanitize input
|
||
v = parseFloat(v);
|
||
if (isNaN(v) || !Number.isFinite(v))
|
||
v = 0;
|
||
else
|
||
v = Math.max(Math.min(v,s.max),s.min);
|
||
element.value = v;
|
||
}
|
||
sound[s.name] = v;
|
||
|
||
if (s.name == 'frequency')
|
||
{
|
||
// try to find matching note
|
||
const eNote = document.getElementById("input_note");
|
||
const noteIndex = noteScale.findIndex(e=>e[0] == v);
|
||
eNote.value = noteIndex;
|
||
}
|
||
|
||
const elementReset = document.getElementById("input_reset_"+s.name);
|
||
|
||
if (elementReset)
|
||
{
|
||
const isDefault = v == defaultSound[s.name];
|
||
elementReset.style.background = isDefault ? '' : '#f3f';
|
||
}
|
||
}
|
||
|
||
option.style.color = sound.favorite? favoriteColor : "inherit";
|
||
option.innerHTML = (sound.favorite?'-> ':sound.modified?'* ':'') + sound.name;
|
||
textarea_code.value = GetCode(sound);
|
||
}
|
||
|
||
SaveLocalStorage();
|
||
}
|
||
|
||
function BuildRandomSound(lengthScale=1, volume=1, randomness=.05)
|
||
{
|
||
// generate a random sound
|
||
const R=()=>Math.random(), C=()=>R()<.5?R():0, S=()=>C()?1:-1,
|
||
|
||
// randomize sound length
|
||
attack = R()**3/4*lengthScale,
|
||
decay = R()**3/4*lengthScale,
|
||
sustain = R()**3/4*lengthScale,
|
||
release = R()**3/4*lengthScale,
|
||
length = attack + decay + sustain + release;
|
||
|
||
// create random sound
|
||
return BuildSound
|
||
(
|
||
volume, // volume
|
||
randomness, // randomness
|
||
R()**2*2e3, // frequency
|
||
attack, // attack
|
||
sustain, // sustain
|
||
release, // release
|
||
R()*5|0, // shape
|
||
R()**2*3, // shapeCurve
|
||
C()**3*99*S(), // slide
|
||
C()**3*99*S(), // deltaSlide
|
||
C()**2*1e3*S(), // pitchJump
|
||
R()**2 * length, // pitchJumpTime
|
||
C() * length, // repeatTime
|
||
C()**4, // noise
|
||
R()*C()**3*500*S(),// modulation
|
||
C()**4, // bitCrush
|
||
C()**3/2, // delay
|
||
1 - C(), // sustain volume
|
||
decay, // decay
|
||
C()**4 // tremolo
|
||
);
|
||
}
|
||
|
||
function AddPresetSound(presetName='Random')
|
||
{
|
||
let sound = BuildSound();
|
||
const R =(a=1,b=0) => b+(a-b)*Math.random();
|
||
|
||
switch (presetName)
|
||
{
|
||
case 'Random':
|
||
{
|
||
sound = BuildRandomSound();
|
||
break;
|
||
}
|
||
case 'Default':
|
||
{
|
||
sound = BuildSound();
|
||
break;
|
||
}
|
||
case 'Pickup':
|
||
{
|
||
sound.frequency = R(100,2000);
|
||
sound.shape = R(3)|0;
|
||
sound.shapeCurve = R(2);
|
||
sound.attack = R(.02);
|
||
sound.decay = R(.05);
|
||
sound.sustain = R(.1);
|
||
sound.sustainVolume = R(.4, 1);
|
||
sound.release = R(.1,.2);
|
||
sound.noise = R()<.8 ? 0 : R(.1);
|
||
if (R()<.5)
|
||
{
|
||
sound.pitchJump = R(1)**2*1000*R(-1,1);
|
||
sound.pitchJumpTime = R(.1);
|
||
}
|
||
sound.slide = R()<.5 ? 0 : R(-1,1)**3*10;
|
||
sound.deltaSlide = R()<.5 ? 0 : R(-1,1)**3*10;
|
||
sound.repeatTime = R()<.5 ? 0 : R(.2);
|
||
sound.bitCrush = R()<.5 ? 0 : R(.1);
|
||
sound.delay = R()<.7 ? 0 : R(.1);
|
||
sound.tremolo = R()<.5 ? 0 : R(.2);
|
||
sound.modulation = R()<.8 ? 0 : R(-1,1)**2*50;
|
||
break;
|
||
}
|
||
case 'Powerup':
|
||
{
|
||
sound.frequency = R(0,700);
|
||
sound.shape = R(3)|0;
|
||
sound.shapeCurve = R(2);
|
||
sound.attack = R(.1);
|
||
sound.decay = R(.1,.3);
|
||
sound.sustain = R(.1,.3);
|
||
sound.sustainVolume = R(.4, 1);
|
||
sound.release = R(.2,.5);
|
||
if (R()<.5)
|
||
{
|
||
sound.pitchJump = R(1)**2*1000*R(-1,1);
|
||
sound.pitchJumpTime = !sound.pitchJump ? 0 : R(.2);
|
||
}
|
||
sound.delay = R()<.8 ? 0 : R(.2);
|
||
sound.repeatTime = R(.2);
|
||
sound.slide = R()<.5 ? 0 : R(-1,1)**3*10;
|
||
sound.deltaSlide = R()<.5 ? 0 : R(-1,1)**3*10;
|
||
sound.noise = R()<.8 ? 0 : R(.2);
|
||
sound.bitCrush = R()<.5 ? 0 : R(.2);
|
||
sound.tremolo = R()<.5 ? 0 : R(.5);
|
||
sound.modulation = R()<.8 ? 0 : R(-1,1)**2*50;
|
||
break;
|
||
}
|
||
case 'Jump':
|
||
{
|
||
sound.frequency = R(0,500);
|
||
sound.shape = R(3)|0;
|
||
sound.shapeCurve = R(2);
|
||
sound.attack = R(.05);
|
||
sound.decay = R(.1);
|
||
sound.sustain = R(.1);
|
||
sound.sustainVolume = R(.4, 1);
|
||
sound.release = R(.05,.1);
|
||
sound.noise = R()<.5 ? 0 : R(2);
|
||
sound.slide = R(-30,30);
|
||
sound.deltaSlide = R()<.5 ? 0 : R(-5,5);
|
||
sound.bitCrush = R()<.5 ? 0 : R(.1);
|
||
sound.delay = R()<.8 ? 0 : R(.05);
|
||
break;
|
||
}
|
||
case 'Shoot':
|
||
{
|
||
sound.frequency = R(0,500);
|
||
sound.shape = R(5)|0;
|
||
sound.shapeCurve = R(2);
|
||
sound.attack = R(.02);
|
||
sound.decay = R(.1);
|
||
sound.sustain = R(.1);
|
||
sound.sustainVolume = R(.5, 1);
|
||
sound.release = R(.1);
|
||
sound.delay = R()<.5 ? 0 : R(.3);
|
||
sound.slide = R()<.5 ? 0 : R(-9,9);
|
||
sound.deltaSlide = R()<.5 ? 0 : R(-1,1);
|
||
sound.noise = R()<.8 ? 0 : R(2);
|
||
sound.repeatTime = R()<.5 ? 0 : R(.1);
|
||
sound.modulation = R()<.8 ? 0 : R(-1,1)**2*500;
|
||
sound.bitCrush = R()<.5 ? 0 : R(.5);
|
||
sound.tremolo = R()<.5 ? 0 : R(.5);
|
||
break;
|
||
}
|
||
case 'Blip':
|
||
{
|
||
sound = BuildRandomSound();
|
||
sound.attack = R(.02);
|
||
sound.decay = R(.02);
|
||
sound.sustain = R(.02);
|
||
sound.release = R(.02);
|
||
break;
|
||
}
|
||
case 'Hit':
|
||
{
|
||
sound.frequency = R(50,500);
|
||
sound.shape = R(5)|0;
|
||
sound.shapeCurve = R(3);
|
||
sound.attack = R(.03);
|
||
sound.decay = R(.1);
|
||
sound.sustain = R(.1);
|
||
sound.sustainVolume = R(.4, 1);
|
||
sound.release = R(.03,.2);
|
||
sound.delay = R()<.5 ? 0 : R(.2);
|
||
sound.slide = R()<.5 ? 0 : R(-1,1)**3*10;
|
||
sound.deltaSlide = R()<.5 ? 0 : R(-1,1)**3*10;
|
||
sound.noise = R(2);
|
||
sound.modulation = R()<.8 ? 0 : R(-1,1)**3*500;
|
||
sound.bitCrush = R(.5);
|
||
sound.repeatTime = R()<.5 ? 0 : R(.2);
|
||
sound.tremolo = R()<.5 ? 0 : R(.3);
|
||
break;
|
||
}
|
||
case 'Explosion':
|
||
{
|
||
sound.frequency = R(0,1e3);
|
||
sound.shape = R(5)|0;
|
||
sound.shapeCurve = R(5);
|
||
sound.attack = R(.05);
|
||
sound.decay = R(.2);
|
||
sound.sustain = R(.3);
|
||
sound.sustainVolume = R(.3, .5);
|
||
sound.release = R(.3, .6);
|
||
sound.slide = R()<.5 ? 0 : R();
|
||
sound.deltaSlide = R()<.5 ? 0 : R();
|
||
sound.delay = R()<.5 ? 0 : R(.5);
|
||
sound.noise = R(2);
|
||
sound.modulation = R()<.8 ? 0 : R(-1,1)**3*99;
|
||
sound.bitCrush = R(1,.1);
|
||
sound.repeatTime = R()<.5 ? 0 : R(.2);
|
||
sound.tremolo = R()<.5 ? 0 : R(.5);
|
||
break;
|
||
}
|
||
case 'Music':
|
||
{
|
||
sound.frequency = noteScale[(R(3)|0)*7][0];
|
||
sound.randomness = 0;
|
||
sound.shape = R(4)|0;
|
||
sound.shapeCurve = R(2);
|
||
sound.attack = R()<.5 ? 0 : R(.1);
|
||
sound.decay = R(.2);
|
||
sound.sustain = R(1);
|
||
sound.sustainVolume = R(.2, .5);
|
||
sound.release = R(.05,.5);
|
||
sound.delay = R()<.5 ? 0 : R(.2);
|
||
sound.noise = R()<.3 ? 0 : R(.4);
|
||
sound.bitCrush = R()<.3 ? 0 : R(.1);
|
||
|
||
if (R()<.5)
|
||
{
|
||
// tremolo
|
||
sound.repeatTime = R(.4);
|
||
sound.tremolo = R(.4);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!sound.pitchJumpTime || !sound.pitchJump)
|
||
sound.pitchJumpTime = sound.pitchJump = 0;
|
||
|
||
const length = sound.attack + sound.sustain + sound.delay;
|
||
if (sound.repeatTime > length)
|
||
sound.repeatTime = 0;
|
||
|
||
const Fixed = (v,l=2) =>
|
||
{
|
||
if (v>10 || v < -10)
|
||
l = 0;
|
||
const f = v.toFixed(l);
|
||
return !parseFloat(f) ? '0': f;
|
||
}
|
||
|
||
// convert to fixed point
|
||
if (typeof sound.frequency != 'string')
|
||
sound.frequency = Fixed(sound.frequency,0);
|
||
sound.shapeCurve = Fixed(sound.shapeCurve);
|
||
sound.attack = Fixed(sound.attack);
|
||
sound.sustain = Fixed(sound.sustain);
|
||
sound.release = Fixed(sound.release);
|
||
sound.slide = Fixed(sound.slide,1);
|
||
sound.deltaSlide = Fixed(sound.deltaSlide,1);
|
||
sound.noise = Fixed(sound.noise,1);
|
||
sound.pitchJump = Fixed(sound.pitchJump,0);
|
||
sound.pitchJumpTime = Fixed(sound.pitchJumpTime);
|
||
sound.repeatTime = Fixed(sound.repeatTime, 2);
|
||
sound.modulation = Fixed(sound.modulation,1);
|
||
sound.bitCrush = Fixed(sound.bitCrush,1);
|
||
sound.delay = Fixed(sound.delay);
|
||
sound.sustainVolume = Fixed(sound.sustainVolume);
|
||
sound.decay = Fixed(sound.decay);
|
||
sound.tremolo = Fixed(sound.tremolo);
|
||
|
||
{
|
||
// renormalize sound volume
|
||
const params = SoundToArray(sound);
|
||
const saveVolume = ZZFX.volume;
|
||
ZZFX.volume = 1;
|
||
const samples = ZZFX.buildSamples(...params);
|
||
DrawSoundWave(samples, 1, sound);
|
||
ZZFX.volume = saveVolume;
|
||
|
||
// get max sample
|
||
let maxSample = 0;
|
||
for(let i=0; i<samples.length; i++)
|
||
maxSample = Math.max(maxSample, Math.abs(samples[i]));
|
||
sound.volume = Fixed(1/maxSample);
|
||
|
||
// prevent rounding up causing sound to go above 1
|
||
if (sound.volume > 1)
|
||
sound.volume = Math.max(1, Fixed(sound.volume - .01));
|
||
}
|
||
|
||
// replace with locked
|
||
for(const s of settings)
|
||
if (IsLocked(s.name))
|
||
sound[s.name]=document.getElementById('input_'+s.name).value;
|
||
|
||
sound.name = presetName + ' ' + generatedSoundCount++;
|
||
AddToList(sound);
|
||
LoadSelected();
|
||
UpdateSettings();
|
||
PlaySelected();
|
||
}
|
||
|
||
function CopySelected(mutate=0)
|
||
{
|
||
const sound = Object.assign({}, GetSelectedSound());
|
||
if (!sound.originalName)
|
||
sound.originalName = sound.name;
|
||
|
||
sound.copyCount = !sound.copyCount ? 1 : sound.copyCount + 1;
|
||
sound.name = sound.originalName
|
||
+ ` - ${mutate?'Mutation':'Copy'} `
|
||
+ sound.copyCount;
|
||
delete sound.favorite;
|
||
|
||
AddToList(sound);
|
||
LoadSelected();
|
||
UpdateSettings();
|
||
if (mutate)
|
||
{
|
||
MutateAllSettings();
|
||
sound.modified = 0;
|
||
UpdateSettings();
|
||
}
|
||
PlaySelected();
|
||
}
|
||
|
||
function AddToList(sound)
|
||
{
|
||
// copy default values if missing
|
||
for(const key in defaultSound)
|
||
{
|
||
if (key in sound)
|
||
continue;
|
||
sound[key] = defaultSound[key];
|
||
}
|
||
|
||
if (!(parseInt(sound.shape)<=4))
|
||
sound.shape = 0;
|
||
|
||
const i = sounds.push(sound) - 1;
|
||
const option = document.createElement('option');
|
||
option.value = i;
|
||
option.style.color = sound.favorite? favoriteColor : "inherit";
|
||
option.innerHTML = (sound.favorite?'-> ':sound.modified?'* ':'') + sound.name;
|
||
select_soundList.add(option, 0);
|
||
select_soundList.selectedIndex = 0;
|
||
}
|
||
|
||
function SetFrequencyToNote()
|
||
{
|
||
const eNote = document.getElementById("input_note");
|
||
const v = eNote.value|0;
|
||
|
||
if (v >= 0)
|
||
{
|
||
const eFrequency = document.getElementById("input_frequency");
|
||
eFrequency.value = noteScale[v][0];
|
||
|
||
//const eRandom = document.getElementById("input_randomness");
|
||
//eRandom.value = 0;
|
||
}
|
||
|
||
const sound = GetSelectedSound();
|
||
sound.modified = 1;
|
||
|
||
UpdateSettings();
|
||
PlaySelected();
|
||
}
|
||
|
||
function SelectedWasChanged(settingName)
|
||
{
|
||
const sound = GetSelectedSound();
|
||
sound.modified = 1;
|
||
|
||
UpdateSettings();
|
||
if (settingName != 'name')
|
||
PlaySelected();
|
||
}
|
||
|
||
function LoadSelected(playOnSelect = false)
|
||
{
|
||
const sound = GetSelectedSound();
|
||
if (!sound)
|
||
return;
|
||
for(const s of settings)
|
||
document.getElementById("input_"+s.name).value = sound[s.name];
|
||
|
||
if (loadedSound != sound)
|
||
loadedSoundSeed = Date.now();
|
||
UpdateSettings();
|
||
if (playOnSelect && loadedSound != sound)
|
||
PlaySelected();
|
||
loadedSound = sound;
|
||
}
|
||
|
||
function FavoriteSelected()
|
||
{
|
||
let i = select_soundList.selectedIndex;
|
||
let index = select_soundList.options[i].value;
|
||
let s = sounds[index];
|
||
if (!s)
|
||
return;
|
||
|
||
s.favorite = !s.favorite
|
||
UpdateSettings();
|
||
PlaySelected();
|
||
}
|
||
|
||
function SelectedToTop()
|
||
{
|
||
const selectedSound = GetSelectedSound();
|
||
const sound = Object.assign({}, selectedSound);
|
||
selectedSound.favorite = 0;
|
||
|
||
RemoveSelected();
|
||
AddToList(sound);
|
||
LoadSelected();
|
||
UpdateSettings();
|
||
PlaySelected();
|
||
}
|
||
|
||
function RemoveSelected()
|
||
{
|
||
let i = select_soundList.selectedIndex;
|
||
let index = select_soundList.options[i].value;
|
||
|
||
if (sounds[index].favorite)
|
||
{
|
||
if (!confirm('Are you sure you want remove this favorited sound?'))
|
||
return;
|
||
}
|
||
|
||
delete sounds[index];
|
||
select_soundList.options.remove(i);
|
||
|
||
AddDefaultIfEmpty();
|
||
i = Math.min(i, select_soundList.length-1);
|
||
select_soundList.selectedIndex = i;
|
||
LoadSelected(1);
|
||
}
|
||
|
||
function IsLocked(name)
|
||
{
|
||
const lock = document.getElementById("input_lock_"+name);
|
||
return lock && lock.checked;
|
||
}
|
||
|
||
function MutateAllSettings()
|
||
{
|
||
const sound = GetSelectedSound();
|
||
if (sound.favorite)
|
||
{
|
||
if (!confirm('Are you sure you want mutate this favorite sound?'))
|
||
return false;
|
||
}
|
||
|
||
let mutateCount = 0;
|
||
for(const s of settings)
|
||
if (s.canMutate && !IsLocked(s.name))
|
||
++mutateCount;
|
||
|
||
for(let i=6;i--;)
|
||
{
|
||
if (mutateCount)
|
||
{
|
||
let mutateIndex = (Math.random()*1e5)%mutateCount|0;
|
||
for(const s of settings)
|
||
if (s.canMutate && !IsLocked(s.name) && !mutateIndex--)
|
||
MutateSetting(s, 0);
|
||
}
|
||
}
|
||
|
||
UpdateSettings();
|
||
return true;
|
||
}
|
||
|
||
function MutateSetting(setting, refresh = true)
|
||
{
|
||
const step = setting.step;
|
||
const name = setting.name;
|
||
if (IsLocked(name)) // unlock
|
||
document.getElementById("input_lock_"+name).checked = 0;
|
||
|
||
const sound = GetSelectedSound();
|
||
sound.modified = 1;
|
||
|
||
let v = sound[name];
|
||
if (setting.type == SETTING_TYPE_SHAPE)
|
||
v = Math.random()*6|0;
|
||
else
|
||
{
|
||
let r = (Math.random())*(refresh?10:2)|0
|
||
if (refresh)
|
||
r += 1;
|
||
if (Math.random() > .5)
|
||
r *= -1;
|
||
v += r * step;
|
||
v = parseFloat(v.toFixed(2));
|
||
}
|
||
|
||
document.getElementById('input_'+name).value = v;
|
||
sound[setting.name] = v;
|
||
|
||
if (refresh)
|
||
UpdateSettings();
|
||
}
|
||
|
||
function ResetAllSettings()
|
||
{
|
||
const sound = GetSelectedSound();
|
||
if (sound.favorite)
|
||
{
|
||
if (!confirm('Are you sure you want reset this favorite sound?'))
|
||
return false;
|
||
}
|
||
|
||
for(const s of settings)
|
||
if (!IsLocked(s.name))
|
||
ResetSetting(s, 0);
|
||
|
||
UpdateSettings();
|
||
return true;
|
||
}
|
||
|
||
function ResetSetting(setting, refresh = true)
|
||
{
|
||
const name = setting.name;
|
||
if (IsLocked(name)) // unlock
|
||
document.getElementById("input_lock_"+name).checked = 0;
|
||
|
||
const sound = GetSelectedSound();
|
||
sound.modified = 1;
|
||
|
||
let v = sound[name];
|
||
if (setting.name in defaultSound)
|
||
v = defaultSound[setting.name];
|
||
|
||
document.getElementById('input_'+name).value = v;
|
||
sound[setting.name] = v;
|
||
|
||
if (refresh)
|
||
UpdateSettings();
|
||
}
|
||
|
||
function ToggleLockAll()
|
||
{
|
||
let isLocked = 0;
|
||
for(const s of settings)
|
||
isLocked |= IsLocked(s.name);
|
||
|
||
for(const s of settings)
|
||
{
|
||
const lock = document.getElementById("input_lock_"+s.name);
|
||
if (lock)
|
||
lock.checked = !isLocked;
|
||
}
|
||
|
||
UpdateSettings();
|
||
PlaySelected();
|
||
}
|
||
|
||
function ClearSounds(clearFavorites)
|
||
{
|
||
canvas_soundWave.width |= 0;
|
||
select_soundList.selectedIndex=-1;
|
||
while(select_soundList.options.length>0)
|
||
select_soundList.options.remove(0);
|
||
BuildSettingsTable();
|
||
|
||
let oldSounds = sounds;
|
||
sounds = [];
|
||
if (!clearFavorites)
|
||
oldSounds.map(s=>s && s.favorite && AddToList(s));
|
||
LoadSelected();
|
||
UpdateSettings();
|
||
if (clearFavorites)
|
||
generatedSoundCount = 0;
|
||
}
|
||
|
||
function ClearSoundsButton(clearFavorites)
|
||
{
|
||
if (clearFavorites)
|
||
{
|
||
if (!confirm('Are you sure you want clear all sounds?\nThis will DELETE your favorites!'))
|
||
return;
|
||
slider_masterVolume.value = 25;
|
||
}
|
||
ClearSounds(clearFavorites);
|
||
AddDefaultIfEmpty();
|
||
UpdateSettings();
|
||
PlaySelected();
|
||
}
|
||
|
||
function AddDefaultIfEmpty()
|
||
{
|
||
if (select_soundList.length > 0)
|
||
return;
|
||
|
||
const sound = BuildSound();
|
||
sound.name = 'Sound Default';
|
||
AddToList(sound);
|
||
LoadSelected();
|
||
UpdateSettings();
|
||
}
|
||
|
||
function CopyToCliboard(text)
|
||
{
|
||
if (!navigator.clipboard)
|
||
{
|
||
alert('Unable to copy to clipboard.');
|
||
return false;
|
||
}
|
||
navigator.clipboard.writeText(text);
|
||
return true;
|
||
}
|
||
|
||
function GetCode(sound)
|
||
{
|
||
// create code string
|
||
const parameters = SoundToArray(sound);
|
||
|
||
// remove defaults
|
||
const littleJS = input_codeStyleLittleJS.checked;
|
||
const shorten = littleJS || input_codeStyleCompact.checked;
|
||
const defaults = SoundToArray(BuildSound());
|
||
let isEnd = 1;
|
||
for(let i = parameters.length-1; shorten&&i>=0; --i)
|
||
{
|
||
if (parameters[i] == defaults[i])
|
||
{
|
||
if (isEnd)
|
||
--parameters.length;
|
||
else
|
||
parameters[i] = '';
|
||
}
|
||
else
|
||
isEnd = 0;
|
||
}
|
||
|
||
// make parameters list
|
||
let code = '';
|
||
for(let i = 0; i < parameters.length; ++i)
|
||
{
|
||
let p = parameters[i].toString();
|
||
// remove leading 0
|
||
if (p.slice(0,2) == '0.')
|
||
p = p.slice(1);
|
||
|
||
if (!settings[i].type)
|
||
{
|
||
let e = parseFloat(parameters[i]).toExponential();
|
||
e = e.replace('+','');
|
||
|
||
if (e.length < p.length)
|
||
p = e;
|
||
}
|
||
|
||
code += (i?',':'') + p;
|
||
}
|
||
|
||
if (!code.length && !littleJS)
|
||
return `zzfx(${code}); // ${sound.name.trim()}`;
|
||
else if (shorten)
|
||
{
|
||
if (littleJS)
|
||
return `new Sound([${code}]); // ${sound.name.trim()}`;
|
||
|
||
return `zzfx(...[${code}]); // ${sound.name.trim()}`;
|
||
}
|
||
else
|
||
return `zzfx(${code}); // ${sound.name.trim()}`;
|
||
}
|
||
|
||
function LoadSound()
|
||
{
|
||
let code = prompt('Enter zzfx code.');
|
||
if (!code)
|
||
return;
|
||
|
||
code = code.split('[')[1];
|
||
if (!code)
|
||
{
|
||
alert('Error! Invalid zzfx code.');
|
||
return;
|
||
}
|
||
|
||
// build sound
|
||
const paramArray = code.split(',').map(p=>p.length? parseFloat(p) : undefined);
|
||
const sound = BuildSound(...paramArray);
|
||
const name = code.split('//')[1];
|
||
sound.name = name || ('Loaded Sound ' + generatedSoundCount++);
|
||
sound.name = sound.name.trim();
|
||
AddToList(sound);
|
||
LoadSelected();
|
||
UpdateSettings();
|
||
PlaySelected();
|
||
}
|
||
|
||
function SaveWave()
|
||
{
|
||
PlaySelected();
|
||
|
||
const sound = BuildSoundFromSettings();
|
||
a_downloadLink.href = BuildWaveDataUrl(sound);
|
||
name = sound.name.trim();
|
||
if (!name.length)
|
||
name = 'zzfx';
|
||
a_downloadLink.download = name + ".wav";
|
||
a_downloadLink.click();
|
||
}
|
||
|
||
function BuildWaveDataUrl(sound)
|
||
{
|
||
// build samples with fixed volume
|
||
const v = ZZFX.volume;
|
||
ZZFX.volume = .5;
|
||
const samples = ZZFX.buildSamples(...SoundToArray(sound));
|
||
ZZFX.volume = v;
|
||
|
||
const URLObject = webkitURL || URL;
|
||
return URLObject.createObjectURL(buildWavBlob([samples], ZZFX.sampleRate));
|
||
}
|
||
|
||
function DrawSoundWave(b, volume, sound)
|
||
{
|
||
// set up canvas
|
||
const x = canvas_soundWave.getContext('2d');
|
||
const w = canvas_soundWave.width;
|
||
const h = canvas_soundWave.height;
|
||
canvas_soundWave.width |= 0;
|
||
randomSeed = loadedSoundSeed;
|
||
const R =()=> SeededRandom();
|
||
|
||
if (!volume)
|
||
return;
|
||
|
||
// show attack, sustain, release
|
||
let length = b.length;
|
||
let X, W;
|
||
let H=R()*3e3;
|
||
x.fillStyle=`hsl(${H+45},79%,10%)`;
|
||
x.fillRect(0,0,w,h);
|
||
if (!length)
|
||
return;
|
||
x.fillStyle=`hsl(${H+=30},79%,10%)`;
|
||
x.fillRect(X=0,0,W=w*sound.attack * ZZFX.sampleRate/b.length,h);
|
||
x.fillStyle=`hsl(${H+=30},79%,10%)`;
|
||
x.fillRect(X+=W,0,W=w*sound.decay * ZZFX.sampleRate/b.length,h);
|
||
x.fillStyle=`hsl(${H+=30},79%,10%)`;
|
||
x.fillRect(X+=W,0,W=w*sound.sustain * ZZFX.sampleRate/b.length,h);
|
||
x.fillStyle=`hsl(${H+=30},79%,10%)`;
|
||
x.fillRect(X+=W,0,W=w*sound.release * ZZFX.sampleRate/b.length,h);
|
||
x.strokeStyle=`#FFF`;
|
||
x.strokeRect(X=0,0,W=w*sound.attack * ZZFX.sampleRate/b.length,h);
|
||
x.strokeRect(X+=W,0,W=w*sound.decay * ZZFX.sampleRate/b.length,h);
|
||
x.strokeRect(X+=W,0,W=w*sound.sustain * ZZFX.sampleRate/b.length,h);
|
||
x.strokeRect(X+=W,0,W=w*sound.release * ZZFX.sampleRate/b.length,h);
|
||
|
||
// draw the wave
|
||
x.strokeStyle=`hsl(${H+180},${R()*99}%,70%)`;
|
||
x.beginPath();
|
||
for(let i=0; i<b.length; i+=10)
|
||
x.lineTo(w*i/b.length, .8*b[i]*h/2/volume+h/2);
|
||
x.stroke();
|
||
}
|
||
|
||
function RandomizeLogo()
|
||
{
|
||
const R =()=> Math.random();
|
||
div_logo.style.color=`hsl(${R()*3e3},${50+50*R()}%,${50+50*R()}%)`;
|
||
div_logo2.style.color=`hsl(${R()*3e3},${50+50*R()}%,${50+50*R()}%)`;
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// save & load
|
||
|
||
function BuildSaveData()
|
||
{
|
||
const volume = slider_masterVolume.value;
|
||
const codeStyle =
|
||
input_codeStyleCompact.checked ? 'compact' :
|
||
input_codeStyleLittleJS.checked ? 'littlejs' : 'full';
|
||
|
||
const data =
|
||
{
|
||
sounds,
|
||
generatedSoundCount,
|
||
volume,
|
||
codeStyle
|
||
}
|
||
|
||
return JSON.stringify(data);
|
||
}
|
||
|
||
function LoadSaveData(dataJSON)
|
||
{
|
||
if (!dataJSON)
|
||
{
|
||
AddDefaultIfEmpty();
|
||
return;
|
||
}
|
||
|
||
const data = JSON.parse(dataJSON);
|
||
if (data.generatedSoundCount)
|
||
generatedSoundCount = data.generatedSoundCount;
|
||
if (data.sounds)
|
||
data.sounds.forEach(o=>o&&AddToList(o));
|
||
|
||
// settings
|
||
if (data.volume)
|
||
slider_masterVolume.value = data.volume;
|
||
if (data.codeStyle)
|
||
{
|
||
if (data.codeStyle == 'full')
|
||
input_codeStyleFull.checked = 1;
|
||
if (data.codeStyle == 'compact')
|
||
input_codeStyleCompact.checked = 1;
|
||
if (data.codeStyle == 'littlejs')
|
||
input_codeStyleLittleJS.checked = 1;
|
||
}
|
||
|
||
AddDefaultIfEmpty();
|
||
select_soundList.selectedIndex = 0;
|
||
LoadSelected();
|
||
UpdateSettings();
|
||
}
|
||
|
||
function SaveLocalStorage()
|
||
{
|
||
localStorage.saveData = BuildSaveData();
|
||
}
|
||
|
||
function LoadLocalStorage()
|
||
{
|
||
//ClearSounds(1);
|
||
LoadSaveData(localStorage.saveData);
|
||
}
|
||
|
||
function Export()
|
||
{
|
||
const fileData = BuildSaveData();
|
||
a_downloadLink.download = "zzfx_sounds.txt";
|
||
a_downloadLink.href='data:application/octet-stream;charset=UTF-8,' + encodeURIComponent(fileData);
|
||
a_downloadLink.click();
|
||
PlaySelected();
|
||
}
|
||
|
||
function Import()
|
||
{
|
||
input_importFile.onchange=e=>LoadFile(e.target.files[0]);
|
||
input_importFile.click();
|
||
}
|
||
|
||
function LoadFile(file)
|
||
{
|
||
input_importFile.onchange=e=>0;
|
||
input_importFile.value='';
|
||
|
||
if (file.type != "text/plain")
|
||
{
|
||
alert('Error! File type mismatch.');
|
||
return;
|
||
}
|
||
|
||
const reader = new FileReader();
|
||
reader.readAsText(file,'UTF-8');
|
||
reader.onload=readerEvent=>
|
||
{
|
||
const fileData = readerEvent.target.result;
|
||
LoadSaveData(fileData);
|
||
PlaySelected();
|
||
}
|
||
}
|
||
|
||
///////////////////////////////////////////////////////////////////////////////
|
||
// shortcuts
|
||
|
||
onkeydown =e=>
|
||
{
|
||
if (e.ctrlKey || e.altKey)
|
||
return;
|
||
|
||
const selected = document.activeElement;
|
||
if (selected.tagName != 'INPUT' && selected.tagName != 'TEXTAREA')
|
||
{
|
||
if (e.keyCode == 32 && selected.tagName != 'BUTTON') // space
|
||
PlaySelected();
|
||
if (e.keyCode == 45) // ins
|
||
AddPresetSound();
|
||
if (e.keyCode == 46) // del
|
||
RemoveSelected();
|
||
if (e.keyCode == 70) // f
|
||
FavoriteSelected();
|
||
if (e.keyCode == 67) // c
|
||
CopySelected();
|
||
if (e.keyCode == 77) // m
|
||
CopySelected(1);
|
||
}
|
||
}
|
||
|
||
// click on canvas to play sound
|
||
canvas_soundWave.onmousedown =e=> {loadedSound=0;LoadSelected(1);e.preventDefault();}
|
||
|
||
// drag and drop exported files into the sound list
|
||
select_soundList.addEventListener('dragover', e=>
|
||
{
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = 'copy';
|
||
select_soundList.style.background = '#800';
|
||
});
|
||
select_soundList.addEventListener('dragleave', e=>
|
||
{
|
||
select_soundList.style.background = '';
|
||
});
|
||
select_soundList.addEventListener('drop', e=>
|
||
{
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
LoadFile(e.dataTransfer.files[0]);
|
||
select_soundList.style.background = '';
|
||
});
|
||
|
||
</script>
|
||
<a href="https://github.com/KilledByAPixel/ZzFX" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#5AF; color:#222; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
|
||
</div>
|
||
</body>
|
||
</html> |