Skip to content

Commit

Permalink
✨ Start metronome from slide
Browse files Browse the repository at this point in the history
- Drop actions to slide
- Fixed new items getting removed if not in selected template
- Special textboxes are removed when changing template
- Text editor will select the best textbox
- List view textboxes in correct oreder
- Text editor not removing extra items
  • Loading branch information
vassbo committed Jun 12, 2024
1 parent 7687033 commit 40418b8
Show file tree
Hide file tree
Showing 22 changed files with 293 additions and 174 deletions.
2 changes: 2 additions & 0 deletions public/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@
"change_drawer_category": "Change drawer category",
"toggle_drawer": "Toggle drawer",
"slide_actions": "Slide actions",
"item_actions": "Item actions",
"clear_history": "Clear history",
"set_key": "Set key",
"custom_key": "Set custom value",
Expand Down Expand Up @@ -556,6 +557,7 @@
"change_volume": "Change volume",
"start_audio_stream": "Start audio stream",
"start_playlist": "Start playlist",
"start_metronome": "Start metronome",
"start_slide_timers": "Start timers on active slide",
"id_select_output_style": "Select output style by ID",
"change_output_style": "Change output style",
Expand Down
14 changes: 13 additions & 1 deletion src/frontend/components/actions/CustomInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import TextInput from "../inputs/TextInput.svelte"
import MidiValues from "./MidiValues.svelte"
import ChooseStyle from "./specific/ChooseStyle.svelte"
import MetronomeInputs from "../drawer/audio/MetronomeInputs.svelte"
export let inputId: string
export let value
Expand Down Expand Up @@ -59,11 +60,15 @@
</script>

