Compare commits

..

12 Commits

Author SHA1 Message Date
Felix Roos 751c4b7390 add pregain + move postgain after sends 2024-01-12 18:56:00 +01:00
Felix Roos a053f40e59 Merge pull request #902 from tidalcycles/build-warnings
fix some build warnings
2024-01-12 18:31:41 +01:00
Felix Roos 9a0459fdd3 move non pages out of pages dir 2024-01-12 18:16:57 +01:00
Felix Roos 46a859736a Merge pull request #891 from daslyfe/remove_jsx_exports_for_vite
prevent vite from complaining about additional exports in jsx files
2024-01-12 18:01:55 +01:00
Felix Roos e4524a2be9 Merge pull request #892 from daslyfe/update_vite
Update Vite version so hot reload works properly with newest pnpm version
2024-01-12 17:05:46 +01:00
Felix Roos f3ee1cc947 Merge pull request #886 from tidalcycles/polymeter-slowcat
support , in < >
2024-01-12 15:09:40 +01:00
Felix Roos f768e1eece Merge pull request #898 from tidalcycles/fix-autocomplete
fix: autocomplete / tooltip code example bug
2024-01-10 13:55:39 +01:00
Felix Roos 7157634db0 fix: autocomplete / tooltip code example bug 2024-01-09 22:34:16 +01:00
Jade (Rose) Rowland 86442adb20 prettier 2024-01-06 15:09:09 -05:00
Jade (Rose) Rowland 5e3a37c9e8 init 2024-01-06 14:55:45 -05:00
Jade (Rose) Rowland 8d325b96ea init 2024-01-06 14:49:52 -05:00
Felix Roos ca3eda8fc7 support , in < > 2024-01-03 21:54:10 +01:00
20 changed files with 720 additions and 589 deletions
+7 -1
View File
@@ -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];
/*
+6 -1
View File
@@ -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
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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
+4 -6
View File
@@ -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;
}
+14 -7
View File
@@ -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) {
+2 -14
View File
@@ -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#');
+17 -37
View File
@@ -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")
+603 -456
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 () => {
+10
View 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 -1
View File
@@ -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 -2
View File
@@ -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];
+2 -1
View File
@@ -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
-17
View File
@@ -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),
};
}
+4 -4
View File
@@ -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 -35
View File
@@ -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 }) {
+38 -1
View File
@@ -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();
};
+3 -1
View File
@@ -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();