Files
topos/node_modules/zzfx/index.html

1387 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
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 + '&nbsp;</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 + '&nbsp;';
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>