diff --git a/checkbox-checked.png b/checkbox-checked.png
new file mode 100644
index 0000000..9888bdd
Binary files /dev/null and b/checkbox-checked.png differ
diff --git a/checkbox-unchecked.png b/checkbox-unchecked.png
new file mode 100644
index 0000000..0a6d8fe
Binary files /dev/null and b/checkbox-unchecked.png differ
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..1c0c734
Binary files /dev/null and b/icon.png differ
diff --git a/index.html b/index.html
index 810636a..71f8104 100644
--- a/index.html
+++ b/index.html
@@ -8,7 +8,7 @@
AutoTracker
-
+
@@ -147,6 +147,54 @@
margin: 0;
}
+ @media (min-width: 600px) {
+ .muted:after {
+ content: " MUTE";
+ font-weight: normal;
+ }
+ }
+
+ @media (max-width: 599px) {
+ .muted:after {
+ content: " M";
+ font-weight: normal;
+ }
+ }
+
+ h3 {
+ cursor: default;
+ }
+
+ input#regenerateEnabled[type=checkbox] {
+ display:none;
+ }
+
+ input#regenerateEnabled[type=checkbox] + label {
+ margin-left: 8px;
+ display: inline-block;
+ padding: 0 0 0 0px;
+ background: url("checkbox-unchecked.png") no-repeat;
+ height: 32px;
+ width: 32px;
+ background-size: 80%;
+ vertical-align: middle;
+ }
+
+ input#regenerateEnabled[type=checkbox]:checked + label {
+ background: url("checkbox-checked.png") no-repeat;
+ background-size: 80%;
+ }
+
+ button#forceGenerate {
+ cursor: default;
+ padding: 0;
+ border: none;
+ background: url("regenerate.png") no-repeat;
+ width: 32px;
+ height: 32px;
+ background-size: 80%;
+ vertical-align: middle;
+ }
diff --git a/regenerate.png b/regenerate.png
new file mode 100644
index 0000000..df21b61
Binary files /dev/null and b/regenerate.png differ
diff --git a/src/audio.ts b/src/audio.ts
index c2680be..b167165 100644
--- a/src/audio.ts
+++ b/src/audio.ts
@@ -5,7 +5,7 @@
*/
import {fill, rnd} from './utils.js'
-type Synth = { play: (note: T) => void}
+export type Synth = { play: (note: T) => void, mute: (muted: boolean) => void}
const A3Frequency = 440;
const A0Frequency = A3Frequency / 8;
@@ -63,6 +63,8 @@ function Audio(ctx: AudioContext) {
pulseOutputGain.connect(outputPanner);
outputPanner.connect(ctx.destination);
+ let muted = false;
+
const freq = wavetableTrigger.frequency,
width = wavetableOffsetGain.gain,
gain = pulseOutputGain.gain;
@@ -79,7 +81,7 @@ function Audio(ctx: AudioContext) {
slide(gain, 0, release);
}
function play(note: Note) {
- if (note.note === "---") {
+ if (muted || note.note === "---") {
noteOff();
} else if (note.note === 'cont') {
// do nothing
@@ -89,7 +91,11 @@ function Audio(ctx: AudioContext) {
set(width, note.fx?.pulseWidth ?? 0.0);
}
- return {play}
+ function mute(_muted: boolean) {
+ muted = _muted;
+ }
+
+ return {play, mute}
}
function DrumSynth(): Synth {
@@ -111,10 +117,21 @@ function Audio(ctx: AudioContext) {
noiseGain.connect(noisePan);
noisePan.connect(ctx.destination);
+ let muted = false;
+
+ function drumOff() {
+ toneGain.gain.cancelScheduledValues(ctx.currentTime);
+ toneGain.gain.setValueAtTime(0, ctx.currentTime);
+ noiseGain.gain.cancelScheduledValues(ctx.currentTime);
+ noiseGain.gain.setValueAtTime(0, ctx.currentTime);
+ }
- function play(slot: Drum) {
+ function play(this: Synth, slot: Drum) {
const vel = slot.vel ? slot.vel : 1;
- if (slot.drum === 'KCK') {
+
+ if (muted) {
+ drumOff()
+ } else if (slot.drum === 'KCK') {
toneOscillator.detune.cancelScheduledValues(ctx.currentTime);
toneOscillator.detune.setValueAtTime(3000, ctx.currentTime);
toneOscillator.detune.setTargetAtTime(0, ctx.currentTime, 0.07);
@@ -143,9 +160,12 @@ function Audio(ctx: AudioContext) {
noiseGain.gain.setValueCurveAtTime(new Float32Array([0.2 * vel,0.15 * vel,0.0]), ctx.currentTime, 0.15);
}
}
- return {
- play,
+
+ function mute(_muted: boolean) {
+ muted = _muted;
}
+
+ return {play, mute}
}
return {
SquareSynth,
diff --git a/src/display.ts b/src/display.ts
index 62a6aaf..436dfef 100644
--- a/src/display.ts
+++ b/src/display.ts
@@ -3,6 +3,8 @@
This work is licensed under a Creative Commons Attribution 4.0 International License
https://creativecommons.org/licenses/by/4.0/
*/
+import { settings, synths } from "./tracker.js";
+
const A0 = -12;
function textRepr(slot: Slot) {
@@ -31,19 +33,39 @@ function textRepr(slot: Slot) {
}
function PatternDisplay(display: HTMLElement) {
+ const regenerateCheckbox = document.createElement("input");
function setPatterns(newPats: Pattern[], saveString: string) {
- display.innerHTML = "";
+ display.innerHTML =
+ ``;
+ document.getElementById("regenerateEnabled")?.addEventListener("input", e =>
+ settings.regenerateEnabled = (e.target as HTMLInputElement).checked
+ );
+ document.getElementById("forceGenerate")?.addEventListener("click", e =>
+ settings.forceGenerate = true
+ );
const container = document.createElement("div");
container.classList.add("columns");
display.append(container);
function add(pattern: Pattern, index: number) {
const pDisplay = document.createElement("code");
pDisplay.innerHTML =
- "" + (index === 4 ? "*" : "⎍") + (index + 1) + "
" +
+ `` +
pattern.map((x, i) => "" + textRepr(x) + "
").join("");
container.append(pDisplay);
+
+ document.getElementById(`patternHeader${index}`)?.addEventListener("click", e => {
+ settings.muted[index] = !settings.muted[index];
+ synths[index].mute(settings.muted[index]);
+ (e.target as HTMLElement).classList.toggle("muted", settings.muted[index]);
+ });
}
newPats.forEach((p, i) => add(p, i))
}
diff --git a/src/generators.ts b/src/generators.ts
index 824420f..9657308 100644
--- a/src/generators.ts
+++ b/src/generators.ts
@@ -46,7 +46,7 @@ function bass(context: MusicContext): Pattern {
}
function bass2(context: MusicContext): Pattern { return fill(PatternSize, i => {
const chord = getChord(context, i);
- return {note: i % 8 === 0 ? ((chord[0] + 4) % 12) - 4: 'cont', vel: 2, fx: {pulseWidth: rnd()}} as Note;
+ return {note: i % 8 === 0 ? ((chord[0] + 4) % 12) - 4: 'cont', fx: {pulseWidth: rnd()}} as Note;
})}
function melody1(context: MusicContext): Pattern {
const slow = flip();
diff --git a/src/tracker.ts b/src/tracker.ts
index f0ebb6f..e9d1e4a 100644
--- a/src/tracker.ts
+++ b/src/tracker.ts
@@ -7,7 +7,7 @@
import PatternDisplay from './display.js'
import {choose, fill, rndInt, rnd, seedRNG} from './utils.js'
-import Audio from "./audio.js";
+import Audio, { Synth } from "./audio.js";
import * as music from './theory.js'
import * as Generators from './generators.js'
import {scales} from "./theory.js";
@@ -26,12 +26,11 @@ const progressions = [
[1,1,1,1,1,1,1,1,4,4,4,4,4,4,4,4]
];
-type Synth = { play: (note: T) => void}
type FourChannelsPlusDrums = [Note, Note, Note, Note, Drum]
type PatternsType = { [K in keyof T]: Pattern };
type SynthsType = { [K in keyof T]: Synth }
-
+export let synths: SynthsType
interface State {
key: Key,
@@ -42,6 +41,14 @@ interface State {
seedCode: string
}
+class Settings {
+ regenerateEnabled: boolean = true;
+ forceGenerate: boolean = false;
+ muted: boolean[] = [false, false, false, false, false];
+}
+
+export const settings = new Settings()
+
type SaveCode = string & {typeTag: "__SaveCode"}
function hex(v: number) { return Math.floor(v).toString(16).toUpperCase().padStart(2,'0'); }
@@ -75,15 +82,25 @@ function restore(code: SaveCode): State {
function bpmClock() {
let intervalHandle = {
- bpmClock: 0
+ bpmClock: 0,
+ frameFunction: (_: number) => {}
};
let fN = 0;
function set(bpm: number, frameFunction: (f: number) => void) {
window.clearInterval(intervalHandle.bpmClock);
intervalHandle.bpmClock = window.setInterval(() => frameFunction(fN++), (60000 / bpm) / 4);
+ intervalHandle.frameFunction = frameFunction;
+ }
+ function setBpm(bpm: number) {
+ set(bpm, intervalHandle.frameFunction);
+ }
+ function resetFrameNumber() {
+ fN = 0;
}
return {
- set
+ set,
+ setBpm,
+ resetFrameNumber
}
}
@@ -139,7 +156,7 @@ function start() {
const ctx: AudioContext = new (window.AudioContext || window.webkitAudioContext)() as AudioContext;
const au = Audio(ctx);
- const synths: SynthsType = [
+ synths = [
au.SquareSynth(),
au.SquareSynth(-0.5),
au.SquareSynth(),
@@ -166,21 +183,30 @@ function start() {
function frame(f: number) {
const positionInPattern = f % PatternSize;
- if (f % 128 === 0 && f!== 0) {
+ if (settings.forceGenerate || settings.regenerateEnabled && f % 128 === 0 && f!== 0) {
+ settings.forceGenerate = false;
mutateState(state);
newPatterns();
- clock.set(state.bpm, frame);
+ clock.resetFrameNumber();
+ clock.setBpm(state.bpm);
display.setPatterns(patterns, save(state));
}
display.highlightRow(positionInPattern);
- // Not a loop because these tuple parts have different types depending on melody vs drum
- synths[0].play(patterns[0][positionInPattern]);
- synths[1].play(patterns[1][positionInPattern]);
- synths[2].play(patterns[2][positionInPattern]);
- synths[3].play(patterns[3][positionInPattern]);
- synths[4].play(patterns[4][positionInPattern]);
+ try {
+ // Not a loop because these tuple parts have different types depending on melody vs drum
+ synths[0].play(patterns[0][positionInPattern]);
+ synths[1].play(patterns[1][positionInPattern]);
+ synths[2].play(patterns[2][positionInPattern]);
+ synths[3].play(patterns[3][positionInPattern]);
+ synths[4].play(patterns[4][positionInPattern]);
+ } catch (e: any) {
+ // Ignore DOMException: AudioParam.setValueAtTime: Can't add events during a curve event
+ if (e.name !== "NotSupportedError") {
+ throw e;
+ }
+ }
}