Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions js/activity-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* ActivityContext
*
* Single authority for accessing the runtime Activity instance.
*
* NOTE: This repo uses RequireJS shims + globals for browser builds.
* This module supports AMD (RequireJS), CommonJS (Jest), and a browser global
* fallback (ActivityContext) without exporting the Activity instance onto
* window.activity.
*/

(function (root, factory) {
// Lazy singleton: factory runs once, under the loader's control when
// possible (AMD), but always exposes a global for non-AMD consumers.
let _mod;

function getModule() {
if (!_mod) _mod = factory();
return _mod;
}

if (typeof define === "function" && define.amd) {
define([], function () {
return getModule();
});
} else if (typeof module !== "undefined" && module.exports) {
module.exports = getModule();
}

// Ensure a global reference exists so non-AMD code (activity.js, synthutils.js)
// can access it immediately.
try {
root.ActivityContext = getModule();
} catch (e) {
// ignore if root is not writable in some hostile environments
}
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";

let _activity = null;

function setActivity(activityInstance) {
if (!activityInstance) {
throw new Error("Cannot set ActivityContext with a falsy value");
}
_activity = activityInstance;
}

function getActivity() {
if (!_activity) {
throw new Error(
"Activity not initialized yet. Use dependency injection or wait for initialization."
);
}
return _activity;
}

return { setActivity, getActivity };
});
28 changes: 26 additions & 2 deletions js/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -2969,7 +2969,19 @@ class Activity {

// Expose activity instance for external checks
if (typeof window !== "undefined") {
window.activity = this;
// Single authority: ActivityContext
// TODO: window.activity is deprecated; use ActivityContext instead
if (
window.ActivityContext &&
typeof window.ActivityContext.setActivity === "function"
) {
window.ActivityContext.setActivity(this);
}

// TEMP compatibility bridge
if (!window.activity) {
window.activity = this;
}
}
};

Expand Down Expand Up @@ -3034,7 +3046,19 @@ class Activity {

// Expose activity instance for external checks
if (typeof window !== "undefined") {
window.activity = this;
// Single authority: ActivityContext
// TODO: window.activity is deprecated; use ActivityContext instead
if (
window.ActivityContext &&
typeof window.ActivityContext.setActivity === "function"
) {
window.ActivityContext.setActivity(this);
}

// TEMP compatibility bridge
if (!window.activity) {
window.activity = this;
}
}
};

Expand Down
10 changes: 8 additions & 2 deletions js/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ requirejs.config({
exports: "Notation"
},
"utils/synthutils": {
deps: ["utils/utils"],
deps: ["utils/utils", "activity/activity-context"],
exports: "Synth"
},
"activity/logo": {
Expand All @@ -96,7 +96,13 @@ requirejs.config({
exports: "Logo"
},
"activity/activity": {
deps: ["utils/utils", "activity/logo", "activity/blocks", "activity/turtles"],
deps: [
"utils/utils",
"activity/activity-context",
"activity/logo",
"activity/blocks",
"activity/turtles"
],
exports: "Activity"
},
"materialize": {
Expand Down
122 changes: 54 additions & 68 deletions js/utils/synthutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2378,24 +2378,39 @@ function Synth() {
* @returns {Promise<void>}
*/
this.startTuner = async () => {
// Initialize required components for pie menu
if (!window.activity) {
window.activity = {
blocks: {
blockList: [],
setPitchOctave: () => {},
findPitchOctave: () => 4,
stageClick: false
},
logo: {
synth: this
},
canvas: document.createElement("canvas"),
blocksContainer: { x: 0, y: 0 },
getStageScale: () => 1,
KeySignatureEnv: ["A", "major", false]
};
}
const getSafeActivity = () => {
try {
if (
typeof window !== "undefined" &&
window.ActivityContext &&
typeof window.ActivityContext.getActivity === "function"
) {
return window.ActivityContext.getActivity();
}
} catch (e) {
// Fall through to warning below.
}

// In practice this runs in the browser; keep a safe fallback for tests.
try {
if (typeof module !== "undefined" && module.exports) {
// eslint-disable-next-line global-require
const ctx = require("../activity-context");
if (ctx && typeof ctx.getActivity === "function") {
return ctx.getActivity();
}
}
} catch (e) {
// Ignore.
}

console.warn("Activity not ready yet in synthutils");
return null;
};

// No fake globals: if Activity isn't ready, fail fast.
const activity = getSafeActivity();
if (!activity) return;

// Initialize wheelnav if not already done
if (typeof wheelnav !== "function") {
Expand Down Expand Up @@ -2676,13 +2691,30 @@ function Synth() {
}

try {
// Create a temporary block object to use with piemenuPitches
// Prepare a non-mutating activity proxy with a local logo fallback
const defaultLogo = {
synth: {
createDefaultSynth: () => {},
loadSynth: () => {},
setMasterVolume: () => {},
trigger: () => {},
inTemperament: "equal"
},
errorMsg: msg => {
console.warn(msg);
}
};

const logo = activity.logo || defaultLogo;
const activityProxy = Object.create(activity);
activityProxy.logo = logo;

const tempBlock = {
container: {
x: targetNoteSelector.offsetLeft,
y: targetNoteSelector.offsetTop
},
activity: window.activity,
activity: activityProxy,
blocks: {
blockList: [
{
Expand Down Expand Up @@ -2730,54 +2762,8 @@ function Synth() {
_triggerLock: false // This is needed for pitch preview
};

// Add required activity properties for preview
if (!window.activity.logo) {
window.activity.logo = {
synth: {
createDefaultSynth: () => {},
loadSynth: () => {},
setMasterVolume: () => {},
trigger: (turtle, note, duration, instrument) => {
// Use the Web Audio API to play the preview note
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();

oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);

// Convert note to frequency
const freq = pitchToFrequency(note[0], "equal");
oscillator.frequency.value = freq;

// Set volume
gainNode.gain.value = 0.1; // Low volume for preview

// Schedule note
oscillator.start();
gainNode.gain.setValueAtTime(
0.1,
audioContext.currentTime
);
gainNode.gain.linearRampToValueAtTime(
0,
audioContext.currentTime + duration
);
oscillator.stop(
audioContext.currentTime + duration
);
},
inTemperament: "equal"
},
errorMsg: msg => {
console.warn(msg);
}
};
}

// Add key signature environment
window.activity.KeySignatureEnv = ["C", "major", false];
// Add key signature environment (on proxy, not real activity)
activityProxy.KeySignatureEnv = ["C", "major", false];

// Make sure wheelDiv is properly positioned and visible
const wheelDiv = docById("wheelDiv");
Expand Down
Loading