Skip to content

Commit

Permalink
♻️ [Changes] 2024 12 07 (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
beefchimi authored Dec 12, 2024
1 parent dd04449 commit 54b5434
Show file tree
Hide file tree
Showing 8 changed files with 53 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/rich-bags-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"earwurm": patch
---

Earwurm state is now set to the `context.state` upon initialization.
10 changes: 5 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ manager.resume();
// suspend and resume the `AudioContext` behind the scenes.
manager.suspend();

// Stop and destory all sounds within each `Stack`.
// Stop and destroy all sounds within each `Stack`.
// Each `Stack` will remain available in the library
// to continue interacting with.
manager.stop();
Expand Down Expand Up @@ -139,7 +139,7 @@ manager.activeEvents;
// Event called whenever the `mute` property changes.
(event: 'mute', listener: (muted: boolean) => void)

// Event called whenever an error is occured on the `AudioContext`.
// Event called whenever an error occurs on the `AudioContext`.
// This could be a result of: failed to resume, failed to close.
(event: 'error', listener: (messages: CombinedErrorMessage) => void)
```
Expand All @@ -150,7 +150,7 @@ There are no static members or relevant tokens exposed at the `manager` level.

## Stack API

Capabilities of an indivudual `Earwurm > Stack`.
Capabilities of an individual `Earwurm > Stack`.

**Initialization:**

Expand Down Expand Up @@ -179,7 +179,7 @@ soundStack.has('SoundId');
// to `created`. Accepts an optional `id`. Otherwise, `id` is auto
// assigned by combining the `id` passed to `new Stack()` with
// the total number of sounds created during this stack’s life.
// Will return a `Promise` which resolves to the`Sound` instance.
// Will return a `Promise` which resolves to the `Sound` instance.
// You can either chain with `.then()`, or `await` and call
// `.play()` once the `Promise` has resolved.
soundStack.prepare(optionalId);
Expand Down Expand Up @@ -265,7 +265,7 @@ soundStack.activeEvents;
// Event called whenever the `mute` property changes.
(event: 'mute', listener: (muted: boolean) => void)

// Event called whenever an error is occured on the `Stack`.
// Event called whenever an error occurs on the `Stack`.
// This could be a result of: failed to load the `path`.
(event: 'error', listener: ({id, message}: StackError) => void)
```
Expand Down
3 changes: 1 addition & 2 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ For a list of all the available commands, please see the root `package.json`. Ov
- All `localhost` addresses for running apps will be printed to your terminal when you run `start`.
- `lint` checks the codebase for `eslint` errors, but does not fix them.
- `lint:fix` automatically fixes any `eslint` errors that can be programatically resolved.
- `format` originally checked the codebase for `prettier` errors, but that is now handled by `eslint` and is performed via the `lint` tasks.
- `type-check` checks the codebase for any TypeScript errors, but does not fix them.
- `test` fires up the `vitest` server and runs all `*.test.ts` files.
- `clean` will delete any `dist` and `coverage` folders.
Expand Down Expand Up @@ -86,7 +85,7 @@ If you want to preview the production build of the `website`, you can run the fo

```sh
pnpm build
pnpm --filter website preview
pnpm preview
```

The `website` should now be available on a locally reachable address, allowing you to test on a `mobile` device.
Expand Down
21 changes: 19 additions & 2 deletions docs/conversion.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Converting audio

This documentation will NOT describe how to record audio and export it from any editing software.
> This documentation will NOT describe how to record audio and export it from any editing software. For that, you may want to read: <https://medium.com/shopify-ux/improving-ui-with-web-audio-368bf674ccf7>
Quite simply, if you have existing audio files that are not already in `webm` format, you can convert them using a command-line tool called [`ffmpeg`](https://ffmpeg.org/). You can learn some audio-specific commands from the [`ffmpeg` documentation](https://ffmpeg.org/ffmpeg.html#Audio-Options).
If you have existing audio files that are not already in `webm` format, you can convert them using a command-line tool called [`ffmpeg`](https://ffmpeg.org/). If you are on a Mac and using Homebrew, `ffmpeg` is [very easy to install](https://formulae.brew.sh/formula/ffmpeg) as a command-line tool.

You can learn some audio-specific commands from the [`ffmpeg` documentation](https://ffmpeg.org/ffmpeg.html#Audio-Options).

Using `ffmpeg`, you can select an audio file as an “input source”, pass some options that tell `ffmpeg` how you want to transform the audio, and point to an “output source” to save the converted asset.

Expand Down Expand Up @@ -30,3 +32,18 @@ ffmpeg -i input-file.wav -dash 1 -map_metadata -1 -acodec libopus -ar 48000 -ac
- `-ab 96k`: Set the bitrate. `96k` for 2-channel `stereo` sound is probably good, but `128k` might be preferrable.
- `-f webm`: Specifiy the output format.
- `{output-file.ext}`: End with the file name _(including extension)_ you wish to save.

## Tricks

`ffmpeg` may not always be able to convert your source assets. When this happens, it is usually because of some file format conflict.

Sometimes it helps to use another tool to convert that source asset. If you are on a Mac, you can usually `right-click` an audio file and select `Encode Selected Audio Files`. From there, just choose the highest quality output format. More than likely, this will be `Apple Lossless`.

As long as that conversion works, you should then be able to point `ffmpeg` at that newly encoded file and proceed with the `.webm` conversion.

## Resources

Below are a few places you can go to get royalty-free audio assets:

- <https://www.epidemicsound.com/>
- <https://sonniss.com/>
19 changes: 12 additions & 7 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ The quickest way to describe the “structure” of an `Earwurm` is:
- The `Manager` contains a library of “stacks”.
- A `Stack` contains a queue of “sounds”.
- A `Sound` is created and queued within the `Stack`.
- The queue builds up upon `stack.prepare()`.
- The queue decreases upon `sound.stop()` _(or `ended` event)_.
- The queue increases when calling: `stack.prepare()`.
- The queue decreases when calling: `sound.stop()` _(or `ended` event)_.
- The queue has a maximum size.
- A `Sound` is automatically destroyed as new sounds are added that would exceed that maximum size.
- As sounds begin playing, their output travels through a “chain of gain nodes”, the final “destination” being your device’s speakers.
Expand Down Expand Up @@ -74,16 +74,21 @@ A rough pseudo-code visualization:

```tsx
const soundStack = manager.get('MySound');
const sound = await soundStack?.prepare();

// We then make 3 consecutive calls to `prepare`
// on the same `Stack`, produces 3 instances of that `Sound`:
const sound1 = await soundStack?.prepare();
const sound2 = await soundStack?.prepare();
const sound3 = await soundStack?.prepare();

// We can imagine some internal code that looks like:
const latestId = totalSoundsCreated + 1;
const updatedStack = [...stack.queue, new Sound(latestId)];

// We then make 3 consecutive calls to `play` on the same `Sound`:
sound?.play();
sound?.play();
sound?.play();
// We then make 3 consecutive calls to `play` each `Sound`:
sound1.play();
sound2.play();
sound3.play();

// If we were to inspect that `Entry` at this exact moment,
// it might look something like this:
Expand Down
15 changes: 8 additions & 7 deletions pkg/earwurm/src/Earwurm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ import type {
} from './types';

export class Earwurm extends EmittenCommon<ManagerEventMap> {
private _vol = 1;
private _mute = false;
private _trans = false;
private _playing = false;
private _keys: StackId[] = [];
private _state: ManagerState = 'suspended';

readonly #context = new AudioContext();
readonly #gainNode = this.#context.createGain();
readonly #request: ManagerConfig['request'];

#library: Stack[] = [];

private _vol = 1;
private _mute = false;
private _trans = false;
private _playing = false;
private _keys: StackId[] = [];

// If the `AudioContext` is `running` upon initialization,
// then it should be safe to mark audio as “unlocked”.
private _state: ManagerState = this.#context.state || 'suspended';
private _unlocked = this.#context.state === 'running';

constructor(config?: ManagerConfig) {
Expand Down Expand Up @@ -140,6 +140,7 @@ export class Earwurm extends EmittenCommon<ManagerEventMap> {
const existingStack = this.get(id);
const identicalStack = existingStack?.path === path;

// Only skip updating the `Stack` if both `id + path` are the same.
if (identicalStack) return collection;

newKeys.push(id);
Expand Down
2 changes: 1 addition & 1 deletion pkg/earwurm/src/Sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export class Sound extends EmittenCommon<SoundEventMap> {
// was "stopped" or came to a natural "end".
// We should also include the `iterations` value here so
// that we can distinguish from a `loop > stop` having
// played to completion at least one. Lastly, we might
// played to completion at least once. Lastly, we might
// also need to indicate if this was a "scratch buffer".
this.emit('ended', {
id: this.id,
Expand Down
4 changes: 2 additions & 2 deletions pkg/earwurm/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import dts from 'vite-plugin-dts';

import pkg from './package.json';

// TODO: We may need a unique `vitest.config.ts` file for this
// package... but simply adding that file does not appear to work.
// TODO: We may need a unique `vitest.config.ts` file.
// https://vitest.dev/guide/#workspaces-support
export default defineConfig({
build: {
lib: {
Expand Down

0 comments on commit 54b5434

Please sign in to comment.