Skip to content

Commit

Permalink
Merge pull request #580 from roipoussiere/music_metadata
Browse files Browse the repository at this point in the history
Music metadata
  • Loading branch information
felixroos authored Jun 9, 2023
2 parents 44be6ce + 51e8174 commit a3baf07
Show file tree
Hide file tree
Showing 8 changed files with 498 additions and 62 deletions.
246 changes: 246 additions & 0 deletions test/metadata.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { describe, expect, it } from 'vitest';
import { getMetadata } from '../website/src/pages/metadata_parser';

describe.concurrent('Metadata parser', () => {
it('loads a tag from inline comment', async () => {
const tune = `// @title Awesome song`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
});
});

it('loads many tags from inline comments', async () => {
const tune = `// @title Awesome song
// @by Sam`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});

it('loads many tags from one inline comment', async () => {
const tune = `// @title Awesome song @by Sam`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});

it('loads a tag from a block comment', async () => {
const tune = `/* @title Awesome song */`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
});
});

it('loads many tags from a block comment', async () => {
const tune = `/*
@title Awesome song
@by Sam
*/`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});

it('loads many tags from many block comments', async () => {
const tune = `/* @title Awesome song */
/* @by Sam */`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});

it('loads many tags from mixed comments', async () => {
const tune = `/* @title Awesome song */
// @by Sam
`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});

it('loads a title tag with quotes syntax', async () => {
const tune = `// "Awesome song"`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
});
});

it('loads a title tag with quotes syntax among other tags', async () => {
const tune = `// "Awesome song" made @by Sam`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});

it('loads a title tag with quotes syntax from block comment', async () => {
const tune = `/* "Awesome song"
@by Sam */`;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam'],
});
});

it('does not load a title tag with quotes syntax after a prefix', async () => {
const tune = `// I don't care about those "metadata".`;
expect(getMetadata(tune)).toStrictEqual({});
});

it('does not load a title tag with quotes syntax after an other comment', async () => {
const tune = `// I don't care about those
// "metadata"`;
expect(getMetadata(tune)).toStrictEqual({});
});

it('does not load a title tag with quotes syntax after other tags', async () => {
const tune = `/*
@by Sam aka "Lady Strudel"
"Sandyyy"
*/`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam aka "Lady Strudel"', '"Sandyyy"'],
});
});

it('loads a tag list with comma-separated values syntax', async () => {
const tune = `// @by Sam, Sandy`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});

it('loads a tag list with duplicate keys syntax', async () => {
const tune = `// @by Sam
// @by Sandy`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});

it('loads a tag list with duplicate keys syntax, with prefixes', async () => {
const tune = `// song @by Sam
// samples @by Sandy`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});

it('loads many tag lists with duplicate keys syntax, within code', async () => {
const tune = `note("a3 c#4 e4 a4") // @by Sam @license CC0
s("bd hh sd hh") // @by Sandy @license CC BY-NC-SA`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
license: ['CC0', 'CC BY-NC-SA'],
});
});

it('loads a tag list with duplicate keys syntax from block comment', async () => {
const tune = `/* @by Sam
@by Sandy */`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});

it('loads a tag list with newline syntax', async () => {
const tune = `/*
@by Sam
Sandy */`;
expect(getMetadata(tune)).toStrictEqual({
by: ['Sam', 'Sandy'],
});
});

it('loads a multiline tag from block comment', async () => {
const tune = `/*
@details I wrote this song in February 19th, 2023.
It was around midnight and I was lying on
the sofa in the living room.
*/`;
expect(getMetadata(tune)).toStrictEqual({
details:
'I wrote this song in February 19th, 2023. ' +
'It was around midnight and I was lying on the sofa in the living room.',
});
});

it('loads a multiline tag from block comment with duplicate keys', async () => {
const tune = `/*
@details I wrote this song in February 19th, 2023.
@details It was around midnight and I was lying on
the sofa in the living room.
*/`;
expect(getMetadata(tune)).toStrictEqual({
details:
'I wrote this song in February 19th, 2023. ' +
'It was around midnight and I was lying on the sofa in the living room.',
});
});

it('loads a multiline tag from inline comments', async () => {
const tune = `// @details I wrote this song in February 19th, 2023.
// @details It was around midnight and I was lying on
// @details the sofa in the living room.
*/`;
expect(getMetadata(tune)).toStrictEqual({
details:
'I wrote this song in February 19th, 2023. ' +
'It was around midnight and I was lying on the sofa in the living room.',
});
});

it('loads empty tags from inline comments', async () => {
const tune = `// @title
// @by`;
expect(getMetadata(tune)).toStrictEqual({
title: '',
by: [],
});
});

