Migrate midi module to Conductor#791
Conversation
Splits midi into a pure, evaluator-free functions.ts (unchanged signatures) and a Conductor-facing index.ts plugin, since sound and stereo_sound import midi_note_to_frequency and friends directly as plain TypeScript and sound/stereo_sound's own Source-facing APIs re-export several of these functions. Migrating index.ts's exports to require an IDataHandler would have broken both call sites immediately. - functions.ts / scales.ts / utils.ts / types.ts: untouched pure logic - conductorAdapters.ts: undecorated helpers (scale-list <-> Conductor list conversion, accidental validation) used by the plugin; kept separate from index.ts so they stay importable from vitest, which hits a decorator syntax error importing index.ts directly - index.ts: BaseModulePlugin subclass wrapping the pure functions; SHARP/FLAT/NATURAL are pushed onto `exports` directly in the constructor since BaseModulePlugin.initialise() only registers exportedNames that are functions - Includes the same __bindExportedMethods() workaround as repeat/rune/binary_tree for the unbound-method issue in BaseModulePlugin.initialise() (source-academy/conductor#41) - sound/stereo_sound's functions.ts and index.ts updated to import midi's pure functions from the new `/functions` subpath instead of the bundle root, which now exports the Conductor plugin
There was a problem hiding this comment.
Code Review
This pull request refactors the midi bundle to support the Conductor plugin architecture, separating pure MIDI functions into functions.ts and implementing the Conductor-facing plugin in index.ts. It also updates the sound and stereo_sound bundles to import directly from the new functions entry point. The review feedback highlights a potential issue where accessing the .name property of functions (such as letter_name_to_midi_note and midi_note_to_letter_name) for error reporting could fail or produce cryptic messages in minified production environments due to name mangling. It is recommended to replace these with hardcoded string literals.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Function.prototype.name gets mangled under minification, which would turn these error messages into cryptic garbage like "t expects...". Two of these were carried over unchanged from the original module; the third is in the new Conductor-facing index.ts.
conductor-migration's midi had drifted from master: 6 functions
(is_note_with_octave, add_octave_to_note, get_octave, get_note_name,
get_accidental, key_signature_to_key) and input validation on the
existing ones (midi_note_to_frequency now range-checks its input) only
existed on master, added there while this migration was in flight.
Confirmed against the published docs page
(source-academy.github.io/modules/documentation/modules/midi.html)
that this is now the complete function/constant list, no more, no
less - verified end-to-end through the actual compiled bundle, not
just unit tests, driving every export exactly the way a real evaluator
calls a closure (detached, not bound to any instance).
Also fixes midi_note_to_letter_name/key_signature_to_key's accidental
parameter to match master: it's the Accidental enum value ('#'/'b'),
not the word 'flat'/'sharp' my first pass used before I'd found the
drift. midi_note_to_letter_name silently treats anything other than
exactly SHARP as flat (matching master's actual, slightly loose
behavior) rather than validating it - key_signature_to_key is the one
that validates, since its own switch has an explicit default case for
that.
Validation now uses conductor's new EvaluatorParameterTypeError /
assertNumberWithinRange (source-academy/conductor#42) in place of
modules-lib's InvalidParameterTypeError / assertNumberWithinRange,
which functions.ts can't depend on without pulling js-slang's
modules-lib re-exports (and everything under it) into sound/
stereo_sound's dependency graph transitively. Message format is
unchanged - verified identical to master's existing test expectations.
|
Is there a way to get Vitest to work with decorators? They're functionalities need to be tested anyway |
Dug into this, afaik, I dont think its possible, not with our current Vite/Vitest setup, and it's not a config issue we can fix from our side. Root cause:Vitest runs test files through Vite's SSR module pipeline, which calls ssrTransformScript → parseAstAsync (from rolldown) unconditionally, regardless of whether oxc or esbuild is set as the TS transformer. That parser doesn't support native TC39 decorator syntax, so any test file importing a decorated class (like our index.ts plugins) throws a SyntaxError before the test even runs. I confirmed esbuild alone can downlevel decorators for non-esnext targets, and traced that Vitest defaults to oxc (which doesn't downlevel decorators for any target). I patched buildtools's runVitest to actually pass through Vite config overrides and forced esbuild/oxc:false, the config verifiably took effect, but the crash persisted, tracing straight to that hard-coded parseAstAsync call in Vite's SSR internals, below any of our config knobs. I reverted that experiment. For now, decorated Conductor-facing classes (index.ts) stay thin/untested-directly, and the actual logic under test lives in undecorated files (functions.ts, conductorAdapters.ts, utils.ts). Do you have any suggestions @leeyi45? |
AaravMalani
left a comment
There was a problem hiding this comment.
LGTM except documentation and the __bindExportedMethods function! I've made a couple non-blocking comments, though
| */ | ||
| import { DataType, type IDataHandler, type TypedValue } from '@sourceacademy/conductor/types'; | ||
| import { mEmptyList } from '@sourceacademy/conductor/util'; | ||
| import type { List } from 'js-slang/dist/stdlib/list'; |
There was a problem hiding this comment.
Maybe should remove our dependency on js-slang? It's not important right now, but would be a nice to have
| const [noteName, accidental, octave] = noteToValues(note, letter_name_to_midi_note.name); | ||
| @moduleMethod([DataType.CONST_STRING], DataType.NUMBER) | ||
| async* letter_name_to_midi_note(note: TypedValue<DataType.CONST_STRING>): AsyncGenerator<void, TypedValue<DataType.NUMBER>, unknown> { | ||
| return { type: DataType.NUMBER, value: letter_name_to_midi_note_func(note.value as NoteWithOctave) }; |
There was a problem hiding this comment.
I disagree with the function signature of these functions; if they can accept any string and throw errors for invalid strings, they should accept strings and they shouldn't need the cast.
| * key_signature_to_key(FLAT, 3); // Returns "Eb", since the key of Eb has 3 flats | ||
| * ``` | ||
| */ | ||
| export function key_signature_to_key(accidental: Accidental.FLAT | Accidental.SHARP, numAccidentals: number): Note { |
There was a problem hiding this comment.
This is unrelated to the migration and my circle of fifths is rusty, but doesn't every major key have a relative minor (D <-> Bm)? Shouldn't it be made clear in the documentation that it returns the major key / have a way to return it in a scale of the user's choosing?
There was a problem hiding this comment.
Actually, all the scales are modes of the major scale, so technically we should be able to support it returning the key in any of the scales (I'm surprised natural and harmonic minor are missing from the scales)
| static channelAttach = []; | ||
| constructor(conduit: IConduit, channels: IChannel<any>[], evaluator: IInterfacableEvaluator) { | ||
| super(conduit, channels, evaluator); | ||
| this.__bindExportedMethods(); |
Description
Fixes #775
Migrates the
midimodule to be Conductor-ready.midiis a dependency of bothsoundandstereo_sound, which import several of its functions (midi_note_to_frequency,letter_name_to_midi_note,letter_name_to_frequency) directly as plain TypeScript, and re-export them as part of their own Source-facing APIs. Rewritingmidi's functions to require anIDataHandler(the waybinary_tree/repeatdo) would have broken both of those call sites and re-exports immediately, so this migration keeps a pure, evaluator-free implementation available for cross-bundle TS consumption:functions.ts/scales.ts/utils.ts/types.ts— pure logic.scales.tsstill builds js-slang-style lists viapair/List, same as before.conductorAdapters.ts— new, undecorated helper used only by the plugin: converts a js-slang scale list into a real Conductor list. Kept out ofindex.tsdeliberately — a test file that importsindex.tsdirectly hits a decorator syntax error under vitest's transform (it doesn't apply the same decorator settingstscdoes), so anything meant to be unit-tested needs to live somewhere undecorated.index.ts— now aBaseModulePluginsubclass wrapping the pure functions.SHARP/FLAT/NATURALare plain string constants, not functions, soBaseModulePlugin.initialise()(which only registersexportedNamesthat are functions) can't pick them up — they're pushed ontothis.exportsdirectly in the constructor instead.sound/stereo_sound: only their imports of@sourceacademy/bundle-midichanged, to@sourceacademy/bundle-midi/functions(the bundle root now exports the Conductor plugin instead of plain functions). No other changes to either bundle — their own Conductor migration is a separate, larger effort (they're ~90% duplicated implementations of each other; deferred pending a decision on whether to merge them).Includes the same
__bindExportedMethods()workaroundrepeat/rune/binary_treeuse for the unbound-method issue inBaseModulePlugin.initialise()(source-academy/conductor#41, merged).conductor-migrationhad drifted frommasterfor this bundleWhile this migration was in flight,
master'smidipicked up 6 new functions (is_note_with_octave,add_octave_to_note,get_octave,get_note_name,get_accidental,key_signature_to_key) and input validation on the existing ones (midi_note_to_frequencynow range-checks its input), none of which existed onconductor-migration. This PR ports all of it, using the newEvaluatorParameterTypeError/EvaluatorNumberRangeError/assertNumberWithinRangefrom source-academy/conductor#42 (a generic, reusable standardized-error-message addition to Conductor itself, not something bundle-specific) in place ofmodules-lib'sInvalidParameterTypeError/assertNumberWithinRange, whichfunctions.tscan't depend on without pulling js-slang'smodules-libre-exports intosound/stereo_sound's dependency graph transitively. Message format is unchanged — verified identical tomaster's existing test expectations.Also fixes
midi_note_to_letter_name/key_signature_to_key'saccidentalparameter to matchmaster: it's theAccidentalenum value ('#'/'b'), not the word'flat'/'sharp'my first pass used before finding the drift.midi_note_to_letter_namesilently treats anything other than exactlySHARPas flat (matchingmaster's actual, slightly loose behavior) rather than validating it —key_signature_to_keyis the one that validates, since its own switch has an explicit default case for that.Depends on source-academy/conductor#42 (not yet merged) for
EvaluatorParameterTypeError/EvaluatorNumberRangeError/assertNumberWithinRange.Testing
yarn workspace @sourceacademy/bundle-midi run tsc/lint/test(29/29) /build— all passyarn workspace @sourceacademy/bundle-sound run tsc/test(28/28) — pass with the updated import pathyarn workspace @sourceacademy/bundle-stereo_sound run tsc/test(18/18) — pass with the updated import path