Compare commits

..

5 commits
v0.1.0 ... main

9 changed files with 269 additions and 186 deletions

View file

@ -3,13 +3,13 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"svelte.enable-ts-plugin": true,
"eslint.validate": [

View file

@ -2,9 +2,15 @@
A soundboard with direct access to [myinstants](https://www.myinstants.com)' library.
## Recommended IDE Setup
## Installation
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode)
To use throughmic, you need to install [vbcable](https://vb-audio.com/Cable/), then download the latest version of throughmic in the releases tabs.
Once done you need to select in the app the vbcable device. You should pick `CABLE Input (VB-Audio Virtual Cable)` with audiooutput selected. If you can't find it, check that you installed correctly VBCable.
Then you need to go to the app you want to use this with, for example discord, and you need to change the micro to `CABLE Output (VB-Audio Virtual Cable)`.
Finally to be able to speak and use the soundboard you need to go to windows sound settings and enable "listen to this device" to send your mic's output to `CABLE Input (VB-Audio Virtual Cable)`.
You should now be able to play sounds.
You don't need to use the myinstants windows, it's for developpement purposes and will probably be removed later.
## Project Setup

View file

@ -1,186 +1,11 @@
<script lang="ts">
import { _Audio } from '$lib/audio'
import { Button } from '$lib/components/ui/button'
import { Input } from '$lib/components/ui/input/'
import * as Table from '$lib/components/ui/table/'
import * as Dialog from '$lib/components/ui/dialog/'
import { onMount } from 'svelte'
import * as Select from '$lib/components/ui/select'
import * as RadioGroup from '$lib/components/ui/radio-group/'
import { Label } from '$lib/components/ui/label/'
type Sound = {
name: string
// author: string
// favorited: number
// viewed: number
link: string
src: string
}
let sounds: Sound[] = $state([])
let searchValue = $state('')
function debounce(func, delay) {
let timeout
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
async function search(): Promise<void> {
sounds = await window.api.search(searchValue)
console.log(sounds)
}
const dSearch = debounce(search, 500)
let sound: _Audio
function play(src: string): void {
if (!localStorage.getItem('output_device_id')) {
return alert('Please select an audio device to ouput sounds.')
}
if (sound) {
sound.stop()
}
sound = new _Audio(src)
sound.setOuputDevice(localStorage.getItem('output_device_id'))
sound.play()
}
let showSelectDeviceDialog = $state(false)
let devices: MediaDeviceInfo[] = $state([])
let device = $state(localStorage.getItem('output_device_id'))
const triggerContent: string = $derived(
devices.find((d) => d.deviceId === device)?.label ?? 'Select a device'
)
let kinds = $state([])
let kind = $state('audiooutput')
async function updateDevices(): Promise<void> {
devices = await navigator.mediaDevices.enumerateDevices()
kinds = []
devices.forEach((d) => {
if (!kinds.includes(d.kind)) {
kinds.push(d.kind)
}
})
devices = devices.filter((d) => d.kind === kind)
}
onMount(async () => {
sounds = await window.api.search()
updateDevices()
console.log(devices)
if (!localStorage.getItem('output_device_id')) {
showSelectDeviceDialog = true
}
})
import Header from '$lib/components/Header.svelte'
import Footer from '$lib/components/Footer.svelte'
import Results from '$lib/components/Results.svelte'
</script>
<header class="absolute top-0 left-0 h-16 w-full">
<Input
placeholder="Search a sound"
class="absolute top-[50%] left-[50%] w-[calc(100%-64px)] translate-[-50%]"
bind:value={searchValue}
onkeyup={dSearch} />
</header>
<Header></Header>
<main
class="absolute top-16 left-[50%] h-[calc(100%-128px)] w-[calc(100%-64px)] translate-x-[-50%] overflow-y-scroll">
<Table.Root>
<!-- <Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Sound</Table.Head>
<Table.Head>Author</Table.Head>
<Table.Head>Favorited</Table.Head>
<Table.Head>Viewed</Table.Head>
<Table.Head class="text-right"></Table.Head>
<Table.Head class="text-right"></Table.Head>
</Table.Row>
</Table.Header> -->
<Table.Body>
{#each sounds as sound (sound)}
<Table.Row>
<Table.Cell class="font-medium"
><Button variant="link" href={sound.link} target="_blank">{sound.name}</Button
></Table.Cell>
<!-- <Table.Cell>{sound.author}</Table.Cell>
<Table.Cell>{sound.favorited}</Table.Cell>
<Table.Cell>{sound.viewed}</Table.Cell> -->
<Table.Cell class="text-right">
<Button variant="link">Add to favorites</Button></Table.Cell>
<Table.Cell class="text-right">
<Button
class="mr-4"
onclick={() => {
play(sound.src)
}}>Play</Button>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</main>
<Results></Results>
<footer class="absolute bottom-0 left-0 h-16 w-full">
<Dialog.Root open={showSelectDeviceDialog}>
<Dialog.Trigger
><Button
class="absolute top-[50%] right-8 w-fit translate-y-[-50%]"
onclick={() => {
showSelectDeviceDialog = true
}}>Select audio device</Button
></Dialog.Trigger>
<Dialog.Content class="max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Select audio device</Dialog.Title>
<Dialog.Description>Usually you should select "CABLE Input"</Dialog.Description>
</Dialog.Header>
<RadioGroup.Root bind:value={kind} onValueChange={updateDevices}>
{#each kinds as kind (kind)}
<div class="flex items-center space-x-2">
<RadioGroup.Item value={kind} />
<Label for="r1">{kind}</Label>
</div>
{/each}
</RadioGroup.Root>
<Select.Root type="single" name="device" bind:value={device}>
<Select.Trigger class="w-full">
{triggerContent}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Devices</Select.Label>
{#each devices as device (`${device.deviceId}`)}
<Select.Item value={device.deviceId} label={device.label}>
{device.label}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
<Dialog.Footer>
<Button
type="button"
class="w-full"
onclick={() => {
console.log('save')
if (device !== '') {
localStorage.setItem('output_device_id', device)
} else localStorage.removeItem('output_device_id')
showSelectDeviceDialog = false
}}>Save</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</footer>
<Footer></Footer>

View file

@ -1,21 +1,29 @@
export class _Audio {
src: string
audio: HTMLAudioElement
secondaryAudio: HTMLAudioElement
constructor(src: string) {
this.src = src
this.audio = new Audio(src)
this.secondaryAudio = new Audio(src)
}
play() {
this.audio.play()
this.secondaryAudio.play()
}
stop() {
this.audio.pause()
this.secondaryAudio.pause()
this.audio.remove()
this.secondaryAudio.remove()
}
setOuputDevice(id: string) {
setMainOuputDevice(id: string) {
this.audio.setSinkId(id)
}
setSecondaryOuputDevice(id: string) {
this.secondaryAudio.setSinkId(id)
}
}

View file

@ -0,0 +1,130 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/'
import { Button } from '$lib/components/ui/button'
import * as Select from '$lib/components/ui/select'
import * as RadioGroup from '$lib/components/ui/radio-group/'
import { Label } from '$lib/components/ui/label/'
import { onMount } from 'svelte'
let showSelectDeviceDialog = $state(false)
let devices: MediaDeviceInfo[] = $state([])
let mainDevice = $state(localStorage.getItem('output_main_device_id'))
let secondaryDevice = $state(localStorage.getItem('output_secondary_device_id'))
const mainTriggerContent: string = $derived(
devices.find((d) => d.deviceId === mainDevice)?.label ?? 'Select a device'
)
const secondaryTriggerContent: string = $derived(
devices.find((d) => d.deviceId === secondaryDevice)?.label ?? 'Select a device'
)
let kinds = $state([])
let mainKind = $state('audiooutput')
let secondaryKind = $state('audiooutput')
async function updateDevices(): Promise<void> {
devices = await navigator.mediaDevices.enumerateDevices()
kinds = []
devices.forEach((d) => {
if (!kinds.includes(d.kind)) {
kinds.push(d.kind)
}
})
devices = devices.filter((d) => d.kind === mainKind)
}
onMount(() => {
updateDevices()
console.log(devices)
if (!localStorage.getItem('output_main_device_id')) {
showSelectDeviceDialog = true
}
})
</script>
<Dialog.Root open={showSelectDeviceDialog}>
<Dialog.Trigger
><Button
class="absolute top-[50%] right-8 w-fit translate-y-[-50%]"
onclick={() => {
showSelectDeviceDialog = true
}}>Select audio devices</Button
></Dialog.Trigger>
<Dialog.Content class="max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Select main audio device</Dialog.Title>
<Dialog.Description>Usually you should select "CABLE Input"</Dialog.Description>
</Dialog.Header>
<RadioGroup.Root bind:value={mainKind} onValueChange={updateDevices}>
{#each kinds as kind (kind)}
<div class="flex items-center space-x-2">
<RadioGroup.Item value={kind} />
<Label for="r1">{kind}</Label>
</div>
{/each}
</RadioGroup.Root>
<Select.Root type="single" name="device" bind:value={mainDevice}>
<Select.Trigger class="w-full">
{mainTriggerContent}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Devices</Select.Label>
{#each devices as device (`${device.deviceId}`)}
<Select.Item value={device.deviceId} label={device.label}>
{device.label}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
<br />
<Dialog.Header>
<Dialog.Title>Select secondary audio device</Dialog.Title>
<Dialog.Description>Usually you should your headset</Dialog.Description>
</Dialog.Header>
<RadioGroup.Root bind:value={secondaryKind} onValueChange={updateDevices}>
{#each kinds as secondaryKind (secondaryKind)}
<div class="flex items-center space-x-2">
<RadioGroup.Item value={secondaryKind} />
<Label for="r1">{secondaryKind}</Label>
</div>
{/each}
</RadioGroup.Root>
<Select.Root type="single" name="device" bind:value={secondaryDevice}>
<Select.Trigger class="w-full">
{secondaryTriggerContent}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Devices</Select.Label>
{#each devices as device (`${device.deviceId}`)}
<Select.Item value={device.deviceId} label={device.label}>
{device.label}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
<Dialog.Footer>
<Button
type="button"
class="w-full"
onclick={() => {
console.log('save')
if (mainDevice !== '') {
localStorage.setItem('output_main_device_id', mainDevice)
} else localStorage.removeItem('output_main_device_id')
if (secondaryDevice !== '') {
localStorage.setItem('output_secondary_device_id', secondaryDevice)
} else localStorage.removeItem('output_secondary_device_id')
showSelectDeviceDialog = false
}}>Save</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View file

@ -0,0 +1,7 @@
<script>
import DevicesSelector from './DevicesSelector.svelte'
</script>
<footer class="absolute bottom-0 left-0 h-16 w-full">
<DevicesSelector></DevicesSelector>
</footer>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input/'
import { states } from '$lib/states.svelte'
let searchValue = $state('')
function debounce(func, delay) {
let timeout
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
async function search(): Promise<void> {
states.sounds = await window.api.search(searchValue)
}
const dSearch = debounce(search, 500)
</script>
<header class="absolute top-0 left-0 h-16 w-full">
<Input
placeholder="Search a sound"
class="absolute top-[50%] left-[50%] w-[calc(100%-64px)] translate-[-50%]"
bind:value={searchValue}
onkeyup={dSearch} />
</header>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button'
import * as Table from '$lib/components/ui/table/'
import { _Audio } from '$lib/audio'
import { onMount } from 'svelte'
import { states } from '$lib/states.svelte'
let sound: _Audio
function play(src: string): void {
if (!localStorage.getItem('output_main_device_id')) {
return alert('Please select an audio device to ouput sounds.')
}
if (sound) {
sound.stop()
}
sound = new _Audio(src)
sound.setMainOuputDevice(localStorage.getItem('output_main_device_id'))
if (localStorage.getItem('output_secondary_device_id')) {
sound.setSecondaryOuputDevice(localStorage.getItem('output_secondary_device_id'))
}
sound.play()
}
onMount(async () => {
states.sounds = await window.api.search()
})
</script>
<main
class="absolute top-16 left-[50%] h-[calc(100%-128px)] w-[calc(100%-64px)] translate-x-[-50%] overflow-y-scroll">
<Table.Root>
<!-- <Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Sound</Table.Head>
<Table.Head>Author</Table.Head>
<Table.Head>Favorited</Table.Head>
<Table.Head>Viewed</Table.Head>
<Table.Head class="text-right"></Table.Head>
<Table.Head class="text-right"></Table.Head>
</Table.Row>
</Table.Header> -->
<Table.Body>
{#each states.sounds as sound (sound)}
<Table.Row>
<Table.Cell class="font-medium"
><Button variant="link" href={sound.link} target="_blank">{sound.name}</Button
></Table.Cell>
<!-- <Table.Cell>{sound.author}</Table.Cell>
<Table.Cell>{sound.favorited}</Table.Cell>
<Table.Cell>{sound.viewed}</Table.Cell> -->
<Table.Cell class="text-right">
<Button variant="link">Add to favorites</Button></Table.Cell>
<Table.Cell class="text-right">
<Button
class="mr-4"
onclick={() => {
play(sound.src)
}}>Play</Button>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</main>

View file

@ -0,0 +1,14 @@
type Sound = {
name: string
// author: string
// favorited: number
// viewed: number
link: string
src: string
}
type States = { sounds: Sound[] }
export const states: States = $state({
sounds: []
})