it('loads tags with whitespaces from inline comments', async () => {
const tune = ` // @title Awesome song
// @by Sam Tagada `;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam Tagada'],
});
});

it('loads tags with whitespaces from block comment', async () => {
const tune = ` /* @title Awesome song
@by Sam Tagada */ `;
expect(getMetadata(tune)).toStrictEqual({
title: 'Awesome song',
by: ['Sam Tagada'],
});
});

it('loads empty tags from block comment', async () => {
const tune = `/* @title
@by */`;
expect(getMetadata(tune)).toStrictEqual({
title: '',
by: [],
});
});

it('does not load tags if there is not', async () => {
const tune = `note("a3 c#4 e4 a4")`;
expect(getMetadata(tune)).toStrictEqual({});
});

it('does not load code that looks like a metadata tag', async () => {
const tune = `const str1 = '@title Awesome song'`;
// need a lexer to avoid this one, but it's a pretty rare use case:
// const tune = `const str1 = '// @title Awesome song'`;

expect(getMetadata(tune)).toStrictEqual({});
});
});
1 change: 1 addition & 0 deletions website/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'Patterns', link: 'technical-manual/patterns' },
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
{ text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' },
{ text: 'Music metadata', link: 'learn/metadata' },
],
Development: [
{ text: 'REPL', link: 'technical-manual/repl' },
Expand Down
4 changes: 3 additions & 1 deletion website/src/pages/examples/index.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
import * as tunes from '../../../src/repl/tunes.mjs';
import HeadCommon from '../../components/HeadCommon.astro';
import { getMetadata } from '../metadata_parser';
---

<head>
Expand All @@ -12,7 +14,7 @@ import HeadCommon from '../../components/HeadCommon.astro';
Object.entries(tunes).map(([name, tune]) => (
<a class="rounded-md bg-slate-900 hover:bg-slate-700 cursor-pointer relative" href={`./#${btoa(tune)}`}>
<div class="absolute w-full h-full flex justify-center items-center">
<span class="bg-slate-800 p-2 rounded-md text-white">{name}</span>
<span class="bg-slate-800 p-2 rounded-md text-white">{getMetadata(tune)['title'] || name}</span>
</div>
<img src={`./img/example-${name}.png`} />
</a>
Expand Down
3 changes: 3 additions & 0 deletions website/src/pages/learn/code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ It is a handy way to quickly turn code on and off.
Try uncommenting this line by deleting `//` and refreshing the pattern.
You can also use the keyboard shortcut `cmd-/` to toggle comments on and off.

You might noticed that some comments in the REPL samples include some words starting with a "@", like `@by` or `@license`.
Those are just a convention to define some information about the music. We will talk about it in the [Music metadata](/learn/metadata) section.

# Strings

Ok, so what about the content inside the quotes (e.g. `"a3 c#4 e4 a4"`)?
Expand Down
94 changes: 94 additions & 0 deletions website/src/pages/learn/metadata.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Music metadata
layout: ../../layouts/MainLayout.astro
---

import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';

# Music metadata

You can optionally add some music metadata in your Strudel code, by using tags in code comments:

```js
// @title Hey Hoo
// @by Sam Tagada
// @license CC BY-NC-SA
```

Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music.

It is for instance used by the [swatch tool](https://github.com/tidalcycles/strudel/tree/main/my-patterns) to display pattern titles in the [examples page](https://strudel.tidalcycles.org/examples/).

## Alternative syntax

You can also use comment blocks:

```js
/*
@title Hey Hoo
@by Sam Tagada
@license CC BY-NC-SA
*/
```

Or define multiple tags in one line:

```js
// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA
```

The `title` tag has an alternative syntax using quotes (must be defined at the very begining):

```js
// "Hey Hoo" @by Sam Tagada
```

## Tags list

Available tags are:

- `@title`: music title
- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe <https://example.com>`)
- `@license`: music license(s)
- `@details`: some additional information about the music
- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.)
- `@genre`: music genre(s) (pop, jazz, etc)
- `@album`: music album name

## Multiple values

Some of them accepts several values, using the comma or new line separator, or duplicating the tag:

```js
/*
@by Sam Tagada
Jimmy
@genre pop, jazz
@url https://example.com
@url https://example.org
*/
```

You can also add optional prefixes and use tags where you want:

```js
/*
song @by Sam Tagada
samples @by Jimmy
*/
...
note("a3 c#4 e4 a4") // @by Sandy
```

## Multiline

If a tag doesn't accept a list, it can take multi-line values:

```js
/*
@details I wrote this song in February 19th, 2023.
It was around midnight and I was lying on
the sofa in the living room.
*/
```
Loading

0 comments on commit a3baf07

Please sign in to comment.