Skip to content

Commit

Permalink
Implement SonicWeave Interchange exporter
Browse files Browse the repository at this point in the history
ref #34
  • Loading branch information
frostburn committed Jul 14, 2024
1 parent 95a4c23 commit 1a2989c
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 5 deletions.
10 changes: 10 additions & 0 deletions src/components/ExporterButtons.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
Expand Down Expand Up @@ -260,6 +266,10 @@ function doExport(exporter: ExporterKey) {
<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" @click="doExport('xendevs')">
<p><strong>SonicWeave Interchange (.swi)</strong></p>
<p>Simplified Scale Workshop 3 format</p>
</a>
<h3>Documentation</h3>
<p>
You can read about the new SonicWeave syntax
Expand Down
18 changes: 15 additions & 3 deletions src/exporters/__tests__/test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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)

Expand Down Expand Up @@ -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
}
23 changes: 23 additions & 0 deletions src/exporters/__tests__/xen-devs.spec.ts
Original file line number Diff line number Diff line change
@@ -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
`)
})
})
4 changes: 3 additions & 1 deletion src/exporters/base.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -23,6 +23,8 @@ export type ExporterParams = {
integratePeriod?: boolean
presetIndex?: number
centsFractionDigits?: number
rawIntervals?: Interval[]
unisonFrequency?: TimeMonzo
}

export class BaseExporter {
Expand Down
4 changes: 3 additions & 1 deletion src/exporters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,7 +22,8 @@ const EXPORTERS = {
ableton: AbletonAsclExporter,
puredata: PureDataExporter,
soniccouture: SoniccoutureExporter,
maxmsp: MaxMSPExporter
maxmsp: MaxMSPExporter,
xendevs: SonicWeaveInterchangeExporter
}

export type ExporterKey = keyof typeof EXPORTERS
Expand Down
37 changes: 37 additions & 0 deletions src/exporters/xen-devs.ts
Original file line number Diff line number Diff line change
@@ -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())
}
}
14 changes: 14 additions & 0 deletions src/stores/scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -728,6 +741,7 @@ export const useScaleStore = defineStore('scale', () => {
labelForIndex,
toJSON,
fromJSON,
computeRawScale,
// Mini-stores
history
}
Expand Down

0 comments on commit 1a2989c

Please sign in to comment.