Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added checkbox-checked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added checkbox-unchecked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 49 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<title>AutoTracker</title>
<!-- <script type="text/javascript" src="dist/tracker.js"></script>-->
<script type="module" src="target/tracker.js"></script>
<!-- <link rel="icon" type="image/png" href="images/icon.png">-->
<link rel="shortcut icon" type="image/png" href="icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="AutoTracker" />
<meta property="og:type" content="website" />
Expand Down Expand Up @@ -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;
}
</style>
</head>
<body>
Expand Down
Binary file added regenerate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 27 additions & 7 deletions src/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
import {fill, rnd} from './utils.js'

type Synth<T> = { play: (note: T) => void}
export type Synth<T> = { play: (note: T) => void, mute: (muted: boolean) => void}

const A3Frequency = 440;
const A0Frequency = A3Frequency / 8;
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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<Drum> {
Expand All @@ -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<Drum>, 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);
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 24 additions & 2 deletions src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -31,19 +33,39 @@ function textRepr(slot: Slot) {
}

function PatternDisplay(display: HTMLElement) {
const regenerateCheckbox = document.createElement("input");

function setPatterns(newPats: Pattern<Slot>[], saveString: string) {
display.innerHTML = "<div class='header'>Pattern ID: <a href='?" + saveString + "' class='save-string'>" + saveString + "</a></div>";
display.innerHTML =
`<div class='header'>
Pattern ID: <a href='?${saveString}' class='save-string'>${saveString}</a>
<input type="checkbox" ${settings.regenerateEnabled ? 'checked' : ''} id="regenerateEnabled"/>
<label for="regenerateEnabled"></label><button type="button" id="forceGenerate"></button>
</div>`;
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<Slot>, index: number) {
const pDisplay = document.createElement("code");
pDisplay.innerHTML =
"<h3>" + (index === 4 ? "*" : "⎍") + (index + 1) + "</h3>" +
`<h3 id="patternHeader${index}" class="${settings.muted[index] ? 'muted': ''}">
${index === 4 ? "*" : "&#x238D;"} ${index + 1}
</h3>` +
pattern.map((x, i) => "<div class='note' data-index='" + i + "'>" + textRepr(x) + "</div>").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))
}
Expand Down
2 changes: 1 addition & 1 deletion src/generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function bass(context: MusicContext): Pattern<Note> {
}
function bass2(context: MusicContext): Pattern<Note> { 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<Note> {
const slow = flip();
Expand Down
54 changes: 40 additions & 14 deletions src/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -26,12 +26,11 @@ const progressions = [
[1,1,1,1,1,1,1,1,4,4,4,4,4,4,4,4]
];

type Synth<T> = { play: (note: T) => void}
type FourChannelsPlusDrums = [Note, Note, Note, Note, Drum]
type PatternsType<T> = { [K in keyof T]: Pattern<T[K]> };
type SynthsType<T> = { [K in keyof T]: Synth<T[K]> }


export let synths: SynthsType<FourChannelsPlusDrums>

interface State {
key: Key,
Expand All @@ -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'); }
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -139,7 +156,7 @@ function start() {
const ctx: AudioContext = new (window.AudioContext || window.webkitAudioContext)() as AudioContext;
const au = Audio(ctx);

const synths: SynthsType<FourChannelsPlusDrums> = [
synths = [
au.SquareSynth(),
au.SquareSynth(-0.5),
au.SquareSynth(),
Expand All @@ -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;
}
}

}

Expand Down