mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 21:58:37 +00:00
Merge branch 'main' into pattern_selection
This commit is contained in:
commit
e7e60d3ed8
@ -3,6 +3,12 @@ import jsdoc from '../../doc.json';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { h } from './html';
|
||||
|
||||
function plaintext(str) {
|
||||
const div = document.createElement('div');
|
||||
div.innerText = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
@ -21,7 +27,7 @@ ${doc.description}
|
||||
)}
|
||||
</ul>
|
||||
<div>
|
||||
${doc.examples?.map((example) => `<div><pre>${example}</pre></div>`)}
|
||||
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
|
||||
</div>
|
||||
</div>`[0];
|
||||
/*
|
||||
|
||||
@ -4,7 +4,14 @@ import { history } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
|
||||
import {
|
||||
EditorView,
|
||||
highlightActiveLineGutter,
|
||||
highlightActiveLine,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
drawSelection,
|
||||
} from '@codemirror/view';
|
||||
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
|
||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
@ -68,6 +75,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
history(),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{
|
||||
|
||||
@ -292,7 +292,7 @@ function peg$parse(input, options) {
|
||||
var peg$f3 = function(s) { return s };
|
||||
var peg$f4 = function(s, stepsPerCycle) { s.arguments_.stepsPerCycle = stepsPerCycle ; return s; };
|
||||
var peg$f5 = function(a) { return a };
|
||||
var peg$f6 = function(s) { s.arguments_.alignment = 'slowcat'; return s; };
|
||||
var peg$f6 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
|
||||
var peg$f7 = function(a) { return x => x.options_['weight'] = a };
|
||||
var peg$f8 = function(a) { return x => x.options_['reps'] = a };
|
||||
var peg$f9 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||
@ -1073,7 +1073,7 @@ function peg$parse(input, options) {
|
||||
}
|
||||
if (s2 !== peg$FAILED) {
|
||||
s3 = peg$parsews();
|
||||
s4 = peg$parsesequence();
|
||||
s4 = peg$parsepolymeter_stack();
|
||||
if (s4 !== peg$FAILED) {
|
||||
s5 = peg$parsews();
|
||||
if (input.charCodeAt(peg$currPos) === 62) {
|
||||
|
||||
@ -119,8 +119,8 @@ polymeter_steps = "%"a:slice
|
||||
|
||||
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
|
||||
// change the alignment to slowcat
|
||||
slow_sequence = ws "<" ws s:sequence ws ">" ws
|
||||
{ s.arguments_.alignment = 'slowcat'; return s; }
|
||||
slow_sequence = ws "<" ws s:polymeter_stack ws ">" ws
|
||||
{ s.arguments_.alignment = 'polymeter_slowcat'; return s; }
|
||||
|
||||
// a slice is either a single step or a sub cycle
|
||||
slice = step / sub_cycle / polymeter / slow_sequence
|
||||
|
||||
@ -91,6 +91,10 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||
if (alignment === 'stack') {
|
||||
return strudel.stack(...children);
|
||||
}
|
||||
if (alignment === 'polymeter_slowcat') {
|
||||
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1)));
|
||||
return strudel.stack(...aligned);
|
||||
}
|
||||
if (alignment === 'polymeter') {
|
||||
// polymeter
|
||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||
@ -104,15 +108,9 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
|
||||
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
||||
}
|
||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||
if (!weightedChildren && alignment === 'slowcat') {
|
||||
return strudel.slowcat(...children);
|
||||
}
|
||||
if (weightedChildren) {
|
||||
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
|
||||
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
||||
if (alignment === 'slowcat') {
|
||||
return pat._slow(weightSum); // timecat + slow
|
||||
}
|
||||
pat.__weight = weightSum;
|
||||
return pat;
|
||||
}
|
||||
|
||||
@ -18,6 +18,9 @@ import {
|
||||
midi2note,
|
||||
renderVoicing,
|
||||
scaleStep,
|
||||
stepInNamedScale,
|
||||
nearestNumberIndex,
|
||||
note2midi,
|
||||
} from '../tonleiter.mjs';
|
||||
|
||||
describe('tonleiter', () => {
|
||||
@ -149,4 +152,34 @@ describe('tonleiter', () => {
|
||||
// expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']);
|
||||
// TODO: test with offset
|
||||
});
|
||||
test('nearestNumber', () => {
|
||||
expect(nearestNumberIndex(0, [0, 2, 4, 5])).toEqual(0);
|
||||
expect(nearestNumberIndex(1, [0, 2, 4, 5])).toEqual(0);
|
||||
expect(nearestNumberIndex(1, [0, 2, 4, 5], true)).toEqual(1);
|
||||
expect(nearestNumberIndex(2, [0, 2, 4, 5])).toEqual(1);
|
||||
expect(nearestNumberIndex(2, [0, 2, 4, 5]), true).toEqual(1);
|
||||
expect(nearestNumberIndex(3, [0, 2, 4, 5])).toEqual(1);
|
||||
expect(nearestNumberIndex(3, [0, 2, 4, 5], true)).toEqual(2);
|
||||
expect(nearestNumberIndex(4, [0, 2, 4, 5])).toEqual(2);
|
||||
});
|
||||
test('stepInNamedScale', () => {
|
||||
expect(stepInNamedScale(1, 'D major')).toEqual(note2midi('E3'));
|
||||
expect(stepInNamedScale(2, 'E major')).toEqual(note2midi('G#3'));
|
||||
expect(stepInNamedScale(0, 'D major', 'E3')).toEqual(note2midi('E3'));
|
||||
expect(stepInNamedScale(0, 'D major', 'E4')).toEqual(note2midi('E4'));
|
||||
expect(stepInNamedScale(0, 'D major', 'Eb4')).toEqual(note2midi('D4'));
|
||||
expect(stepInNamedScale(0, 'D major', 'F4')).toEqual(note2midi('E4'));
|
||||
expect(stepInNamedScale(0, 'D major', 'F#4')).toEqual(note2midi('F#4'));
|
||||
expect(stepInNamedScale(0, 'D major', 'G4')).toEqual(note2midi('G4'));
|
||||
|
||||
expect(stepInNamedScale(0, 'F major', 'F4')).toEqual(note2midi('F4'));
|
||||
expect(stepInNamedScale(0, 'F major', 'G4')).toEqual(note2midi('G4'));
|
||||
expect(stepInNamedScale(0, 'F major', 'A4')).toEqual(note2midi('A4'));
|
||||
expect(stepInNamedScale(0, 'F major', 'Bb4')).toEqual(note2midi('Bb4'));
|
||||
expect(stepInNamedScale(0, 'F major', 'C4')).toEqual(note2midi('C4'));
|
||||
|
||||
expect(stepInNamedScale(1, 'F major', 'C4')).toEqual(note2midi('D4'));
|
||||
expect(stepInNamedScale(1, 'F major', 'C3')).toEqual(note2midi('D3'));
|
||||
expect(stepInNamedScale(1, 'F minor', 'D4')).toEqual(note2midi('Eb4'));
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||
import { register, _mod, silence, logger, pure, isNote } from '@strudel.cycles/core';
|
||||
import { stepInNamedScale } from './tonleiter.mjs';
|
||||
|
||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||
|
||||
@ -184,7 +185,12 @@ export const scale = register('scale', function (scale, pat) {
|
||||
return silence;
|
||||
}
|
||||
try {
|
||||
const note = scaleStep(asNumber, scale);
|
||||
let note;
|
||||
if (value.anchor) {
|
||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
||||
} else {
|
||||
note = scaleStep(asNumber, scale);
|
||||
}
|
||||
value = pure(isObject ? { ...value, note } : note);
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { isNote, isNoteWithOctave, _mod, noteToMidi, tokenizeNote } from '@strudel.cycles/core';
|
||||
import { Interval } from '@tonaljs/tonal';
|
||||
import { Interval, Scale } from '@tonaljs/tonal';
|
||||
|
||||
// https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515
|
||||
|
||||
@ -29,6 +29,7 @@ export function tokenizeChord(chord) {
|
||||
}
|
||||
export const note2pc = (note) => note.match(/^[A-G][#b]?/i)[0];
|
||||
export const note2oct = (note) => tokenizeNote(note)[2];
|
||||
export const note2midi = noteToMidi;
|
||||
|
||||
export const note2chroma = (note) => {
|
||||
return pc2chroma(note2pc(note));
|
||||
@ -60,12 +61,12 @@ export const step2semitones = (x) => {
|
||||
return Interval.semitones(x);
|
||||
};
|
||||
|
||||
export const x2midi = (x) => {
|
||||
export const x2midi = (x, defaultOctave) => {
|
||||
if (typeof x === 'number') {
|
||||
return x;
|
||||
}
|
||||
if (typeof x === 'string') {
|
||||
return noteToMidi(x);
|
||||
return noteToMidi(x, defaultOctave);
|
||||
}
|
||||
};
|
||||
|
||||
@ -83,18 +84,63 @@ export function scaleStep(notes, offset, octaves = 1) {
|
||||
return notes[offset] + octOffset;
|
||||
}
|
||||
|
||||
export function nearestNumberIndex(target, numbers, preferHigher) {
|
||||
let bestIndex = 0,
|
||||
bestDiff = Infinity;
|
||||
numbers.forEach((s, i) => {
|
||||
const diff = Math.abs(s - target);
|
||||
// preferHigher only works if numbers are sorted in ascending order!
|
||||
if ((!preferHigher && diff < bestDiff) || (preferHigher && diff <= bestDiff)) {
|
||||
bestIndex = i;
|
||||
bestDiff = diff;
|
||||
}
|
||||
});
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
let scaleSteps = {}; // [scaleName]: semitones[]
|
||||
|
||||
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||
let [root, scaleName] = Scale.tokenize(scale);
|
||||
const rootMidi = x2midi(root);
|
||||
const rootChroma = midi2chroma(rootMidi);
|
||||
if (!scaleSteps[scaleName]) {
|
||||
let { intervals } = Scale.get(`C ${scaleName}`);
|
||||
// cache result
|
||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||
}
|
||||
const steps = scaleSteps[scaleName];
|
||||
if (!steps) {
|
||||
return null;
|
||||
}
|
||||
let transpose = rootMidi;
|
||||
if (anchor) {
|
||||
anchor = x2midi(anchor, 3);
|
||||
const anchorChroma = midi2chroma(anchor);
|
||||
const anchorDiff = _mod(anchorChroma - rootChroma, 12);
|
||||
const zeroIndex = nearestNumberIndex(anchorDiff, steps, preferHigher);
|
||||
step = step + zeroIndex;
|
||||
transpose = anchor - anchorDiff;
|
||||
}
|
||||
const octOffset = Math.floor(step / steps.length) * 12;
|
||||
step = _mod(step, steps.length);
|
||||
const targetMidi = steps[step] + transpose;
|
||||
return targetMidi + octOffset;
|
||||
}
|
||||
|
||||
// different ways to resolve the note to compare the anchor to (see renderVoicing)
|
||||
let modeTarget = {
|
||||
below: (v) => v.slice(-1)[0],
|
||||
duck: (v) => v.slice(-1)[0],
|
||||
above: (v) => v[0],
|
||||
root: (v) => v[0],
|
||||
};
|
||||
|
||||
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
|
||||
const [root, symbol] = tokenizeChord(chord);
|
||||
const rootChroma = pc2chroma(root);
|
||||
anchor = anchor?.note || anchor;
|
||||
const anchorChroma = pitch2chroma(anchor);
|
||||
anchor = x2midi(anchor?.note || anchor, 4);
|
||||
const anchorChroma = midi2chroma(anchor);
|
||||
const voicings = dictionary[symbol].map((voicing) =>
|
||||
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
|
||||
);
|
||||
@ -110,18 +156,21 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below'
|
||||
}
|
||||
return diff;
|
||||
});
|
||||
if (mode === 'root') {
|
||||
bestIndex = 0;
|
||||
}
|
||||
|
||||
const octDiff = Math.ceil(offset / voicings.length) * 12;
|
||||
const indexWithOffset = _mod(bestIndex + offset, voicings.length);
|
||||
const voicing = voicings[indexWithOffset];
|
||||
const targetStep = modeTarget[mode](voicing);
|
||||
const anchorMidi = noteToMidi(anchor, 4) - chromaDiffs[indexWithOffset] + octDiff;
|
||||
const anchorMidi = anchor - chromaDiffs[indexWithOffset] + octDiff;
|
||||
|
||||
const voicingMidi = voicing.map((v) => anchorMidi - targetStep + v);
|
||||
let notes = voicingMidi.map((n) => midi2note(n));
|
||||
|
||||
if (mode === 'duck') {
|
||||
notes = notes.filter((_, i) => voicingMidi[i] !== noteToMidi(anchor));
|
||||
notes = notes.filter((_, i) => voicingMidi[i] !== anchor);
|
||||
}
|
||||
if (n !== undefined) {
|
||||
return [scaleStep(notes, n, octaves)];
|
||||
|
||||
1059
pnpm-lock.yaml
generated
1059
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getMetadata } from '../website/src/pages/metadata_parser';
|
||||
import { getMetadata } from '../website/src/metadata_parser';
|
||||
|
||||
describe.concurrent('Metadata parser', () => {
|
||||
it('loads a tag from inline comment', async () => {
|
||||
|
||||
@ -71,7 +71,7 @@ let _videos = [
|
||||
params: 'start=1278',
|
||||
},
|
||||
{
|
||||
title: 'Zach B @ (Algo|Afro) Futures 2023',
|
||||
title: 'Zach B-B @ (Algo|Afro) Futures 2023',
|
||||
id: 'zUoZvkZ3J7Q',
|
||||
params: 'start=2547',
|
||||
},
|
||||
|
||||
10
website/src/my_patterns.js
Normal file
10
website/src/my_patterns.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { getMetadata } from './metadata_parser';
|
||||
|
||||
export function getMyPatterns() {
|
||||
const my = import.meta.glob('../../my-patterns/**', { as: 'raw', eager: true });
|
||||
return Object.fromEntries(
|
||||
Object.entries(my)
|
||||
.filter(([name]) => name.endsWith('.txt'))
|
||||
.map(([name, raw]) => [getMetadata(raw)['title'] || name.split('/').slice(-1), raw]),
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
import * as tunes from '../../../src/repl/tunes.mjs';
|
||||
import HeadCommon from '../../components/HeadCommon.astro';
|
||||
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
import { getMetadata } from '../../metadata_parser';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
@ -25,3 +25,4 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
}
|
||||
</div>
|
||||
</body>
|
||||
../../metadata_parser
|
||||
@ -2,9 +2,9 @@ import { createCanvas } from 'canvas';
|
||||
import { pianoroll } from '@strudel.cycles/core';
|
||||
import { evaluate } from '@strudel.cycles/transpiler';
|
||||
import '../../../../test/runtime.mjs';
|
||||
import { getMyPatterns } from './list.json';
|
||||
import { getMyPatterns } from '../../my_patterns';
|
||||
|
||||
export async function get({ params, request }) {
|
||||
export async function GET({ params, request }) {
|
||||
const patterns = await getMyPatterns();
|
||||
const { name } = params;
|
||||
const tune = patterns[name];
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
import { getMyPatterns } from './list.json';
|
||||
import { getMyPatterns } from '../../my_patterns.js';
|
||||
|
||||
import { Content } from '../../../../my-patterns/README.md';
|
||||
import HeadCommon from '../../components/HeadCommon.astro';
|
||||
@ -37,3 +37,4 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
}
|
||||
</div>
|
||||
</body>
|
||||
../../list.json
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
|
||||
export function getMyPatterns() {
|
||||
const my = import.meta.glob('../../../../my-patterns/**', { as: 'raw', eager: true });
|
||||
return Object.fromEntries(
|
||||
Object.entries(my)
|
||||
.filter(([name]) => name.endsWith('.txt'))
|
||||
.map(([name, raw]) => [getMetadata(raw)['title'] || name.split('/').slice(-1), raw]),
|
||||
);
|
||||
}
|
||||
|
||||
export async function get() {
|
||||
const all = await getMyPatterns();
|
||||
return {
|
||||
body: JSON.stringify(all),
|
||||
};
|
||||
}
|
||||
@ -8,9 +8,10 @@ import { code2hash, getDrawContext, logger, silence } from '@strudel.cycles/core
|
||||
import cx from '@src/cx.mjs';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import { getAudioContext, initAudioOnFirstClick, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { defaultAudioDeviceName, getAudioDevices, setAudioDevice } from './panel/AudioDeviceSelector';
|
||||
import { defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { getAudioDevices, setAudioDevice } from './util.mjs';
|
||||
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
initUserCode,
|
||||
setActivePattern,
|
||||
@ -29,12 +30,11 @@ import Loader from './Loader';
|
||||
import { Panel } from './panel/Panel';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { prebake } from './prebake.mjs';
|
||||
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
|
||||
import { getRandomTune, initCode, loadModules, shareCode, ReplContext } from './util.mjs';
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import './Repl.css';
|
||||
|
||||
const { code: randomTune, name } = getRandomTune();
|
||||
export const ReplContext = createContext(null);
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
|
||||
|
||||
@ -1,42 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { getAudioDevices, setAudioDevice } from '../util.mjs';
|
||||
import { SelectInput } from './SelectInput';
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
|
||||
const initdevices = new Map();
|
||||
export const defaultAudioDeviceName = 'System Standard';
|
||||
|
||||
export const getAudioDevices = async () => {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
|
||||
const devicesMap = new Map();
|
||||
devicesMap.set(defaultAudioDeviceName, '');
|
||||
mediaDevices.forEach((device) => {
|
||||
devicesMap.set(device.label, device.deviceId);
|
||||
});
|
||||
return devicesMap;
|
||||
};
|
||||
|
||||
export const setAudioDevice = async (id) => {
|
||||
let audioCtx = getAudioContext();
|
||||
if (audioCtx.sinkId === id) {
|
||||
return;
|
||||
}
|
||||
await audioCtx.suspend();
|
||||
await audioCtx.close();
|
||||
audioCtx = setDefaultAudioContext();
|
||||
await audioCtx.resume();
|
||||
const isValidID = (id ?? '').length > 0;
|
||||
if (isValidID) {
|
||||
try {
|
||||
await audioCtx.setSinkId(id);
|
||||
} catch {
|
||||
logger('failed to set audio interface', 'warning');
|
||||
}
|
||||
}
|
||||
initializeAudioOutput();
|
||||
};
|
||||
|
||||
// Allows the user to select an audio interface for Strudel to play through
|
||||
export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { controls, evalScope, hash2code, logger } from '@strudel.cycles/core';
|
||||
import { settingPatterns } from '../settings.mjs';
|
||||
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext } from '@strudel.cycles/webaudio';
|
||||
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import './Repl.css';
|
||||
import * as tunes from './tunes.mjs';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { createContext } from 'react';
|
||||
|
||||
// Create a single supabase client for interacting with your database
|
||||
const supabase = createClient(
|
||||
@ -110,3 +113,37 @@ export async function shareCode(codeToShare) {
|
||||
logger(message);
|
||||
}
|
||||
}
|
||||
|
||||
export const ReplContext = createContext(null);
|
||||
|
||||
export const getAudioDevices = async () => {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
|
||||
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
|
||||
const devicesMap = new Map();
|
||||
devicesMap.set(defaultAudioDeviceName, '');
|
||||
mediaDevices.forEach((device) => {
|
||||
devicesMap.set(device.label, device.deviceId);
|
||||
});
|
||||
return devicesMap;
|
||||
};
|
||||
|
||||
export const setAudioDevice = async (id) => {
|
||||
let audioCtx = getAudioContext();
|
||||
if (audioCtx.sinkId === id) {
|
||||
return;
|
||||
}
|
||||
await audioCtx.suspend();
|
||||
await audioCtx.close();
|
||||
audioCtx = setDefaultAudioContext();
|
||||
await audioCtx.resume();
|
||||
const isValidID = (id ?? '').length > 0;
|
||||
if (isValidID) {
|
||||
try {
|
||||
await audioCtx.setSinkId(id);
|
||||
} catch {
|
||||
logger('failed to set audio interface', 'warning');
|
||||
}
|
||||
}
|
||||
initializeAudioOutput();
|
||||
};
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { persistentMap, persistentAtom } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { register } from '@strudel.cycles/core';
|
||||
import { defaultAudioDeviceName } from './repl/panel/AudioDeviceSelector';
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
import * as tunes from './repl/tunes.mjs';
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
|
||||
export const defaultAudioDeviceName = 'System Standard';
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user