{#if inputId === "output_style"}
<div style="display: flex;flex-direction: column;">
<div class="column">
<ChooseStyle value={value || {}} on:change={(e) => updateValue("", e)} />
</div>
{:else if inputId === "midi"}
<MidiValues midi={value?.midi || {}} type="output" on:change={(e) => updateValue("midi", e)} />
{:else if inputId === "metronome"}
<div class="column">
<MetronomeInputs values={value || { tempo: 120, beats: 4 }} on:change={(e) => updateValue("", e)} volume={false} />
</div>
{:else}
<CombinedInput style={inputId === "midi" ? "flex-direction: column;" : ""}>
{#if inputId === "index"}
Expand Down Expand Up @@ -99,3 +104,10 @@
{/if}
</CombinedInput>
{/if}

<style>
.column {
display: flex;
flex-direction: column;
}
</style>
1 change: 1 addition & 0 deletions src/frontend/components/actions/actionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const actionData = {
change_volume: { name: "actions.change_volume", icon: "volume", input: "volume" },
start_audio_stream: { slideId: "audioStream", name: "actions.start_audio_stream", icon: "audio_stream", input: "id" },
start_playlist: { name: "actions.start_playlist", icon: "playlist", input: "id" },
start_metronome: { name: "actions.start_metronome", icon: "metronome", input: "metronome" },

// TIMERS
start_slide_timers: { slideId: "startTimer", name: "actions.start_slide_timers", icon: "timer" },
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/components/actions/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { TransitionType } from "../../../types/Show"
import { send } from "../../utils/request"
import { updateTransition } from "../../utils/transitions"
import { startMetronome } from "../drawer/audio/metronome"
import { clearAudio, startPlaylist, updateVolume } from "../helpers/audio"
import { displayOutputs } from "../helpers/output"
import {
Expand Down Expand Up @@ -64,6 +65,12 @@ export type API_midi = {
}
defaultValues?: boolean // only used by actions
}
export type API_metronome = {
tempo?: number
beats?: number
volume?: number
// notesPerBeat?: number
}

/// ACTIONS ///

Expand Down Expand Up @@ -125,6 +132,7 @@ export const API_ACTIONS = {
change_volume: (data: API_volume) => updateVolume(data.volume ?? data.gain, data.gain !== undefined),
start_audio_stream: (data: API_id) => startAudioStream(data.id), // BC
start_playlist: (data: API_id) => startPlaylist(data.id),
start_metronome: (data: API_metronome) => startMetronome(data),

// TIMERS
// play / pause playing timers
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/components/context/contextMenus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const contextMenuItems: { [key: string]: ContextMenuItem } = {
chord_list: { label: "edit.chords", icon: "chords", items: ["LOAD_chord_list"] },
custom_key: { label: "actions.custom_key", icon: "edit" },
// ITEM
item_actions: { label: "actions.actions", icon: "actions", items: ["LOAD_item_actions"] },
item_actions: { label: "actions.item_actions", icon: "actions", items: ["LOAD_item_actions"] },
item_bind_to: { label: "actions.bind_to", icon: "bind", items: ["LOAD_bind_item"] },
format: { label: "actions.format", icon: "format", items: ["find_replace", "cut_in_half", "SEPERATOR", "uppercase", "lowercase", "capitalize", "trim"] },
dynamic_values: { label: "actions.dynamic_values", icon: "star", items: ["LOAD_dynamic_values"] },
Expand Down
51 changes: 23 additions & 28 deletions src/frontend/components/drawer/audio/Metronome.svelte
Original file line number Diff line number Diff line change
@@ -1,48 +1,43 @@
<script lang="ts">
import { onMount } from "svelte"
import { dictionary, metronome, outLocked } from "../../../stores"
import { dictionary, metronome, outLocked, playingMetronome } from "../../../stores"
import type { API_metronome } from "../../actions/api"
import Icon from "../../helpers/Icon.svelte"
import T from "../../helpers/T.svelte"
import { clone } from "../../helpers/array"
import Button from "../../inputs/Button.svelte"
import CombinedInput from "../../inputs/CombinedInput.svelte"
import NumberInput from "../../inputs/NumberInput.svelte"
import SelectElem from "../../system/SelectElem.svelte"
import MetronomeInputs from "./MetronomeInputs.svelte"
import { toggleMetronome, updateMetronome } from "./metronome"
function playPause() {
paused = !paused
toggleMetronome()
}
$: updateMetronome(values)
let values: any = {}
let values: API_metronome = {}
let paused = true
onMount(() => {
values = clone($metronome)
paused = !values.timeout
})
$: values = clone($metronome)
$: updatePausedState($playingMetronome)
function updatePausedState(active) {
paused = !active
}
</script>

<div class="scroll">
<Button style="flex: 0" disabled={$outLocked} center title={paused ? $dictionary.media?.play : $dictionary.media?.pause} on:click={playPause}>
<Icon id={paused ? "play" : "pause"} white={paused} size={5} />
</Button>
<SelectElem id="metronome" data={{ tempo: values.tempo || 120, beats: values.beats || 4 }} draggable>
<Button style="width: 100%;" disabled={$outLocked} center title={paused ? $dictionary.media?.play : $dictionary.media?.pause} on:click={playPause}>
<Icon id={paused ? "play" : "pause"} white={paused} size={5} />
</Button>
</SelectElem>

<CombinedInput>
<p><T id="audio.tempo" /> (<T id="audio.bpm" />)</p>
<NumberInput value={values.tempo || 120} min={1} decimals={1} max={320} on:change={(e) => (values.tempo = e.detail)} />
</CombinedInput>
<CombinedInput>
<p><T id="audio.beats" /></p>
<NumberInput value={values.beats || 4} min={1} max={16} on:change={(e) => (values.beats = e.detail)} />
</CombinedInput>
<CombinedInput>
<p><T id="media.volume" /></p>
<NumberInput value={values.volume || 1} min={0.1} max={3} decimals={1} step={0.1} inputMultiplier={100} on:change={(e) => (values.volume = e.detail)} />
</CombinedInput>
<!-- <NumberInput value={values.notesPerBeat} min={1} max={4} on:change={(e) => (values.notesPerBeat = e.detail)} /> -->
<MetronomeInputs
{values}
on:change={(e) => {
values = e.detail
updateMetronome(values)
}}
/>
</div>

<style>
Expand Down
37 changes: 37 additions & 0 deletions src/frontend/components/drawer/audio/MetronomeInputs.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import { createEventDispatcher } from "svelte"
import T from "../../helpers/T.svelte"
import CombinedInput from "../../inputs/CombinedInput.svelte"
import NumberInput from "../../inputs/NumberInput.svelte"
import type { API_metronome } from "../../actions/api"
export let values: API_metronome = {}
export let volume: boolean = true
let dispatch = createEventDispatcher()
function updateValue(key, e) {
let value = Number(e.detail)
console.log(value, Math.floor(value), Math.floor(values.tempo || 0) === (values.tempo || 0))
if (Math.floor(value) === value) value = Math.floor(value) // remove .0
console.log(value)
dispatch("change", { ...values, [key]: value })
}
</script>

<CombinedInput>
<p><T id="audio.tempo" /> (<T id="audio.bpm" />)</p>
<NumberInput value={values.tempo || 120} min={1} decimals={1} fixed={Math.floor(values.tempo || 0) === (values.tempo || 0) ? 0 : 1} max={320} on:change={(e) => updateValue("tempo", e)} />
</CombinedInput>
<CombinedInput>
<p><T id="audio.beats" /></p>
<NumberInput value={values.beats || 4} min={1} max={16} on:change={(e) => updateValue("beats", e)} />
</CombinedInput>
<!-- <NumberInput value={values.notesPerBeat} min={1} max={4} on:change={(e) => (values.notesPerBeat = e.detail)} /> -->
{#if volume}
<CombinedInput>
<p><T id="media.volume" /></p>
<NumberInput value={values.volume || 1} min={0.1} max={3} decimals={1} step={0.1} inputMultiplier={100} on:change={(e) => updateValue("volume", e)} />
</CombinedInput>
{/if}
93 changes: 32 additions & 61 deletions src/frontend/components/drawer/audio/metronome.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
import { get } from "svelte/store"
import { gain, metronome, volume } from "../../../stores"
import { gain, metronome, playingMetronome, volume } from "../../../stores"
import { clone } from "../../helpers/array"
import type { API_metronome } from "../../actions/api"

const audioContext = new AudioContext()

const defaultMetronomeValues = {
// context: null,
timeout: null as any,
tempo: 120, // BPM
beats: 4,
volume: 1,
// notesPerBeat: 1
}
let metronomeValues: any = {}
let metronomeValues: API_metronome = {}

function initializeValues() {
metronomeValues = clone(defaultMetronomeValues)
metronome.set(metronomeValues)
}

export function toggleMetronome() {
if (metronomeValues.timeout) stopMetronome()
if (get(playingMetronome)) stopMetronome()
else startMetronome()
}

export function startMetronome(values = {}) {
if (get(metronome)?.context) metronomeValues = get(metronome)
export function startMetronome(values: API_metronome = {}) {
if (get(metronome)?.tempo) metronomeValues = get(metronome)
if (Object.keys(values).length) {
let oldValues = clone(metronomeValues)
delete oldValues.volume

updateMetronome(values, true)

// return if playing and values are the same
let newValues = clone(values)
delete newValues.volume
if (get(playingMetronome) && JSON.stringify(newValues) === JSON.stringify(oldValues)) return
}

if (!metronomeValues.tempo) initializeValues()
if (metronomeValues.timeout) stopMetronome()
if (Object.keys(values).length) updateMetronome(values, true)
if (get(playingMetronome)) stopMetronome()

initializeMetronome()
}

export function updateMetronome(values, starting: boolean = false) {
if (!values.tempo) values.tempo = metronomeValues.tempo
if (!starting && values.tempo !== metronomeValues.tempo) return startMetronome(values)
export function updateMetronome(values: API_metronome, starting: boolean = false) {
if (!values.tempo) values.tempo = metronomeValues.tempo || defaultMetronomeValues.tempo
if (!starting && get(playingMetronome) && values.tempo !== metronomeValues.tempo) return startMetronome(values)

metronomeValues.tempo = values.tempo
if (values.beats) metronomeValues.beats = values.beats
Expand All @@ -45,9 +55,8 @@ export function updateMetronome(values, starting: boolean = false) {
}

export function stopMetronome() {
clearTimeout(metronomeValues.timeout)
metronomeValues.timeout = null
metronome.set(metronomeValues)
clearTimeout(get(playingMetronome))
playingMetronome.set(null)

startTime = 0
beatsPlayed = 0
Expand Down Expand Up @@ -80,7 +89,7 @@ let startTime = 0
async function initializeMetronome() {
await setAudioBuffers()

let beatsPerSecond = 60 / metronomeValues.tempo
let beatsPerSecond = 60 / (metronomeValues.tempo || defaultMetronomeValues.tempo)
timeBetweenEachBeat = beatsPerSecond

scheduleNextNote()
Expand All @@ -94,11 +103,13 @@ function scheduleNextNote(time = 0, beat = 1) {
return
}

if (beat > metronomeValues.beats) beat = 1
if (beat > (metronomeValues.beats || defaultMetronomeValues.beats)) beat = 1

metronomeValues.timeout = setTimeout(() => {
scheduleNote(beat)
}, (time + timeBetweenEachBeat - preScheduleTime) * 1000)
playingMetronome.set(
setTimeout(() => {
scheduleNote(beat)
}, (time + timeBetweenEachBeat - preScheduleTime) * 1000)
)
}

let beatsPlayed = 0
Expand Down Expand Up @@ -137,45 +148,5 @@ function playNote(time: number, first: boolean = false) {
let accentVolume = 2
let secondaryVolume = 1.75
function getVolume(beatVolume) {
return beatVolume * metronomeValues.volume * get(volume) * get(gain)
return beatVolume * (metronomeValues.volume || 1) * get(volume) * get(gain)
}

// const beepLength: number = 0.05
// function scheduleNoteOsc(beatNumber, time) {
// // audio creator
// let osc = audioContext.createOscillator()
// // volume control
// let gainNode = audioContext.createGain()

// osc.connect(gainNode)
// gainNode.connect(audioContext.destination)

// let mainVolume = getVolume(secondaryVolume)

// if (beatNumber === twelvelet && accentVolume > 0.25) {
// osc.frequency.value = 880.0
// gainNode.gain.value = getVolume(accentVolume)
// } else if (beatNumber % twelvelet === 0) {
// // quarter notes = medium pitch
// osc.frequency.value = 440.0
// gainNode.gain.value = mainVolume
// // } else if (beatNumber % 6 === 0) {
// // // eighth notes
// // osc.frequency.value = 440.0
// // gainNode.gain.value = notesPerBeat > 1 ? mainVolume : 0
// // } else if (beatNumber % 4 === 0) {
// // // triplet notes
// // osc.frequency.value = 300.0
// // gainNode.gain.value = notesPerBeat > 2 ? mainVolume : 0
// // } else if (beatNumber % 3 === 0) {
// // // sixteenth notes = low pitch
// // osc.frequency.value = 220.0
// // gainNode.gain.value = notesPerBeat > 3 ? mainVolume : 0
// } else {
// // don't play the remaining twelvelet notes
// gainNode.gain.value = 0
// }

// osc.start(time)
// osc.stop(time + beepLength)
// }
5 changes: 3 additions & 2 deletions src/frontend/components/drawer/info/AudioMix.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { drawer, gain, metronome, volume } from "../../../stores"
import { drawer, gain, playingMetronome, volume } from "../../../stores"
import Icon from "../../helpers/Icon.svelte"
import T from "../../helpers/T.svelte"
import { updateVolume } from "../../helpers/audio"
Expand All @@ -26,7 +26,8 @@
else openedPage = pageId
}
$: if ($metronome?.timeout) openedPage = "metronome"
$: metronomeActive = !!$playingMetronome
$: if (metronomeActive) openedPage = "metronome"
</script>

<!-- TODO: effects?: https://alemangui.github.io/pizzicato/ -->
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/components/helpers/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { audioAnalyser } from "../output/audioAnalyser"
import { clone, shuffleArray } from "./array"
import { encodeFilePath } from "./media"
import { checkNextAfterMedia } from "./showActions"
import { stopMetronome } from "../drawer/audio/metronome"

export async function playAudio({ path, name = "", audio = null, stream = null }: any, pauseIfPlaying: boolean = true, startAt: number = 0, playMultiple: boolean = false) {
let existing: any = get(playingAudio)[path]
Expand Down Expand Up @@ -285,6 +286,9 @@ export function clearAudio(path: string = "", clearPlaylist: boolean = true) {
// turn off any playlist
if (clearPlaylist && (!path || get(activePlaylist)?.active === path)) activePlaylist.set(null)

// stop playing metronome
if (clearPlaylist && !path) stopMetronome()

// let clearTime = get(transitionData).audio.duration
// TODO: starting audio before previous clear is finished will not start/clear audio
const clearTime = get(special).audio_fade_duration ?? 1.5
Expand Down
Loading

0 comments on commit 40418b8

Please sign in to comment.