mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-13 09:25:11 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14184993d0 | |||
| c350f262e6 | |||
| 2a2c7437e4 | |||
| 5656cd2fc6 | |||
| b6e42876ad | |||
| 3671344867 | |||
| 9d661808ba | |||
| 6a5d849b3c | |||
| b92d1d09a2 | |||
| c79b2653e7 | |||
| 554b72c916 |
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Note } from '../tonleiter.mjs';
|
||||
|
||||
describe('tonleiter', () => {
|
||||
it('Should tokenize notes', () => {
|
||||
expect(Note.tokenize('c')).toEqual(['c', '']);
|
||||
expect(Note.tokenize('C')).toEqual(['C', '']);
|
||||
expect(Note.tokenize('c#')).toEqual(['c', '#']);
|
||||
expect(Note.tokenize('Bb')).toEqual(['B', 'b']);
|
||||
});
|
||||
});
|
||||
@@ -148,6 +148,9 @@ export const scale = register('scale', function (scale /* : string */, pat) {
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
const { pc, oct = 3 } = Note.get(tonic);
|
||||
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
|
||||
/* const scaleWIthOctave = `${pc + oct} ${scaleName}`;
|
||||
const degree = Scale.degrees(scaleWIthOctave);
|
||||
note = degree(asNumber + (asNumber >= 0 ? 1 : 0)); */
|
||||
}
|
||||
return hap.withValue(() => (isObject ? { ...hap.value, note } : note)).setContext({ ...hap.context, scale });
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
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 accidentalOffset = (accidentals) => {
|
||||
return accidentals.split('#').length - accidentals.split('b').length;
|
||||
};
|
||||
|
||||
const accidentalString = (offset) => {
|
||||
if (offset < 0) {
|
||||
return 'b'.repeat(-offset);
|
||||
}
|
||||
if (offset > 0) {
|
||||
return '#'.repeat(offset);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const Step = {
|
||||
tokenize(step) {
|
||||
const [accidentals, stepNumber] = step.match(/^([#b]*)([1-9]+)$/).slice(1);
|
||||
return [accidentals, parseInt(stepNumber)];
|
||||
},
|
||||
accidentals(step) {
|
||||
return accidentalOffset(Step.tokenize(step)[0]);
|
||||
},
|
||||
};
|
||||
|
||||
Step.tokenize('#11');
|
||||
Step.tokenize('b13');
|
||||
Step.tokenize('bb6');
|
||||
Step.accidentals('3');
|
||||
Step.accidentals('b3');
|
||||
|
||||
export const Note = {
|
||||
// TODO: support octave numbers
|
||||
tokenize(note) {
|
||||
return [note[0], note.slice(1)];
|
||||
},
|
||||
accidentals(note) {
|
||||
return accidentalOffset(this.tokenize(note)[1]);
|
||||
},
|
||||
};
|
||||
|
||||
Note.tokenize('C##');
|
||||
Note.accidentals('C#');
|
||||
Note.accidentals('C##');
|
||||
Note.accidentals('Eb');
|
||||
Note.accidentals('Bbb');
|
||||
|
||||
// TODO: support octave numbers
|
||||
function transpose(note, step) {
|
||||
// example: E, 3
|
||||
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 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('');
|
||||
}
|
||||
|
||||
transpose('F#', '3');
|
||||
transpose('C', '3');
|
||||
transpose('D', '3');
|
||||
transpose('E', '3');
|
||||
transpose('Eb', '3');
|
||||
transpose('Ebb', '3');
|
||||
@@ -31,8 +31,35 @@ export default defineConfig({
|
||||
mdx(options),
|
||||
tailwind(),
|
||||
AstroPWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: 'auto',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg}'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ url }) =>
|
||||
[
|
||||
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
|
||||
/^https:\/\/freesound\.org\/.*/i,
|
||||
/^https:\/\/cdn\.freesound\.org\/.*/i,
|
||||
/^https:\/\/shabda\.ndre\.gr\/.*/i,
|
||||
].some((regex) => regex.test(url)),
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'external-samples',
|
||||
expiration: {
|
||||
maxEntries: 5000,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 14 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
},
|
||||
manifest: {
|
||||
includeAssets: ['favicon.ico', 'icons/apple-icon-180.png', 'favicon.svg'],
|
||||
|
||||
Reference in New Issue
Block a user