Skip to content

Commit

Permalink
Add autoplayer
Browse files Browse the repository at this point in the history
  • Loading branch information
reimu committed Oct 5, 2023
1 parent 975be8b commit 6f7024b
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 93 deletions.
74 changes: 74 additions & 0 deletions src/autoplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { keys } from "./notes.js";

// a4 is the reference note
// -9 is the offset of c3
// we offset by a customizable amount of octaves because too low sounds bad
const a4 = 440;
const initialOffsetOctaves = 0;
const initialOffset = -9 + initialOffsetOctaves * 12;

let audioContext = new AudioContext();

const autoplayEle = document.getElementById("autoplay") as HTMLButtonElement;
const stopEle = document.getElementById("stopplay") as HTMLButtonElement;
const inputEle = document.getElementById("input") as HTMLTextAreaElement;
const delayEle = document.getElementById("delay") as HTMLInputElement;
const errorEle = document.getElementById("error") as HTMLSpanElement;

function play(notes: string, baseDelay: number) {
let delay = 0;
let inChord = false;
for (const note of notes.split("")) {
// chord notation, don't delay but keep track
if (note === "[") {
inChord = true;
continue;
}

if (note === "]") {
inChord = false;
continue;
}

const idx = keys.indexOf(note);
// unknown symbol, probably timing related, delay
if (idx === -1) {
delay += baseDelay;
continue;
}

playNote(idx, delay, baseDelay);
if (!inChord) {
delay += baseDelay;
}
}
}

function playNote(i: number, delay: number, playFor: number) {
const oscillator = audioContext.createOscillator();
oscillator.type = "sine";
oscillator.connect(audioContext.destination);
oscillator.frequency.value = 2 ** ((initialOffset + i) / 12) * a4;
oscillator.start(audioContext.currentTime + delay / 1000);
oscillator.stop(audioContext.currentTime + (delay + playFor) / 1000);
}

autoplayEle.addEventListener("click", () => {
errorEle.innerText = "";
try {
const notes = inputEle.value;
const delay = parseInt(delayEle.value, 10);
if (isNaN(delay)) {
throw new Error(`Invalid delay: ${delayEle.value}`);
}
play(notes, delay);
} catch (e) {
console.error(e);
errorEle.innerText = String(e);
}
});

stopEle.addEventListener("click", () => {
audioContext.close();
audioContext = new AudioContext();
});
48 changes: 34 additions & 14 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,38 @@
<link rel="stylesheet" href="style.css" />
</head>
<body>
<section id="container">
<section id="toolbar">
<button id="transpose">Transpose</button>
<label>
Halfsteps to shift by
<input id="shift" type="number" value="0" />
</label>
<label>
Autoshift if outside of range
<input id="autoshift" type="checkbox" />
</label>
<span id="error"> </span>
<section class="container">
<section class="h-container">
<section class="toolbar">
<label title="Number of halfsteps to transpose by">
Halfsteps
<input id="shift" type="number" value="0" />
</label>
<label title="Shift note by octaves if outside of range">
Autoshift
<input id="autoshift" type="checkbox" />
</label>
<button id="transpose" title="Transpose">Transpose</button>
</section>
<section class="toolbar">
<label title="Base for consecutive non-chord notes">
Base delay
<input id="delay" type="number" value="100" />
</label>
<p
>Any non-note character except <br />for [ and ] will
add delay between notes</p
>
<button id="autoplay" title="Autoplay output"
>Autoplay</button
>
<button id="stopplay" title="Stop autoplaying">
Stop</button
>
</section>
</section>
<section id="notes">
<span id="error"> </span>
<section class="notes-container">
<textarea
class="notes"
id="input"
Expand All @@ -35,6 +53,8 @@
</section>
</section>

<script type="module" src="main.js"></script>
<script type="module" src="notes.js"></script>
<script type="module" src="transpose.js"></script>
<script type="module" src="autoplay.js"></script>
</body>
</html>
64 changes: 64 additions & 0 deletions src/notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export const keys = [
"1",
"!",
"2",
"@",
"3",
"4",
"$",
"5",
"%",
"6",
"^",
"7",
"8",
"*",
"9",
"(",
"0",
"q",
"Q",
"w",
"W",
"e",
"E",
"r",
"t",
"T",
"y",
"Y",
"u",
"i",
"I",
"o",
"O",
"p",
"P",
"a",
"s",
"S",
"d",
"D",
"f",
"g",
"G",
"h",
"H",
"j",
"J",
"k",
"l",
"L",
"z",
"Z",
"x",
"c",
"C",
"v",
"V",
"b",
"B",
"n",
"m",
"M",
];
27 changes: 20 additions & 7 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,39 @@ section {
gap: 12px;
}

#container {
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}

.notes {
flex: 50%;
.h-container {
display: flex;
flex-direction: row;
}

#toolbar {
display: flex;
.toolbar {
border: 1px solid black;
padding: 12px;
}

.toolbar > label {
display: block;
}

#notes {
.toolbar > *:not(:last-child) {
margin-bottom: 12px;
}

.notes-container {
display: flex;
flex: 1;
}

.notes {
flex: 50%;
}

#error {
color: var(--red-1);
}
79 changes: 7 additions & 72 deletions src/main.ts → src/transpose.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,4 @@
const keys = [
"1",
"!",
"2",
"@",
"3",
"4",
"$",
"5",
"%",
"6",
"^",
"7",
"8",
"*",
"9",
"(",
"0",
"q",
"Q",
"w",
"W",
"e",
"E",
"r",
"t",
"T",
"y",
"Y",
"u",
"i",
"I",
"o",
"O",
"p",
"P",
"a",
"s",
"S",
"d",
"D",
"f",
"g",
"G",
"h",
"H",
"j",
"J",
"k",
"l",
"L",
"z",
"Z",
"x",
"c",
"C",
"v",
"V",
"b",
"B",
"n",
"m",
"M",
];
import { keys } from "./notes.js";

const transposeEle = document.getElementById("transpose") as HTMLButtonElement;
const inputEle = document.getElementById("input") as HTMLTextAreaElement;
Expand All @@ -71,7 +8,6 @@ const errorEle = document.getElementById("error") as HTMLSpanElement;
const autoshiftEle = document.getElementById("autoshift") as HTMLInputElement;

function t(notes: string, steps: number): string {
// TODO: handle extended keys
const transposedNotes = notes
.split("")
.map((key, i) => {
Expand All @@ -83,20 +19,19 @@ function t(notes: string, steps: number): string {
`, at position: ${i};${notes.slice(i - 5, i + 5)}`
);
else return key; // probably a non-note symbol

let newPos = pos + steps;
if (newPos < 0 || newPos >= keys.length) {
if (!autoshiftEle.checked)
throw new Error(
`detected key outside of range: ${key}` +
`, at position: ${i};${notes.slice(i - 5, i + 5)}`
);
if (!autoshiftEle.checked) return "?";
else {
// must be able to divide somehow
// possible to create duplicates in chords
while (newPos < 0 || newPos >= keys.length) {
newPos += 12 * (steps > 0 ? -1 : 1);
}
}
}

return keys[newPos];
})
.join("");
Expand All @@ -106,8 +41,8 @@ function t(notes: string, steps: number): string {
/\[(.*?)\]/g,
(_, letters: string) =>
"[" +
letters
.split("")
// deduplicate things in chords that may be introduced by autoshift
Array.from(new Set(letters.split("")))
.sort((a, b) => {
// symbol or upper
if (!/[0-9]/.test(a) && a.toUpperCase() === a) {
Expand Down

0 comments on commit 6f7024b

Please sign in to comment.