Skip to content

Commit

Permalink
Add MTS exporter, add more Korg formats, comply with xen-dev-utils v0…
Browse files Browse the repository at this point in the history
….1.4

Addresses #175, #384, and #387
  • Loading branch information
vsicurella committed Nov 25, 2023
1 parent 04cf81a commit 44ba665
Show file tree
Hide file tree
Showing 12 changed files with 706 additions and 169 deletions.
46 changes: 45 additions & 1 deletion src/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@

--color-error: red;

/* Mimic Bootstrap alert with 'danger' variant */
--color-alert-danger: rgba(104, 35, 39, 1.0);
--color-alert-danger-background: rgba(243, 216, 218, 1.0);
--color-alert-danger-border: rgba(239, 199, 204, 1.0);

--section-gap: 160px;
--base-font-size: 15px;
--base-line-height: 1.5;
Expand Down Expand Up @@ -231,6 +236,9 @@ ul.btn-group, .btn-dropdown-group ul {
.control select {
flex-grow: 1;
}
.control input.half {
flex-grow: 0.25 !important;
}
.control.checkbox-container {
flex-flow: unset;
}
Expand All @@ -251,7 +259,7 @@ ul.btn-group, .btn-dropdown-group ul {
}
}

/* UI element - expandable secion */
/* UI element - expandable section */
.section::after {
content: " ▼";
font-size: 0.5rem;
Expand All @@ -274,3 +282,39 @@ p.section {
font-weight: bold;
cursor: pointer;
}

/* UI element - question mark with tooltip on hover */
span.info-question {
background: var(--color-background);
color: var(--color-text);
border-color: var(--color-text);
border-radius: 50%;
border-width: 2px;
border-style: solid;
padding-left: 4px;
padding-right: 4px;
font-size: smaller;

transition: 0.3s ease;
}
span.info-question::after {
content: '?';
}
span.info-question:hover {
background: var(--color-text);
color: var(--color-background);
transition: all 0.3s ease;
}

/* Padded box for displaying UI feedback message */
div.alert-box-danger {
background: var(--color-alert-danger-background);
border-width: 1px;
border-radius: 1%;
border-style: solid;
border-color: var(--color-alert-danger-border);
padding: 8px;
}
p.alert-message-danger {
color: var(--color-alert-danger);
}
43 changes: 33 additions & 10 deletions src/components/ScaleBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { APP_TITLE } from "@/constants";
import { sanitizeFilename, midiNoteNumberToName } from "@/utils";
import { exportFile, type ExporterKey } from "@/exporters";
import Modal from "@/components/ModalDialog.vue";
import ReaperExportModal from "@/components/modals/ReaperExport.vue";
import ReaperExportModal from "@/components/modals/export/ReaperExport.vue";
import MtsSysexExportModal from "@/components/modals/export/MtsSysexExport.vue";
import KorgExportModal from "@/components/modals/export/KorgExport.vue";
import ShareUrlModal from "@/components/modals/ShareUrl.vue";
import EqualTemperamentModal from "@/components/modals/generation/EqualTemperament.vue";
import HarmonicSeriesModal from "@/components/modals/generation/HarmonicSeries.vue";
Expand Down Expand Up @@ -136,7 +138,9 @@ function doExport(exporter: ExporterKey) {
exportFile(exporter, params);
}
const showKorgExportModal = ref(false);
const showReaperExportModal = ref(false);
const showMtsSysexExportModal = ref(false);
const showShareUrlModal = ref(false);
const shareUrlModal = ref<any>(null);
Expand Down Expand Up @@ -473,15 +477,10 @@ function copyToClipboard() {
<p><strong>Sytrus pitch map (.fnv)</strong></p>
<p>Envelope state file for the pitch envelope in Image-Line Sytrus</p>
</a>
<a href="#" class="btn" @click="doExport('mnlgtuns')">
<p><strong>Korg 'logue user scale (.mnlgtuns)</strong></p>
<p>Single scale for Korg 'logue Sound Librarian.</p>
<p>Full tuning of all MIDI notes</p>
</a>
<a href="#" class="btn" @click="doExport('mnlgtuno')">
<p><strong>Korg 'logue user octave (.mnlgtuno)</strong></p>
<p>Single scale for Korg 'logue Sound Librarian.</p>
<p>Only supports octave-repeating scales with 12 intervals</p>
<a href="#" class="btn" @click="showKorgExportModal = true">
<p><strong>Korg Sound Librarian scale (.mnlgtuns + others)</strong></p>
<p>Tuning formats for use with Monologue, Minilogue,</p>
<p>Minilogue XD, and Prologue synthesizers</p>
</a>
<a href="#" class="btn" @click="doExport('deflemask')">
<p><strong>Deflemask reference (.txt)</strong></p>
Expand All @@ -491,6 +490,10 @@ function copyToClipboard() {
<p><strong>Reaper note name map (.txt)</strong></p>
<p>Displays custom note names on Reaper's piano roll</p>
</a>
<a href="#" class="btn" @click="showMtsSysexExportModal = true">
<p><strong>MTS Sysex Bulk Tuning Dump (.syx)</strong></p>
<p>Binary data of a Bulk Tuning Dump SysEx message</p>
</a>
<a
href="#"
class="btn"
Expand Down Expand Up @@ -522,6 +525,16 @@ function copyToClipboard() {
/>

<Teleport to="body">
<KorgExportModal
:show="showKorgExportModal"
@confirm="showKorgExportModal = false"
@cancel="showKorgExportModal = false"
:newline="props.newline"
:scaleName="scaleName"
:baseMidiNote="baseMidiNote"
:scale="scale"
/>

<ReaperExportModal
:show="showReaperExportModal"
@confirm="showReaperExportModal = false"
Expand All @@ -532,6 +545,16 @@ function copyToClipboard() {
:scale="scale"
/>

<MtsSysexExportModal
:show="showMtsSysexExportModal"
@confirm="showMtsSysexExportModal = false"
@cancel="showMtsSysexExportModal = false"
:newline="props.newline"
:scaleName="scaleName"
:baseMidiNote="baseMidiNote"
:scale="scale"
/>

<RankTwoModal
:show="showRankTwoModal"
:centsFractionDigits="centsFractionDigits"
Expand Down
109 changes: 109 additions & 0 deletions src/components/modals/export/KorgExport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script setup lang="ts">
import { KorgModels, KorgExporter, getKorgModelInfo } from "@/exporters/korg";
import { sanitizeFilename } from "@/utils";
import { computed, ref } from "vue";
import Modal from "@/components/ModalDialog.vue";
import type { Scale } from "scale-workshop-core";
const props = defineProps<{
newline: string;
scaleName: string;
baseMidiNote: number;
scale: Scale;
}>();
const emit = defineEmits(["confirm", "cancel"]);
const models = [
KorgModels.MONOLOGUE,
KorgModels.MINILOGUE,
KorgModels.MINILOGUE_XD,
KorgModels.PROLOGUE,
];
const modelName = ref("minilogue");
const useOctaveFormat = ref(false);
const dialogErrorMessage = computed(() => {
if (useOctaveFormat.value) {
const message = KorgExporter.getOctaveFormatErrorMessage(props.scale);
if (message.length > 0) return message;
}
// Can check for other errors here...
return String();
});
const fileTypePreview = computed(() => {
const format = getKorgModelInfo(modelName.value);
return useOctaveFormat.value ? format.octave : format.scale;
});
function doExport() {
const params = {
newline: props.newline,
scale: props.scale,
filename: sanitizeFilename(props.scaleName),
baseMidiNote: props.baseMidiNote,
};
const exporter = new KorgExporter(
params,
modelName.value,
useOctaveFormat.value
);
exporter.saveFile();
emit("confirm");
}
</script>

<template>
<Modal @confirm="doExport" @cancel="$emit('cancel')">
<template #header>
<h2>Export Korg Sound Librarian scale</h2>
</template>
<template #body>
<div class="control-group">
<div class="control">
<label for="modelName">Synth Model</label>
<select id="modelName" v-model="modelName">
<option v-for="name of models" :key="name" :value="name">
{{ getKorgModelInfo(name).title }}
</option>
</select>
</div>
<div id="format" class="control radio-group">
<label for="format">Tuning Format</label>
<label>
<input type="radio" :value="false" v-model="useOctaveFormat" />
&nbsp;Scale (128-note table)
</label>
<label>
<input type="radio" :value="true" v-model="useOctaveFormat" />
&nbsp;Octave (12-note table, octave repeating with fixed C)
</label>
</div>
<p>
<label>Export Format: </label>
{{ fileTypePreview }}
</p>
<div class="alert-box-danger" v-if="dialogErrorMessage.length > 0">
<p class="alert-message-danger">
{{ dialogErrorMessage }}
</p>
</div>
</div>
</template>
<template #footer>
<div class="btn-group">
<button
@click="$emit('confirm')"
:disabled="dialogErrorMessage.length > 0"
>
OK
</button>
<button @click="$emit('cancel')">Cancel</button>
</div>
</template>
</Modal>
</template>
105 changes: 105 additions & 0 deletions src/components/modals/export/MtsSysexExport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script setup lang="ts">
import MtsSysexExporter from "@/exporters/mts-sysex";
import { sanitizeFilename } from "@/utils";
import { ref, watch } from "vue";
import Modal from "@/components/ModalDialog.vue";
import type { Scale } from "scale-workshop-core";
import { clamp } from "xen-dev-utils";
const props = defineProps<{
newline: string;
scaleName: string;
baseMidiNote: number;
scale: Scale;
}>();
const emit = defineEmits(["confirm", "cancel"]);
// Rarely implemented parameters
// const deviceId = ref(0);
// const bank = ref(0); (only for message 0x0804)
function clampName(name: string): string {
return name.slice(0, 16);
}
const name = ref(clampName(props.scaleName));
function nameInputCallback(nameInput: string): void {
name.value = clampName(nameInput);
}
watch(
() => props.scaleName,
(newName) => nameInputCallback(newName),
{ immediate: true }
);
function formatPresetIndex(input: string): string {
const number = parseInt(input.replace(/\D/g, ""));
if (Number.isNaN(number)) return "0";
return String(clamp(0, 127, number));
}
const presetIndex = ref("0");
function presetIndexInputCallback(indexInput: string): void {
presetIndex.value = formatPresetIndex(indexInput);
}
function doExport() {
const params = {
newline: props.newline,
scale: props.scale,
filename: sanitizeFilename(props.scaleName),
baseMidiNote: props.baseMidiNote,
name: name.value,
presetIndex: parseInt(presetIndex.value),
};
const exporter = new MtsSysexExporter(params);
exporter.saveFile();
emit("confirm");
}
</script>

<template>
<Modal @confirm="doExport" @cancel="$emit('cancel')">
<template #header>
<h2>Export MTS Bulk Tuning Dump</h2>
</template>
<template #body>
<div class="control-group">
<div class="control">
<label for="name">Name (16-character limit)</label>
<input
class="half"
type="text"
id="name"
v-model="name"
@input="nameInputCallback(name)"
/>
</div>
<div class="control">
<label for="preset-index">
Preset Index&nbsp;
<span
@click="$event.preventDefault()"
class="info-question"
title="Refer to your synth's manual for a valid range"
>
</span>
</label>
<input
class="half"
id="preset-index"
type="text"
v-model="presetIndex"
@input="presetIndexInputCallback(presetIndex)"
/>
</div>
</div>
</template>
</Modal>
</template>
File renamed without changes.
12 changes: 0 additions & 12 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,5 @@ export const WINDOWS_NEWLINE = "\r\n";

export const NUMBER_OF_NOTES = 128;

// Korg specific constants for exporters and importers
export const KORG = {
programmer: "ScaleWorkshop",
mnlg: {
octaveSize: 12,
scaleSize: 128,
maxCents: 12800,
refA: { val: 6900, ind: 69, freq: 440.0 },
refC: { val: 6000, ind: 60, freq: 261.6255653 },
},
};

// Browser interaction
export const LEFT_MOUSE_BTN = 0;
Loading

0 comments on commit 44ba665

Please sign in to comment.