diff --git a/index.html b/index.html
index d88f361..aa4cb5e 100644
--- a/index.html
+++ b/index.html
@@ -35,14 +35,31 @@
Partial Player Plus
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/script.js b/script.js
index c4b423e..803a003 100644
--- a/script.js
+++ b/script.js
@@ -3,6 +3,8 @@ const multiplierInput = document.getElementById("multiplier");
const volumeSlider = document.getElementById("volume");
const noteInput = document.getElementById("note");
+const transposeNote = document.getElementById("transpose");
+const transposeToggleButton = document.getElementById("transposeToggle");
const referenceAInput = document.getElementById("referenceA");
const outputDisplay = document.getElementById("outputFrequency");
@@ -69,6 +71,7 @@ let oscillator = null;
let gainNode = null;
let holdTimeout;
let holdInterval;
+let transposeEnabled = true;
function getVolume() {
return Math.pow(Number(volumeSlider.value) / 100, 2);
@@ -153,45 +156,65 @@ function frequencyToMidi(freq, referenceA) {
}
-function frequencyToNote(freq, referenceA) {
+function updateTransposeToggle() {
+ const label = transposeEnabled ? "On" : "Off";
+ transposeToggleButton.textContent = `Transpose: ${label}`;
+ transposeToggleButton.classList.toggle("active", transposeEnabled);
+}
- const midiFloat = frequencyToMidi(freq, referenceA);
+function toggleTranspose() {
+ transposeEnabled = !transposeEnabled;
+ updateTransposeToggle();
+ updateDisplay();
+}
+function frequencyToNote(freq, referenceA) {
+ const midiFloat = frequencyToMidi(freq, referenceA);
const midi = Math.round(midiFloat);
-
const cents = (midiFloat - midi) * 100;
+ //const octave = Math.floor(midi / 12) - 1;
+ let octave = Math.floor(midi / 12) - 1;
+ let noteIndex = midi % 12;
+ let note = NOTE_NAMES[noteIndex];
- const octave = Math.floor(midi / 12) - 1;
+if (transposeEnabled) {
+ const transposeName = getNoteName(transposeNote.value);
- const note = NOTE_NAMES[midi % 12];
+ if (transposeName) {
+ const transposeIndex = NOTE_INDEX[transposeName];
- return {
+ if (transposeIndex !== undefined) {
- note: note + octave,
+ // interval from C to the selected transposition
+ const delta = transposeIndex - NOTE_INDEX.C;
- cents: cents
+ const displayMidi = midi - delta;
- };
+ noteIndex = ((displayMidi % 12) + 12) % 12;
+ note = NOTE_NAMES[noteIndex];
+ octave = Math.floor(displayMidi / 12) - 1;
+ }
+ }
+}
+ return {
+ note: note + octave,
+ cents: cents
+ };
}
function updateDisplay() {
-
const freq = getFrequency();
const refA = Number(referenceAInput.value);
- const nearest = frequencyToNote(freq, refA)
+ const nearest = frequencyToNote(freq, refA);
const sign = nearest.cents >= 0 ? "+" : "";
- nearestNote.textContent =
- `${nearest.note} (${sign}${nearest.cents.toFixed(2)}¢)`;
+ nearestNote.textContent = `${nearest.note} (${sign}${nearest.cents.toFixed(2)}¢)`;
- outputDisplay.textContent = freq.toFixed(2) + " Hz" ;
+ outputDisplay.textContent = freq.toFixed(2) + " Hz";
if (oscillator) {
- oscillator.frequency.setValueAtTime(
- freq,
- audioContext.currentTime
- );
+ oscillator.frequency.setValueAtTime(freq, audioContext.currentTime);
}
}
@@ -209,22 +232,25 @@ function updateFromFrequency() {
}
function parseNote(text) {
+ const match = String(text).trim().match(/^([A-Ga-g])([#b]?)(-?\d+)?$/i);
- const match = text.trim().match(/^([A-Ga-g])([#b]?)(-?\d+)$/);
+ if (!match) return null;
- if (!match)
- return null;
+ const letter = match[1].toUpperCase();
+ const accidental = (match[2] || "").toLowerCase();
return {
-
- note: (match[1].toUpperCase() + match[2]),
-
- octave: Number(match[3])
-
+ note: letter + accidental,
+ octave: match[3] === undefined ? null : Number(match[3])
};
+}
+function getNoteName(text) {
+ const parsed = parseNote(text);
+ return parsed ? parsed.note : null;
}
+
function noteToMidi(noteString) {
const parsed = parseNote(noteString);
@@ -364,6 +390,9 @@ fundamentalInput.addEventListener("input", updateFromFrequency);
noteInput.addEventListener("input", updateFromNote);
+transposeNote.addEventListener("input", updateDisplay);
+transposeToggleButton.addEventListener("click", toggleTranspose);
+
referenceAInput.addEventListener("input", updateFromNote);
multiplierInput.addEventListener("input", updateDisplay);
@@ -383,6 +412,6 @@ setupSpinner(multPlus, multiplierInput, 1);
setupSpinner(aMinus, referenceAInput, -1, updateFromNote);
setupSpinner(aPlus, referenceAInput, 1, updateFromNote);
-
+updateTransposeToggle();
updateDisplay();
diff --git a/style.css b/style.css
index 2e73d7b..999cf63 100644
--- a/style.css
+++ b/style.css
@@ -78,6 +78,48 @@ input[type=text] {
}
+.dual-control-row {
+ display: flex;
+ gap: 16px;
+}
+
+.dual-control {
+ flex: 1;
+}
+
+.dual-control input {
+ width: 100%;
+}
+
+.transpose-controls {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ align-items: center;
+}
+
+.transpose-controls button {
+ width: 100%;
+ padding: 8px 4px;
+ border: none;
+ border-radius: 8px;
+ background: #4fd2ff;
+ color: #111;
+ font-weight: bold;
+ font-size: 9px;
+}
+
+.transpose-controls button.active {
+ background: #a2211d;
+ color: white;
+}
+
+.transpose-controls input {
+ width: 100%;
+}
+
+
+
input[type=range] {
width: 100%;