Skip to content

Commit

Permalink
Add MTS-SysEx exporter
Browse files Browse the repository at this point in the history
  • Loading branch information
vsicurella committed Jul 8, 2023
1 parent c75afb2 commit 777c57d
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 26 deletions.
30 changes: 6 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"vue": "^3.2.33",
"vue-router": "^4.1.5",
"webmidi": "^3.0.21",
"xen-dev-utils": "^0.1.2",
"xen-dev-utils": "github:xenharmonic-devs/xen-dev-utils#mts-methods",
"scale-workshop-core": "github:xenharmonic-devs/scale-workshop-core#v0.0.1"
},
"devDependencies": {
Expand Down
18 changes: 17 additions & 1 deletion src/components/ScaleBuilder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ 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 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 @@ -137,6 +138,7 @@ function doExport(exporter: ExporterKey) {
}
const showReaperExportModal = ref(false);
const showMtsSysexExportModal = ref(false);
const showShareUrlModal = ref(false);
const shareUrlModal = ref<any>(null);
Expand Down Expand Up @@ -491,6 +493,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 @@ -532,6 +538,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
116 changes: 116 additions & 0 deletions src/components/modals/export/MtsSysexExport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<script setup lang="ts">
import MtsSysexExporter from "@/exporters/mts-sysex";
import { sanitizeFilename } from "@/utils";
import { ref } 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"]);
function clampName(name: string): string {
return name.slice(0, 16);
}
function formatPresetIndex(input: string | number): number {
let number =
typeof input === "string" ? parseInt(input.replace(/\D/g, "")) : input;
if (Number.isNaN(number)) return 0;
return clamp(0, 127, number);
}
// Rarely implemented parameters
// const deviceId = ref(0);
// const bank = ref(0); (only for message 0x0804)
const name = ref(clampName(props.scaleName));
const presetIndex = ref(0);
function doExport() {
const params = {
newline: props.newline,
scale: props.scale,
filename: sanitizeFilename(props.scaleName),
baseMidiNote: props.baseMidiNote,
name: name.value,
presetIndex: 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"
maxlength="16"
/>
</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"
type="number"
id="preset-index"
v-model="presetIndex"
@input="presetIndex = formatPresetIndex(presetIndex)"
/>
</div>
</div>
</template>
</Modal>
</template>

<style>
input.half {
flex-grow: 0.25 !important;
}
span.info-question {
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:hover {
background: white;
color: black;
border-color: white;
transition: all 0.3s ease;
}
</style>
File renamed without changes.
60 changes: 60 additions & 0 deletions src/exporters/__tests__/mts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createHash } from "crypto";
import { describe, it, expect } from "vitest";

import { getTestData } from "./test-data";
import MtsSysexExporter from "../mts-sysex";

describe("MTS exporter", () => {
it("can handle all line types", async () => {
const params = getTestData(
"MTS Sysex Bulk Tuning Dump exporter unit test v0.0.0"
);
params.presetIndex = 1;

const exporter = new MtsSysexExporter(params);

const scaleData = exporter.getBulkTuningData();
expect(createHash("sha256").update(scaleData).digest("base64")).toBe(
"Ps5Ddp9lBYZgCn7Y8aSBnhXOcfIm+sh9AcnybiLX4Zg="
);

// Name is padded with spaces
const nameData = exporter.getNameData();
expect(nameData).toEqual([
84, 101, 115, 116, 32, 83, 99, 97, 108, 101, 32, 32, 32, 32, 32, 32,
]);

const sysExMsg = exporter.buildSysExDump();
expect(createHash("sha256").update(sysExMsg).digest("base64")).toBe(
"Dx+z9IBMNqBrj7/g4EJ3XQxMPKbtQqmpfjLdoPPrV48="
);
});

it("can gracefully handle invalid parameters", async () => {
const params = getTestData(
"MTS Sysex Bulk Tuning Dump exporter unit test v0.0.0"
);
params.name = "Super Special Test Scale";
params.presetIndex = -1;

const exporter = new MtsSysexExporter(params);

const scaleData = exporter.getBulkTuningData();
expect(createHash("sha256").update(scaleData).digest("base64")).toBe(
"Ps5Ddp9lBYZgCn7Y8aSBnhXOcfIm+sh9AcnybiLX4Zg="
);

// Name is truncated
const nameData = exporter.getNameData();
expect(nameData).toEqual([
83, 117, 112, 101, 114, 32, 83, 112, 101, 99, 105, 97, 108, 32, 84, 101,
]);

const sysExMsg = exporter.buildSysExDump();
expect(createHash("sha256").update(sysExMsg).digest("base64")).toBe(
"8TgBDEn+mTm/xar39zeO9NBCcRn6KymbO/ZeMfa8rq0="
);
});

return;
});
1 change: 1 addition & 0 deletions src/exporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ExporterParams = {
centsRoot?: number;
displayPeriod?: boolean;
integratePeriod?: boolean;
presetIndex?: number;
};

export class BaseExporter {
Expand Down
2 changes: 2 additions & 0 deletions src/exporters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import MaxMSPExporter from "@/exporters/max-msp";
import PureDataExporter from "@/exporters/pure-data";
import { ScalaSclExporter, ScalaKbmExporter } from "@/exporters/scala";
import SoniccoutureExporter from "@/exporters/soniccouture";
import MtsSysexExporter from "@/exporters/mts-sysex";

const EXPORTERS = {
anamarkv1: AnaMarkV1Exporter,
Expand All @@ -22,6 +23,7 @@ const EXPORTERS = {
sytrus: SytrusExporter,
mnlgtuns: MnlgtunsExporter,
mnlgtuno: MnlgtunoExporter,
mtsSysex: MtsSysexExporter,
deflemask: DeflemaskExporter,
};

Expand Down
Loading

0 comments on commit 777c57d

Please sign in to comment.