diff --git a/README.md b/README.md index 09e3eff..81b9eed 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ # Obsidian Metronome 🎵 -Use this plugin to add interactive metronomes to your notes. Use just the visual metronome or turn on a click or beep. Customize the tempo, meter, sound, size, and more. Pair it with [obsidian-plugin-abcjs](https://github.com/TilBlechschmidt/obsidian-plugin-abcjs) to have your score and metronome living in harmony in a single note! 🎵 +Use this plugin to add interactive metronomes to your notes. Use just the visual metronome or turn on a click or beep. Customize the tempo, meter, instrument, size, and more. Pair it with [obsidian-plugin-abcjs](https://github.com/TilBlechschmidt/obsidian-plugin-abcjs) to have your score and metronome living in harmony in a single note! + +This markdown in your Obsidian note: + +````markdown +# My Metronome + +```metronome +bpm: 120 +meter: 3/4 +size: medium +``` +```` + +Produces this interactive metronome: ![Kiku](images/demo-1.gif) @@ -8,13 +22,16 @@ Use this plugin to add interactive metronomes to your notes. Use just the visual I use [obsidian-plugin-abcjs](https://github.com/TilBlechschmidt/obsidian-plugin-abcjs) for writing out music notation in Obsidian and sometimes during live performances I have a need to know what tempo to play a song. This plugin provides a visual metronome that I can have right alongside my music that serves as a great reminder of the correct tempo for a live performance. During practice, I can unmute it and practice alongside it as it clicks on the beat. -## Getting Started +## Install 1. In Obsidian, go to Settings > Community Plugins and disable safe mode if it is enabled. 1. Select **Browse** and search for the **Metronome** plugin. 1. Install the plugin. 1. Enable the plugin. -1. Open a note and place a code block in your note like the one below. Give it a `bpm` ([beats per minute](https://en.wikipedia.org/wiki/Tempo)) for your tempo. In the example below, I've set `bpm` to `120`. + +## Usage + +Open a note and place a code block in your note like the one below. Give it a `bpm` ([beats per minute](https://en.wikipedia.org/wiki/Tempo)) for your tempo. In the example below, I've set `bpm` to `120`. ````markdown # My basic metronome @@ -37,9 +54,9 @@ You can customize the metronome further: bpm: 120 meter: 12/8 size: large -sound: beep -beepTick: A5 -beepTock: A4 +instrument: beep +tickNotes: A5 +tockNotes: A4 ``` ```` @@ -49,23 +66,23 @@ beepTock: A4 These are all the options you can change when creating a metronome. Only the `bpm` option is required. -| Option | Type | Description | Default | -| ----------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -| `bpm` | number | Tempo in [beats per minute](https://en.wikipedia.org/wiki/Tempo). Must be greater than 0. | (required) | -| `meter` | string | The [time signature](https://en.wikipedia.org/wiki/Time_signature) (meter) to play in. The bottom number must be 1, 2, 4, 8, 16, 32, or 64. Examples: 4/4, 3/4, 6/8, 12/8. | `4/4` | -| `muted` | `yes` or `no` | Whether or not the metronome's audio starts muted. | `yes` | -| `autoStart` | `yes` or `no` | Whether or not the metronome starts flashing visually right away. If `autoStart` is `yes` and `muted` is `no`, then the metronome's sound will also start playing immediately. | `yes` | -| `size` | `small`, `medium`, `large`, or `xlarge` | Control the size of the metronome in the note. | `small` | -| `style` | `pulse`, `pendulum`, `line` | Control the style of the metronome. `pulse` makes the whole area flash color. `pendulum` shows an illustration of a metronome with a swinging pendulum (works best with `large` and up sizes). `line` shows a vertical line moving left and right (works best with `large` and up sizes). | `pulse` | -| `sound` | `click` or `beep` | Control whether the metronome clicks or beeps when unmuted. | `click` | -| `beepTick` | string | When `sound` is set to `beep`, this determines the pitch of the **downbeat**, specified in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation). (For example, middle C is C4.) Multiple tones can be played at the same time by providing a comma-separated list of tones. If there is no meter, every beat is considered a downbeat. | `C6`
`F5,A5,C5` | -| `beepTock` | string | When `sound` is set to `beep`, this determines the pitch of the **upbeat**, specified in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation). (For example, middle C is C4.) Multiple tones can be played at the same time by providing a comma-separated list of tones. If there is no meter, every beat is considered a downbeat. | `C5`
`F4,A4,C4 ` | +| Option | Type | Description | Default | +| ------------ | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | +| `bpm` | number | Tempo in [beats per minute](https://en.wikipedia.org/wiki/Tempo). Must be greater than 0. | (required) | +| `meter` | string | The [time signature](https://en.wikipedia.org/wiki/Time_signature) (meter) to play in. The bottom number must be 1, 2, 4, 8, 16, 32, or 64. Examples: 4/4, 3/4, 6/8, 12/8. | `4/4` | +| `muted` | `yes` or `no` | Whether or not the metronome's audio starts muted. | `yes` | +| `autoStart` | `yes` or `no` | Whether or not the metronome starts flashing visually right away. If `autoStart` is `yes` and `muted` is `no`, then the metronome's sound will also start playing immediately. | `yes` | +| `size` | `small`, `medium`, `large`, or `xlarge` | Control the size of the metronome in the note. | `small` | +| `style` | `pulse`, `pendulum`, `line` | Control the style of the metronome. `pulse` makes the whole area flash color. `pendulum` shows an illustration of a metronome with a swinging pendulum (works best with `large` and up sizes). `line` shows a vertical line moving left and right (works best with `large` and up sizes). | `pulse` | +| `instrument` | `click`, `beep`, `AMSynth`, `DuoSynth`, `FMSynth`, `MembraneSynth`, `MetalSynth`, `MonoSynth`, or `PluckSynth` | Change the metronome's instrument. | `click` | +| `tickNotes` | string | This determines the note(s) played on the **downbeat**, specified in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation). (For example, middle C is C4.) Multiple tones can be played at the same time by providing a comma-separated list of tones. If there is no meter, every beat is considered a downbeat. Has no effect when `instrument` is `click`. | `C6`
`F5,A5,C5` | +| `tockNotes` | string | This determines the note(s) played on the **upbeat**, specified in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation). (For example, middle C is C4.) Multiple tones can be played at the same time by providing a comma-separated list of tones. If there is no meter, every beat is considered a downbeat. Has no effect when `instrument` is `click`. | `C5`
`F4,A4,C4 ` | ## Examples -### Small metronome in 6/8 time that does not autostart +### Setting `autoStart: no` on a metronome -Looks great in dark mode too! +If a metronome has `autoStart: no`, it won't start flashing when you open the note. You can also edit metronomes inline using Obsidian's live preview feature. ![](images/demo-4.gif) @@ -77,15 +94,88 @@ The metronome works great when placed alongside music notation using the [obsidi ### Play multiple tones at once -Set `beepTick` and `beepTock` to a comma-separated list of tones to play those tones all at once in a chord. +Set `tickNotes` and `tockNotes` to a comma-separated list of tones to play those tones all at once in a chord. ````markdown ```metronome bpm: 80 meter: 4/4 -sound:beep -beepTick: F4,A4,C4 -beepTock: D4,F4,A4 +instrument:beep +tickNotes: F4,A4,C4 +tockNotes: D4,F4,A4 +``` +```` + +### Using different instruments and tones + +````markdown +```metronome +bpm: 90 +meter: 4/4 +instrument: AMSynth +tickNotes: C5,E5 +tockNotes: D5,F5 +``` +```` + +````markdown +```metronome +bpm: 90 +meter: 4/4 +instrument: DuoSynth +muted: no +tickNotes: A3 +tockNotes: B3 +``` +```` + +````markdown +```metronome +bpm: 90 +meter: 4/4 +instrument: FMSynth +tickNotes: A4 +tockNotes: C#5,E5 +``` +```` + +````markdown +```metronome +bpm: 90 +meter: 4/4 +instrument: MembraneSynth +tickNotes: B6 +tockNotes: B5 +``` +```` + +````markdown +```metronome +bpm: 90 +meter: 4/4 +instrument: MetalSynth +tickNotes: G3 +tockNotes: D0 +``` +```` + +````markdown +```metronome +bpm: 90 +meter: 4/4 +instrument: MonoSynth +tickNotes: C5,E5,G5 +tockNotes: F5,A6,C6 +``` +```` + +````markdown +```metronome +bpm: 90 +meter: 4/4 +instrument: PluckSynth +tickNotes: A4 +tockNotes: E4 ``` ```` @@ -93,8 +183,7 @@ beepTock: D4,F4,A4 ````markdown ```metronome -bpm: 68 -meter: 3/4 +bpm: 125 size: large style: pulse ``` @@ -108,8 +197,7 @@ Works best on `large` and `xlarge` sizes. ````markdown ```metronome -bpm: 68 -meter: 3/4 +bpm: 125 size: large style: pendulum ``` @@ -119,15 +207,30 @@ style: pendulum ### `line` style -![](images/demo-8.gif) - Works best on `large` and `xlarge` sizes. ````markdown ```metronome -bpm: 68 -meter: 3/4 +bpm: 125 size: large style: line ``` ```` + +![](images/demo-8.gif) + +### Works with themes + +The metronome works great with Obsidian's themes in light or dark mode. + +Theme: Obsidian Default (Dark) + +![](images/demo-theme-obsidian-dark.gif) + +Theme: Minimal (Light) + +![](images/demo-theme-minimal-light.gif) + +Theme: [Obsidian Atom](https://github.com/kognise/obsidian-atom) (Dark) + +![](images/demo-theme-atom-dark.gif) diff --git a/images/demo-1.gif b/images/demo-1.gif index 30d1397..4ffb2c1 100644 Binary files a/images/demo-1.gif and b/images/demo-1.gif differ diff --git a/images/demo-2.gif b/images/demo-2.gif index b28350a..2a1de9a 100644 Binary files a/images/demo-2.gif and b/images/demo-2.gif differ diff --git a/images/demo-3.gif b/images/demo-3.gif index 8df2417..ca23903 100644 Binary files a/images/demo-3.gif and b/images/demo-3.gif differ diff --git a/images/demo-4.gif b/images/demo-4.gif index 886b6af..dd3ffbc 100644 Binary files a/images/demo-4.gif and b/images/demo-4.gif differ diff --git a/images/demo-5.gif b/images/demo-5.gif index 840d616..4122942 100644 Binary files a/images/demo-5.gif and b/images/demo-5.gif differ diff --git a/images/demo-6.gif b/images/demo-6.gif index c101594..4e01ad6 100644 Binary files a/images/demo-6.gif and b/images/demo-6.gif differ diff --git a/images/demo-7.gif b/images/demo-7.gif index 9ea4322..1d2e28e 100644 Binary files a/images/demo-7.gif and b/images/demo-7.gif differ diff --git a/images/demo-8.gif b/images/demo-8.gif index d3095ed..ca5f561 100644 Binary files a/images/demo-8.gif and b/images/demo-8.gif differ diff --git a/images/demo-theme-atom-dark.gif b/images/demo-theme-atom-dark.gif new file mode 100644 index 0000000..fab820f Binary files /dev/null and b/images/demo-theme-atom-dark.gif differ diff --git a/images/demo-theme-minimal-light.gif b/images/demo-theme-minimal-light.gif new file mode 100644 index 0000000..3fdf107 Binary files /dev/null and b/images/demo-theme-minimal-light.gif differ diff --git a/images/demo-theme-obsidian-dark.gif b/images/demo-theme-obsidian-dark.gif new file mode 100644 index 0000000..7befc89 Binary files /dev/null and b/images/demo-theme-obsidian-dark.gif differ diff --git a/src/components/Metronome.vue b/src/components/Metronome.vue index c1f2890..055b719 100644 --- a/src/components/Metronome.vue +++ b/src/components/Metronome.vue @@ -6,12 +6,7 @@ import MetronomeToggle from "./MetronomeToggle.vue"; import { ref, watch, toRefs, onBeforeUnmount } from "vue"; import MetronomeIcon from "../components/MetronomeIcon.vue"; import StyleLine from "../components/StyleLine.vue"; -import { - tick as playTick, - tickUpbeat as playTickUpbeat, - tock as playTock, - beep as playBeep, -} from "../sounds"; +import { playTick, playTickUpbeat, playTock, playSynth } from "../sounds"; import { useTick } from "../hooks/useTick"; import { useParentMarkdownWrapperVisibilityWatcher } from "../hooks/useParentMarkdownWrapperVisibilityWatcher"; import { useCSSAnimationSynchronizer } from "../hooks/useCSSAnimationSynchronizer"; @@ -23,9 +18,9 @@ const props = defineProps<{ size: MetronomeCodeBlockParameters["size"]; style: MetronomeCodeBlockParameters["style"]; autoStart: MetronomeCodeBlockParameters["autoStart"]; - sound: MetronomeCodeBlockParameters["sound"]; - beepTick: MetronomeCodeBlockParameters["beepTick"]; - beepTock: MetronomeCodeBlockParameters["beepTock"]; + instrument: MetronomeCodeBlockParameters["instrument"]; + tickNotes: MetronomeCodeBlockParameters["tickNotes"]; + tockNotes: MetronomeCodeBlockParameters["tockNotes"]; }>(); const metronome = ref(null); @@ -47,17 +42,23 @@ const haltAnimationStyle = useCSSAnimationSynchronizer({ onTick( () => !muted.value && - (props.sound === "beep" ? playBeep(props.beepTick) : playTick()) + (props.instrument === "click" + ? playTick() + : playSynth(props.tickNotes, props.instrument)) ); onTickAlternate( () => !muted.value && - (props.sound === "beep" ? playBeep(props.beepTick) : playTickUpbeat()) + (props.instrument === "click" + ? playTickUpbeat() + : playSynth(props.tockNotes, props.instrument)) ); onTock( () => !muted.value && - (props.sound === "beep" ? playBeep(props.beepTock) : playTock()) + (props.instrument === "click" + ? playTock() + : playSynth(props.tockNotes, props.instrument)) ); const emit = defineEmits(["didCreateInterval"]); diff --git a/src/main.ts b/src/main.ts index 8e33e03..49eca23 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,10 @@ import Metronome from "./components/Metronome.vue"; import { App, createApp } from "vue"; import { Meter } from "./models/Meter"; import { MetronomeSize, isMetronomeSize } from "./models/MetronomeSize"; -import { MetronomeSound, isMetronomeSound } from "./models/MetronomeSound"; +import { + MetronomeInstrument, + isMetronomeInstrument, +} from "./models/MetronomeInstrument"; import { MetronomeStyle, isMetronomeStyle } from "./models/MetronomeStyle"; import { Frequency } from "tone/build/esm/core/type/Units"; import { MetronomeBPM } from "./models/MetronomeBPM"; @@ -15,9 +18,9 @@ export interface MetronomeCodeBlockParameters { size: MetronomeSize; style: MetronomeStyle; autoStart: boolean; - sound: MetronomeSound; - beepTick: Frequency[]; - beepTock: Frequency[]; + instrument: MetronomeInstrument; + tickNotes: Frequency[]; + tockNotes: Frequency[]; } export default class MetronomePlugin extends Plugin { @@ -82,11 +85,13 @@ export default class MetronomePlugin extends Plugin { size: isMetronomeSize(values.size) ? values.size : "small", style: isMetronomeStyle(values.style) ? values.style : "pulse", autoStart: values.autostart ? values.autoStart === "yes" : null, - sound: isMetronomeSound(values.sound) ? values.sound : "click", - beepTick: parseMaybeCommaSeparatedToArray(values.beeptick) || [ + instrument: isMetronomeInstrument(values.instrument) + ? values.instrument + : "click", + tickNotes: parseMaybeCommaSeparatedToArray(values.ticknotes) || [ "C6", ], - beepTock: parseMaybeCommaSeparatedToArray(values.beeptock) || [ + tockNotes: parseMaybeCommaSeparatedToArray(values.tocknotes) || [ "C5", ], }; diff --git a/src/models/MetronomeInstrument.ts b/src/models/MetronomeInstrument.ts new file mode 100644 index 0000000..56d2b4f --- /dev/null +++ b/src/models/MetronomeInstrument.ts @@ -0,0 +1,41 @@ +export type MetronomeInstrument = + | "click" + | "beep" + | "AMSynth" + | "DuoSynth" + | "FMSynth" + | "MembraneSynth" + | "MetalSynth" + | "MonoSynth" + | "NoiseSynth" + | "PluckSynth" + | "Synth"; + +export const defaultMetronomeInstrument = "beep"; + +export function isMetronomeInstrument(inputInstrument: string): boolean { + return [ + "click", + "beep", + "AMSynth", + "DuoSynth", + "FMSynth", + "MembraneSynth", + "MetalSynth", + "MonoSynth", + "PluckSynth", + ].includes(inputInstrument); +} + +export const normalizeInstrumentNameForToneJS = ( + instrument: MetronomeInstrument +) => { + switch (instrument) { + case "click": + return "NoiseSynth"; + case "beep": + return "Synth"; + default: + return instrument; + } +}; diff --git a/src/models/MetronomeSound.ts b/src/models/MetronomeSound.ts deleted file mode 100644 index c561eaa..0000000 --- a/src/models/MetronomeSound.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type MetronomeSound = "click" | "beep"; - -export function isMetronomeSound( - inputSound: string -): inputSound is MetronomeSound { - return ["click", "beep"].includes(inputSound); -} diff --git a/src/sounds.ts b/src/sounds.ts index e128ffd..4910569 100644 --- a/src/sounds.ts +++ b/src/sounds.ts @@ -1,7 +1,56 @@ import * as Tone from "tone/build/esm"; +import { PolySynth } from "tone/build/esm"; import { Frequency } from "tone/build/esm/core/type/Units"; +import { + Instrument, + InstrumentOptions, +} from "tone/build/esm/instrument/Instrument"; +import { + MetronomeInstrument, + normalizeInstrumentNameForToneJS, +} from "./models/MetronomeInstrument"; -const beepSynth = new Tone.PolySynth().toDestination(); +const synthCache: { + [key: string]: Tone.PolySynth | Instrument; +} = {}; +const getSynth = (instrument: MetronomeInstrument) => { + if (!synthCache[instrument]) { + let synth; + switch (instrument) { + case "AMSynth": + synth = new Tone.PolySynth(Tone.AMSynth); + break; + case "DuoSynth": + synth = new Tone.PolySynth(Tone.DuoSynth); + break; + case "FMSynth": + synth = new Tone.PolySynth(Tone.FMSynth); + break; + case "MembraneSynth": + synth = new Tone.PolySynth(Tone.MembraneSynth); + break; + case "MetalSynth": + synth = new Tone.PolySynth(Tone.MetalSynth); + break; + case "MonoSynth": + synth = new Tone.PolySynth(Tone.MonoSynth); + break; + case "NoiseSynth": + synth = new Tone.NoiseSynth(); + break; + case "PluckSynth": + synth = new Tone.PluckSynth(); + break; + case "Synth": + default: + synth = new Tone.PolySynth(Tone.Synth); + break; + } + synthCache[instrument] = synth.toDestination(); + } + + return synthCache[instrument]; +}; const whiteNoise = new Tone.NoiseSynth({ noise: { type: "white" }, @@ -15,15 +64,28 @@ const pinkNoise = new Tone.NoiseSynth({ noise: { type: "pink" }, }).toDestination(); -export const tick = () => whiteNoise.triggerAttackRelease("8n"); -export const tock = () => brownNoise.triggerAttackRelease("8n"); -export const tickUpbeat = () => pinkNoise.triggerAttackRelease("8n"); -export const beep = (notes: Frequency[] = ["C4"]) => { +export const playTick = () => whiteNoise.triggerAttackRelease("8n"); +export const playTock = () => brownNoise.triggerAttackRelease("8n"); +export const playTickUpbeat = () => pinkNoise.triggerAttackRelease("8n"); +export const playSynth = ( + notes: Frequency[] = ["C4"], + instrument: MetronomeInstrument = "click" +) => { + const synth = getSynth(normalizeInstrumentNameForToneJS(instrument)); + try { - beepSynth.triggerAttackRelease(notes, "16n"); + if (synth instanceof PolySynth) { + synth.triggerAttackRelease(notes, "16n"); + } else if (synth instanceof Instrument) { + // Mono synth + synth.triggerAttackRelease(notes[0], "16n"); + } } catch (e) { // Probably invalid note value. Play no note instead. - console.error(e); - beepSynth.releaseAll(); + if (synth instanceof PolySynth) { + synth.releaseAll(); + } else if (synth instanceof Instrument) { + synth.triggerRelease(); + } } };