-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
2,216 additions
and
9 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,5 +4,8 @@ | |
}, | ||
"scripts": { | ||
"test": "jest" | ||
}, | ||
"dependencies": { | ||
"local-web-server": "^5.3.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
// curve generation code | ||
|
||
export { generateCurve }; | ||
|
||
import { Scale } from "./scl.js"; | ||
import { Keymap } from "./kbm.js"; | ||
|
||
const TOTAL_KEYS = 128; | ||
const C5_UNITS = 0x7c00; | ||
const C5_KEY = 60; | ||
const CENTS_PER_SEMITONE = 100; | ||
const UNITS_PER_SEMITONE = 0x100; | ||
const MIN_UNITS = 0; | ||
const MAX_UNITS = 0xffff; | ||
|
||
function generateCurve(scale: Scale, keymap: Keymap, offset: number): Uint8Array { | ||
const buf = new Uint8Array(TOTAL_KEYS * 2); | ||
for (let key = 0; key < TOTAL_KEYS; key++) { | ||
const units = (key >= keymap.firstNote && key <= keymap.lastNote) ? | ||
calcPitch(scale, keymap, key, offset) : | ||
C5_UNITS + (key - C5_KEY) * UNITS_PER_SEMITONE; | ||
buf[key * 2] = units & 0xff; | ||
buf[key * 2 + 1] = units >> 8; | ||
} | ||
return buf; | ||
} | ||
|
||
function modulo(x: number, y: number): number { | ||
let n = x % y; | ||
while (n < 0) { | ||
n += y; | ||
} | ||
return n; | ||
} | ||
|
||
function calcPitch( | ||
scale: Scale, keymap: Keymap, key: number, centsOffset: number): number { | ||
// sorry for all the increment/decrement nonsense | ||
const offset = key - 1 - keymap.middleNote; | ||
const octave = Math.floor(offset / keymap.size); | ||
let mapIndex = modulo(offset + 1, keymap.size); | ||
const scaleIndex = keymap.mapping[mapIndex]; | ||
const units = scaleIndex === null ? | ||
MIN_UNITS : | ||
C5_UNITS + Math.round( | ||
(scale.notes[modulo(scaleIndex - 1, scale.notes.length)] + | ||
octave * scale.notes[scale.notes.length - 1] + | ||
centsOffset) * | ||
UNITS_PER_SEMITONE / CENTS_PER_SEMITONE | ||
); | ||
return Math.max(MIN_UNITS, Math.min(MAX_UNITS, units)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
<!DOCTYPE html> | ||
<html lang="en-US"> | ||
|
||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width"> | ||
<meta name="description" content="Convert Scala scales and keyboard maps to SunVox pitch curves"> | ||
<title>Scala to SunVox</title> | ||
<link rel="stylesheet" href="/normalize.css"> | ||
<link rel="stylesheet" href="/styles.css"> | ||
<script src="script.js" type="module" defer></script> | ||
</head> | ||
|
||
<body> | ||
|
||
<header> | ||
<h1>Scala to SunVox</h1> | ||
</header> | ||
|
||
<main> | ||
|
||
<section> | ||
<p> | ||
This webpage converts | ||
<a href="https://www.huygens-fokker.org/scala/">Scala</a> scales and | ||
keyboard maps to <a href="https://warmplace.ru/soft/sunvox/">SunVox</a> | ||
pitch curves. | ||
</p> | ||
</section> | ||
|
||
<section> | ||
<label for="sclInput">Select .scl:</label> | ||
<input type="file" id="sclInput" accept=".scl"> | ||
</section> | ||
|
||
<section> | ||
<label for="kbmInput">Select .kbm (optional):</label> | ||
<input type="file" id="kbmInput" accept=".kbm"> | ||
</section> | ||
|
||
<section> | ||
<label for="centsInput">Cents offset:</label> | ||
<input type="number" id="centsInput" value="0"> | ||
</section> | ||
|
||
<section> | ||
<button id="convertButton">Convert to .curve16bit</button> | ||
<p id="messageArea"></p> | ||
</section> | ||
|
||
</main> | ||
|
||
<footer> | ||
<ul> | ||
<li><a href="/">Index</a></li> | ||
<li><a href="https://github.com/jangler/tuning">GitHub</a></li> | ||
</ul> | ||
</footer> | ||
|
||
</body> | ||
|
||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// kbm-parsing code | ||
|
||
export { Keymap, parseKbm, defaultMap }; | ||
|
||
type Keymap = { | ||
size: number; | ||
firstNote: number; | ||
lastNote: number; | ||
middleNote: number; | ||
referenceNote: number; | ||
frequency: number; | ||
formalOctave: number; | ||
mapping: Array<number | null>; | ||
} | ||
|
||
function parseKbm(text: string): Keymap { | ||
const lines = text.split('\n').filter((s) => !s.startsWith('!')); | ||
if (lines.length < 7) { | ||
throw new Error('Invalid mapping file') | ||
} | ||
const keymap = { | ||
size: parseInt(lines[0]), | ||
firstNote: parseInt(lines[1]), | ||
lastNote: parseInt(lines[2]), | ||
middleNote: parseInt(lines[3]), | ||
referenceNote: parseInt(lines[4]), | ||
frequency: parseFloat(lines[5]), | ||
formalOctave: parseInt(lines[6]), | ||
mapping: lines.slice(7).map((s) => | ||
s.trim() == 'x' ? null : parseInt(s)), | ||
}; | ||
if (keymap.mapping.length != keymap.size) { | ||
throw new Error('Wrong number of keys in mapping'); | ||
} | ||
return keymap; | ||
} | ||
|
||
function defaultMap(size: number): Keymap { | ||
return { | ||
size: size, | ||
firstNote: 0, | ||
lastNote: 127, | ||
middleNote: 60, | ||
referenceNote: 69, | ||
frequency: 440.0, | ||
formalOctave: size, | ||
mapping: [...new Array(size).keys()], | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// scl-parsing code | ||
|
||
export { Scale, parseScl }; | ||
|
||
type Scale = { | ||
description: string; | ||
notes: Array<number>; | ||
} | ||
|
||
function centsFromRatio(num: number, den: number): number { | ||
return 1200 * Math.log(num / den) / Math.log(2); | ||
} | ||
|
||
function parseNote(line: string): number { | ||
line = line.trim(); | ||
if (/^\d+$/.test(line)) { | ||
const num = parseInt(line); | ||
return centsFromRatio(num, 1); | ||
} else if (/^\d+\/\d+$/.test(line)) { | ||
const tokens = line.split('/').map((x) => parseInt(x)); | ||
return centsFromRatio(tokens[0], tokens[1]); | ||
} else if (/^\d*.\d*$/.test(line)) { | ||
return parseFloat(line); | ||
} | ||
throw new Error(`Could not parse pitch value: ${line}`) | ||
} | ||
|
||
function parseScl(text: string): Scale { | ||
const lines = text.split('\n').filter((s) => !s.startsWith('!')); | ||
if (lines.length < 2) { | ||
throw new Error('Invalid scale file') | ||
} | ||
const scale = { | ||
description: lines[0], | ||
notes: lines.slice(2).filter((s) => s.length > 0).map(parseNote), | ||
}; | ||
const count = parseInt(lines[1]); | ||
if (scale.notes.length != count) { | ||
throw new Error('Wrong number of notes in scale'); | ||
} | ||
return scale; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// front-end | ||
|
||
import { parseScl } from './scl.js'; | ||
import { parseKbm, defaultMap } from './kbm.js'; | ||
import { generateCurve } from './curve.js'; | ||
|
||
const sclInput = | ||
document.querySelector('#sclInput') as HTMLInputElement; | ||
const kbmInput = | ||
document.querySelector('#kbmInput') as HTMLInputElement; | ||
const centsInput = | ||
document.querySelector('#centsInput') as HTMLInputElement; | ||
const convertButton = | ||
document.querySelector('#convertButton') as HTMLButtonElement; | ||
const messageArea = | ||
document.querySelector('#messageArea') as HTMLParagraphElement; | ||
|
||
function reportError(err: Error) { | ||
messageArea.setAttribute('style', 'display: block;'); | ||
messageArea.innerText = err.message + '.'; | ||
} | ||
|
||
function clearError() { | ||
messageArea.setAttribute('style', 'display: none;') | ||
} | ||
|
||
// https://stackoverflow.com/questions/19327749/ | ||
function download(blob: Blob, filename: string) { | ||
const url = URL.createObjectURL(blob); | ||
const a = document.createElement('a'); | ||
a.setAttribute('style', 'display: none'); | ||
a.href = url; | ||
a.download = filename; | ||
document.body.appendChild(a); | ||
a.click(); | ||
setTimeout(() => { | ||
URL.revokeObjectURL(url); | ||
document.body.removeChild(a); | ||
}); | ||
} | ||
|
||
convertButton.addEventListener('click', (event) => { | ||
const scl = sclInput.files?.item(0); | ||
const kbm = kbmInput.files?.item(0); | ||
const offset = centsInput.valueAsNumber; | ||
if (isNaN(offset)) { | ||
reportError(new Error('Invalid cents offset')); | ||
} else if (scl) { | ||
scl.text().then((sclText) => { | ||
const scale = parseScl(sclText); | ||
if (kbm) { | ||
kbm.text().then((kbmText) => { | ||
const keymap = parseKbm(kbmText); | ||
const buf = generateCurve(scale, keymap, offset); | ||
finish(buf, scl.name); | ||
}).catch((err) => { | ||
reportError(err); | ||
}); | ||
} else { | ||
const keymap = defaultMap(scale.notes.length); | ||
const buf = generateCurve(scale, keymap, offset); | ||
finish(buf, scl.name); | ||
} | ||
}).catch((err) => { | ||
reportError(err); | ||
}); | ||
} else { | ||
reportError(new Error('No scale selected')); | ||
} | ||
}); | ||
|
||
function finish(buf: Uint8Array, filename: string) { | ||
clearError(); | ||
try { | ||
const blob = new Blob([buf], { type: 'application/octet-stream' }); | ||
download(blob, filename.replace('.scl', '.curve16bit')); | ||
} catch (err) { | ||
reportError(err as Error); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters