diff --git a/src/components/ExporterButtons.vue b/src/components/ExporterButtons.vue index 385147f3..2924f125 100644 --- a/src/components/ExporterButtons.vue +++ b/src/components/ExporterButtons.vue @@ -125,6 +125,12 @@ function doExport(exporter: ExporterKey) { date: new Date() } + if (exporter === 'xendevs') { + const { rawIntervals, unisonFrequency } = scale.computeRawScale() + params.rawIntervals = rawIntervals + params.unisonFrequency = unisonFrequency + } + exportFile(exporter, params) }) } @@ -260,6 +266,10 @@ function doExport(exporter: ExporterKey) {

MTS Sysex Bulk Tuning Dump (.syx)

Binary data of a Bulk Tuning Dump SysEx message

+ +

SonicWeave Interchange (.swi)

+

Simplified Scale Workshop 3 format

+

Documentation

You can read about the new SonicWeave syntax diff --git a/src/exporters/__tests__/test-data.ts b/src/exporters/__tests__/test-data.ts index dcb0fdbb..45780330 100644 --- a/src/exporters/__tests__/test-data.ts +++ b/src/exporters/__tests__/test-data.ts @@ -4,7 +4,8 @@ import { UNIX_NEWLINE, WINDOWS_NEWLINE } from '../../constants' import { Scale } from '../../scale' import { Fraction } from 'xen-dev-utils' -export function getTestData(appTitle: string) { +export function getTestData(appTitle: string, raw = false) { + const sourceText = '100.\nC5_5\n4\\5\n5/3\n1,3591409142295225r\n3486784401/3276800000\n2/1' const absoluteC5 = new Interval(TimeMonzo.fromFractionalFrequency(528, 3), 'logarithmic', 0, { type: 'AbsoluteFJS', pitch: { @@ -19,7 +20,16 @@ export function getTestData(appTitle: string) { subscripts: [[5, '']] }) const visitor = getSourceVisitor() + // Prefix visitor.visit(parseAST('A4 = 440 Hz').body[0]) + let rawIntervals: Interval[] | undefined + let unisonFrequency: TimeMonzo | undefined + if (raw) { + // Body + visitor.executeProgram(parseAST(sourceText)) + rawIntervals = visitor.currentScale + unisonFrequency = visitor.rootContext!.unisonFrequency + } const ev = visitor.createExpressionVisitor() const relativeC5 = relative.bind(ev)(absoluteC5) @@ -80,9 +90,11 @@ export function getTestData(appTitle: string) { appTitle, description: 'A scale for testing if the exporter works', midiOctaveOffset: 0, - sourceText: '100.\nC5_5\n4\\5\n5/3\n1,3591409142295225r\n3486784401/3276800000\n2/1', + sourceText, labels: ['100.', 'C5_5', '4\\5', '5/3', '1,3591409142295225r', '3486784401/3276800000', '2/1'], - date: new Date('2022-02-22T20:22Z') + date: new Date('2022-02-22T20:22Z'), + rawIntervals, + unisonFrequency } return params } diff --git a/src/exporters/__tests__/xen-devs.spec.ts b/src/exporters/__tests__/xen-devs.spec.ts new file mode 100644 index 00000000..47f9cd17 --- /dev/null +++ b/src/exporters/__tests__/xen-devs.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest' + +import SonicWeaveInterchangeExporter from '../xen-devs' + +import { getTestData } from './test-data' + +describe('SonicWeave Interchange exporter', () => { + it('can handle all line types', () => { + const params = getTestData('SWI exporter unit test v0.0.0') + const exporter = new SonicWeaveInterchangeExporter(params) + expect(exporter.getFileContents()).toBe(`(* Created using SWI exporter unit test v0.0.0 *) + +"Test Scale" +[1/12> "" niente +[1 1 -1> "" niente +[4/5> "" niente +[0 -1 1> "" niente +[531.234049066756>@rc "" niente +[-20 20 -5> "" niente +[1> "" niente +`) + }) +}) diff --git a/src/exporters/base.ts b/src/exporters/base.ts index 5b23c3ad..e0c93615 100644 --- a/src/exporters/base.ts +++ b/src/exporters/base.ts @@ -1,5 +1,5 @@ import type { Scale } from '@/scale' -import type { Interval } from 'sonic-weave' +import type { Interval, TimeMonzo } from 'sonic-weave' export type LineFormat = 'label' | 'cents' | 'frequency' | 'decimal' | 'degree' @@ -23,6 +23,8 @@ export type ExporterParams = { integratePeriod?: boolean presetIndex?: number centsFractionDigits?: number + rawIntervals?: Interval[] + unisonFrequency?: TimeMonzo } export class BaseExporter { diff --git a/src/exporters/index.ts b/src/exporters/index.ts index d9bd939a..08267dcd 100644 --- a/src/exporters/index.ts +++ b/src/exporters/index.ts @@ -8,6 +8,7 @@ import PureDataExporter from '@/exporters/pure-data' import SoniccoutureExporter from '@/exporters/soniccouture' import AbletonAsclExporter from '@/exporters/ableton' import MaxMSPExporter from '@/exporters/max-msp' +import SonicWeaveInterchangeExporter from './xen-devs' const EXPORTERS = { scalascl: ScalaSclExporter, @@ -21,7 +22,8 @@ const EXPORTERS = { ableton: AbletonAsclExporter, puredata: PureDataExporter, soniccouture: SoniccoutureExporter, - maxmsp: MaxMSPExporter + maxmsp: MaxMSPExporter, + xendevs: SonicWeaveInterchangeExporter } export type ExporterKey = keyof typeof EXPORTERS diff --git a/src/exporters/xen-devs.ts b/src/exporters/xen-devs.ts new file mode 100644 index 00000000..eaf4c7aa --- /dev/null +++ b/src/exporters/xen-devs.ts @@ -0,0 +1,37 @@ +import { APP_TITLE } from '@/constants' +import { BaseExporter, type ExporterParams } from '@/exporters/base' +import { literalToString } from 'sonic-weave' + +// Specs: https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/interchange.md +export default class SonicWeaveInterchangeExporter extends BaseExporter { + constructor(params: ExporterParams) { + super(params) + } + + getFileContents() { + const params = this.params + const lines = [`(* Created using ${params.appTitle ?? APP_TITLE} *)`, ''] + lines.push(JSON.stringify(params.scale.title)) + if (params.unisonFrequency) { + const unisonFrequency = literalToString(params.unisonFrequency.asInterchangeLiteral()) + lines.push(`1 = ${unisonFrequency}`) + lines.push('') + } + const intervals = params.rawIntervals ?? params.relativeIntervals + for (const interval of intervals) { + const universal = interval.shallowClone() + universal.node = universal.asMonzoLiteral(true) + let line = universal.toString(undefined, true) + if (line.startsWith('(') && line.endsWith(')')) { + line = line.slice(1, -1) + } + lines.push(line) + } + lines.push('') + return lines.join('\n') + } + + saveFile() { + super.saveFile(this.params.filename + '.swi', this.getFileContents()) + } +} diff --git a/src/stores/scale.ts b/src/stores/scale.ts index 463775f7..eac5c9ee 100644 --- a/src/stores/scale.ts +++ b/src/stores/scale.ts @@ -692,6 +692,19 @@ export const useScaleStore = defineStore('scale', () => { const history = undoHistory(serialize, restore) + // Only one exporter makes use of this so we don't maintain it in active memory. + function computeRawScale() { + const globalVisitor = getGlobalScaleWorkshopVisitor() + const visitor = new StatementVisitor(globalVisitor) + visitor.isUserRoot = true + const ast = parseAST(sourceText.value) + visitor.executeProgram(ast) + return { + rawIntervals: visitor.currentScale, + unisonFrequency: visitor.rootContext!.unisonFrequency + } + } + return { // Live state ...LIVE_STATE, @@ -728,6 +741,7 @@ export const useScaleStore = defineStore('scale', () => { labelForIndex, toJSON, fromJSON, + computeRawScale, // Mini-stores history }