mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 05:33:13 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 751c4b7390 | |||
| a053f40e59 | |||
| 9a0459fdd3 | |||
| 46a859736a | |||
| e4524a2be9 | |||
| f3ee1cc947 | |||
| f768e1eece | |||
| 7157634db0 | |||
| 86442adb20 | |||
| 5e3a37c9e8 | |||
| 8d325b96ea | |||
| ca3eda8fc7 |
@@ -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];
|
||||
/*
|
||||
|
||||
@@ -87,7 +87,12 @@ const generic_params = [
|
||||
*/
|
||||
['gain'],
|
||||
/**
|
||||
* Gain applied after all effects have been processed.
|
||||
* Gain applied before effect sends (delay, reverb ...)
|
||||
*
|
||||
*/
|
||||
['pregain'],
|
||||
/**
|
||||
* Gain applied after effect sends (delay, reverb ...)
|
||||
*
|
||||
* @name postgain
|
||||
* @example
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -272,7 +272,8 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||
bank,
|
||||
source,
|
||||
gain = 0.8,
|
||||
postgain = 1,
|
||||
pregain = 1,
|
||||
postgain,
|
||||
density = 0.03,
|
||||
// filters
|
||||
ftype = '12db',
|
||||
@@ -470,16 +471,15 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||
chain.push(phaserFX);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = new GainNode(ac, { gain: postgain });
|
||||
chain.push(post);
|
||||
connectToDestination(post, channels);
|
||||
// pre fx gain
|
||||
const pre = new GainNode(ac, { gain: pregain });
|
||||
chain.push(pre);
|
||||
|
||||
// delay
|
||||
let delaySend;
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||
delaySend = effectSend(post, delyNode, delay);
|
||||
delaySend = effectSend(pre, delyNode, delay);
|
||||
}
|
||||
// reverb
|
||||
let reverbSend;
|
||||
@@ -496,9 +496,16 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||
}
|
||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
reverbSend = effectSend(pre, reverbNode, room);
|
||||
}
|
||||
|
||||
let post = pre;
|
||||
if (postgain !== undefined) {
|
||||
// post fx gain
|
||||
post = new GainNode(ac, { gain: postgain });
|
||||
chain.push(post);
|
||||
}
|
||||
connectToDestination(post, channels);
|
||||
// analyser
|
||||
let analyserSend;
|
||||
if (analyze) {
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
|
||||
describe('tonleiter', () => {
|
||||
test('Step ', () => {
|
||||
expect(Step.tokenize('b7')).toEqual(['b', 7]);
|
||||
expect(Step.tokenize('#11')).toEqual(['#', 11]);
|
||||
expect(Step.tokenize('b13')).toEqual(['b', 13]);
|
||||
expect(Step.tokenize('bb6')).toEqual(['bb', 6]);
|
||||
@@ -37,25 +36,14 @@ describe('tonleiter', () => {
|
||||
expect(Step.accidentals('#11')).toEqual(1);
|
||||
});
|
||||
test('Note', () => {
|
||||
expect(Note.tokenize('C3')).toEqual(['C', '', 3]);
|
||||
expect(Note.tokenize('C##')).toEqual(['C', '##', undefined]);
|
||||
expect(Note.tokenize('Bb')).toEqual(['B', 'b', undefined]);
|
||||
expect(Note.tokenize('C##')).toEqual(['C', '##']);
|
||||
expect(Note.tokenize('Bb')).toEqual(['B', 'b']);
|
||||
expect(Note.accidentals('C#')).toEqual(1);
|
||||
expect(Note.accidentals('C##')).toEqual(2);
|
||||
expect(Note.accidentals('Eb')).toEqual(-1);
|
||||
expect(Note.accidentals('Bbb')).toEqual(-2);
|
||||
});
|
||||
test('transpose', () => {
|
||||
expect(transpose('Bb4', '4')).toEqual('Eb5'); // fails -> E###########5
|
||||
expect(transpose('Bb4', '6')).toEqual('G5');
|
||||
|
||||
expect(transpose('D3', 'b7')).toEqual('C4');
|
||||
expect(transpose('C3', 'b7')).toEqual('Bb3');
|
||||
|
||||
expect(transpose('C', 'b7')).toEqual('Bb');
|
||||
expect(transpose('D', 'b7')).toEqual('C');
|
||||
expect(transpose('E', 'b7')).toEqual('D');
|
||||
expect(transpose('E', '7')).toEqual('D#');
|
||||
expect(transpose('F#', '3')).toEqual('A#');
|
||||
expect(transpose('C', '3')).toEqual('E');
|
||||
expect(transpose('D', '3')).toEqual('F#');
|
||||
|
||||
@@ -101,14 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
|
||||
let scaleSteps = {}; // [scaleName]: semitones[]
|
||||
|
||||
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||
let emitNotes = false; // true is experimental
|
||||
let [root, scaleName] = Scale.tokenize(scale);
|
||||
const rootMidi = x2midi(root);
|
||||
const rootChroma = midi2chroma(rootMidi);
|
||||
let intervals;
|
||||
// TODO: don't use Scale.get, just read from a map { [scaleName]: intervals }
|
||||
intervals = Scale.get(`C ${scaleName}`).intervals;
|
||||
if (!scaleSteps[scaleName]) {
|
||||
let { intervals } = Scale.get(`C ${scaleName}`);
|
||||
// cache result
|
||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||
}
|
||||
@@ -116,31 +113,19 @@ export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||
if (!steps) {
|
||||
return null;
|
||||
}
|
||||
let offset = rootMidi;
|
||||
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;
|
||||
offset = anchor - anchorDiff;
|
||||
transpose = anchor - anchorDiff;
|
||||
}
|
||||
let octaves = Math.floor(step / steps.length);
|
||||
const octOffset = Math.floor(step / steps.length) * 12;
|
||||
step = _mod(step, steps.length);
|
||||
if (emitNotes) {
|
||||
// TODO: anchor octave currently has no effect
|
||||
// this branch is for emitting notes
|
||||
const interval = interval2step(intervals[step]);
|
||||
let [pc, acc, oct = 3] = tokenizeNote(root);
|
||||
// oct += octaves;
|
||||
const rootWithOctave = pc + acc + oct;
|
||||
if (anchor) {
|
||||
// octaves = offset / 12 - oct;
|
||||
}
|
||||
let targetNote = transpose(rootWithOctave, interval, octaves);
|
||||
return targetNote;
|
||||
}
|
||||
return steps[step] + offset + octaves * 12;
|
||||
const targetMidi = steps[step] + transpose;
|
||||
return targetMidi + octOffset;
|
||||
}
|
||||
|
||||
// different ways to resolve the note to compare the anchor to (see renderVoicing)
|
||||
@@ -197,9 +182,6 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below'
|
||||
const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7];
|
||||
const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B'];
|
||||
const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
|
||||
const intervalSteps = { P: '', M: '', m: 'b', A: '#', d: 'b' };
|
||||
|
||||
export const interval2step = (interval) => intervalSteps[interval.slice(-1)] + interval.slice(0, -1);
|
||||
|
||||
export const accidentalOffset = (accidentals) => {
|
||||
return accidentals.split('#').length - accidentals.split('b').length;
|
||||
@@ -232,28 +214,26 @@ export const Step = {
|
||||
export const Note = {
|
||||
// TODO: support octave numbers
|
||||
tokenize(note) {
|
||||
return tokenizeNote(note);
|
||||
return [note[0], note.slice(1)];
|
||||
},
|
||||
accidentals(note) {
|
||||
return accidentalOffset(this.tokenize(note)[1]);
|
||||
},
|
||||
};
|
||||
|
||||
export function transpose(note, step, octaveOffset = 0) {
|
||||
// TODO: support octave numbers
|
||||
export function transpose(note, step) {
|
||||
// example: E, 3
|
||||
const stepNumber = Step.tokenize(step)[1]; // 3 / 7
|
||||
const [noteLetter, acc, noteOctave] = Note.tokenize(note); // E / D
|
||||
const noteIndex = noteLetters.indexOf(noteLetter); // 2 "E is C+2" / 1
|
||||
const targetNoteIndex = noteIndex + stepNumber - 1;
|
||||
const targetNote = noteLetters[targetNoteIndex % 7]; // G "G is a third above E" / "C "
|
||||
const rootIndex = notes.indexOf(noteLetter); // 4 "E is 4 semitones above C" / 2
|
||||
const targetIndex = notes.indexOf(targetNote); // 7 "G is 7 semitones above C" / 0
|
||||
const stepNumber = Step.tokenize(step)[1]; // 3
|
||||
const noteLetter = Note.tokenize(note)[0]; // E
|
||||
const noteIndex = noteLetters.indexOf(noteLetter); // 2 "E is C+2"
|
||||
const targetNote = noteLetters[(noteIndex + stepNumber - 1) % 8]; // G "G is a third above E"
|
||||
const rootIndex = notes.indexOf(noteLetter); // 4 "E is 4 semitones above C"
|
||||
const targetIndex = notes.indexOf(targetNote); // 7 "G is 7 semitones above C"
|
||||
const indexOffset = targetIndex - rootIndex; // 3 (E to G is normally a 3 semitones)
|
||||
const stepIndex = steps.indexOf(stepNumber); // 4 ("3" is normally 4 semitones)
|
||||
const accidentalOffset = Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset;
|
||||
const offsetAccidentals = accidentalString(accidentalOffset % 12); // "we need to add a # to to the G to make it a major third from E"
|
||||
const targetOctave = noteOctave ? noteOctave + Math.floor(targetNoteIndex / 7) + octaveOffset : '';
|
||||
return [targetNote, offsetAccidentals].join('') + targetOctave;
|
||||
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
|
||||
return [targetNote, offsetAccidentals].join('');
|
||||
}
|
||||
|
||||
//Note("Bb3").transpose("c3")
|
||||
|
||||
Generated
+603
-456
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 () => {
|
||||
|
||||
@@ -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,
|
||||
@@ -24,12 +25,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();
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@ import { persistentMap, persistentAtom } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { register } from '@strudel.cycles/core';
|
||||
import * as tunes from './repl/tunes.mjs';
|
||||
import { defaultAudioDeviceName } from './repl/panel/AudioDeviceSelector';
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
|
||||
export const defaultAudioDeviceName = 'System Standard';
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
@@ -171,6 +172,7 @@ export function updateUserCode(code) {
|
||||
setActivePattern(example);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activePattern) {
|
||||
// create new user pattern
|
||||
activePattern = newUserPattern();
|
||||
|
||||
Reference in New Issue
Block a user