Skip to content

Commit

Permalink
Merge pull request #6 from ctoth/synth
Browse files Browse the repository at this point in the history
Add synthesis
  • Loading branch information
ctoth authored Jun 21, 2024
2 parents 6d05482 + 5c565cf commit 32e8b65
Show file tree
Hide file tree
Showing 13 changed files with 277 additions and 74 deletions.
16 changes: 15 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,21 @@
const soundUrl = document.getElementById('soundUrl').value;
// Removed duplicate declaration of loopCheckbox
const loopCheckbox = document.getElementById('loopSound').checked;
sound = await cacophony.createSound(soundUrl, SoundType.Buffer, panType);
switch (soundUrl) {
case "sine":
case "sawtooth":
case "square":
case "triangle":
sound = await cacophony.createOscillator({
type: soundUrl,
panType: panType,
});

break;
default:
sound = await cacophony.createSound(soundUrl, SoundType.Buffer, panType);
break;
}
if (loopCheckbox) sound.loop('infinite');
sound.play();
window.sound = sound;
Expand Down
29 changes: 10 additions & 19 deletions src/cacophony.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AudioContext, AudioWorkletNode, IAudioListener, IMediaStreamAudioSourceNode, IPannerNode, IPannerOptions } from 'standardized-audio-context';
import { AudioContext, AudioWorkletNode, IAudioListener, IMediaStreamAudioSourceNode, IOscillatorOptions, IPannerNode, IPannerOptions } from 'standardized-audio-context';
import phaseVocoderProcessorWorkletUrl from './bundles/phase-vocoder-bundle.js?url';
import { AudioCache } from './cache';
import { BiquadFilterNode, GainNode, AudioBuffer } from './context';
Expand All @@ -7,12 +7,14 @@ import { Group } from './group';
import { Playback } from './playback';
import { Sound } from './sound';
import { createStream } from './stream';
import { Synth } from './synth';


export enum SoundType {
HTML = 'HTML',
Streaming = 'Streaming',
Buffer = 'Buffer'
Buffer = 'Buffer',
Oscillator = 'oscillator'
}


Expand Down Expand Up @@ -72,9 +74,8 @@ export interface BaseSound {
addFilter(filter: BiquadFilterNode): void;
removeFilter(filter: BiquadFilterNode): void;
volume: number;
playbackRate: number;
loop?(loopCount?: LoopCount): LoopCount;
duration: number;
position?: Position;
threeDOptions?: any;
}


Expand Down Expand Up @@ -133,27 +134,17 @@ export class Cacophony {
return new AudioWorkletNode!(this.context, name);
}
}

clearMemoryCache(): void {
AudioCache.clearMemoryCache();
}


createOscillator = ({ frequency, type, periodicWave }: OscillatorOptions) => {
if (frequency === undefined) {
frequency = 440;
}
const oscillator = this.context.createOscillator();
oscillator.type = type || 'sine';
if (periodicWave) {
oscillator.setPeriodicWave(periodicWave);
}
oscillator.frequency.setValueAtTime(frequency, this.context.currentTime);
oscillator.connect(this.globalGainNode);
return oscillator
createOscillator(options: OscillatorOptions) {
const synth = new Synth(this.context, this.globalGainNode, SoundType.Oscillator, 'HRTF', options);
return synth;
}


async createSound(buffer: AudioBuffer, soundType?: SoundType, panType?: PanType): Promise<Sound>

async createSound(url: string, soundType?: SoundType, panType?: PanType): Promise<Sound>
Expand Down
19 changes: 9 additions & 10 deletions src/container.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Position } from "./cacophony";
import { BiquadFilterNode, IPannerOptions } from "./context";
import { BiquadFilterNode } from "./context";
import { FilterManager } from "./filters";
import { Playback } from "./playback";
import { BasePlayback, Playback } from "./playback";

type Constructor<T = FilterManager> = abstract new (...args: any[]) => T;

export function PlaybackContainer<TBase extends Constructor>(Base: TBase) {
abstract class PlaybackContainer extends Base {
playbacks: Playback[] = [];
protected playbacks: BasePlayback[] = [];
protected _position: Position = [0, 0, 0];
protected _stereoPan: number = 0;
protected _threeDOptions: IPannerOptions = {
protected _threeDOptions: PannerOptions = {
coneInnerAngle: 360,
coneOuterAngle: 360,
coneOuterGain: 0,
Expand All @@ -31,7 +31,7 @@ export function PlaybackContainer<TBase extends Constructor>(Base: TBase) {
};
protected _volume: number = 1;

abstract preplay(): Playback[]
abstract preplay(): BasePlayback[]

/**
* Starts playback of the sound and returns a Playback instance representing this particular playback.
Expand All @@ -40,7 +40,7 @@ export function PlaybackContainer<TBase extends Constructor>(Base: TBase) {
* @returns {Playback[]} An array containing the Playback instances that have been started.
*/

play(): Playback[] {
play(): BasePlayback[] {
const playback = this.preplay();
playback.forEach(p => p.play());
return playback;
Expand Down Expand Up @@ -96,15 +96,14 @@ export function PlaybackContainer<TBase extends Constructor>(Base: TBase) {
return this.playbacks.some(p => p.isPlaying);
}


/**
* Retrieves the current 3D spatial position of the sound in the audio context.
* The position is returned as an array of three values[x, y, z].
* @returns { Position } The current position of the sound.
*/

get position(): Position {
return [this._threeDOptions.positionX, this._threeDOptions.positionY, this._threeDOptions.positionZ]
return [this._threeDOptions.positionX as number, this._threeDOptions.positionY as number, this._threeDOptions.positionZ as number]
}

/**
Expand All @@ -122,11 +121,11 @@ export function PlaybackContainer<TBase extends Constructor>(Base: TBase) {
}


get threeDOptions(): IPannerOptions {
get threeDOptions(): PannerOptions {
return this._threeDOptions;
}

set threeDOptions(options: Partial<IPannerOptions>) {
set threeDOptions(options: Partial<PannerOptions>) {
this._threeDOptions = { ...this._threeDOptions, ...options };
this.playbacks.forEach(p => p.threeDOptions = this._threeDOptions);
}
Expand Down
12 changes: 6 additions & 6 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { AudioContext, IAudioBuffer, IAudioBufferSourceNode, IBiquadFilterNode, IGainNode, IMediaElementAudioSourceNode, IPannerNode, IStereoPannerNode } from 'standardized-audio-context';
export {
AudioBuffer, AudioContext, IAudioBuffer, IPannerOptions,
} from 'standardized-audio-context';
import { AudioContext, IAudioBuffer, IAudioBufferSourceNode, IAudioNode, IBiquadFilterNode, IGainNode, IMediaElementAudioSourceNode, IOscillatorNode, IPannerNode, IStereoPannerNode } from 'standardized-audio-context';
export type AudioNode = IAudioNode<AudioContext>;
export type BiquadFilterNode = IBiquadFilterNode<AudioContext>;
export type MediaElementSourceNode = IMediaElementAudioSourceNode<AudioContext>;
export type AudioBuffer = IAudioBuffer;
export type AudioBufferSourceNode = IAudioBufferSourceNode<AudioContext>;
export type SourceNode = AudioBufferSourceNode | MediaElementSourceNode;

export type OscillatorNode = IOscillatorNode<AudioContext>;
export type SourceNode = AudioBufferSourceNode | MediaElementSourceNode | OscillatorNode;

export type GainNode = IGainNode<AudioContext>;
export type PannerNode = IPannerNode<AudioContext>;
export type StereoPannerNode = IStereoPannerNode<AudioContext>;
export { AudioContext };
4 changes: 4 additions & 0 deletions src/filters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { BiquadFilterNode } from './context'

export type FilterCloneOverrides = {
filters?: BiquadFilterNode[];
};

export abstract class FilterManager {
protected _filters: BiquadFilterNode[] = [];

Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './cacophony';
export * from './group';
export * from './playback';
export * from './sound';
export * from './sound';
export * from './synth';
49 changes: 49 additions & 0 deletions src/oscillatorMixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { OscillatorNode } from "./context";
import { BasePlayback } from "./playback";

export type OscillatorCloneOverrides = {
oscillatorOptions?: Partial<OscillatorOptions>;
};

type Constructor<T = {}> = abstract new (...args: any[]) => T;

export function OscillatorMixin<TBase extends Constructor>(Base: TBase) {
abstract class OscillatorMixin extends BasePlayback {

private _oscillatorOptions: Partial<OscillatorOptions> = {};
declare public source?: OscillatorNode;

get oscillatorOptions(): Partial<OscillatorOptions> {
return this._oscillatorOptions;
}

set oscillatorOptions(options: Partial<OscillatorOptions>) {
this._oscillatorOptions = options;
if (this.source && this.source instanceof OscillatorNode) {
Object.assign(this.source, options);
}
}

play(): [this] {
if (!this.source) {
throw new Error('No source node found');
}
this.source.start();
this._playing = true;
return [this];
}

stop() {
if (this.source && this.source.stop) {
this.source.stop();
this._playing = false;
}
}

pause(): void {
this.stop();
}

};
return OscillatorMixin;
}
15 changes: 9 additions & 6 deletions src/pannerMixin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { AudioContext, IPannerOptions, PannerNode, StereoPannerNode } from "./context";

import type { AudioContext, PannerNode, StereoPannerNode } from "./context";
import { PanType, Position } from "./cacophony";
import { FilterManager } from "./filters";

export type PanCloneOverrides = {
panType?: PanType;
stereoPan?: number; // -1 (left) to 1 (right)
threeDOptions?: Partial<PannerOptions>; // HRTF panning only
position?: Position; // HRTF panning only, [x, y, z]
};

type Constructor<T = FilterManager> = abstract new (...args: any[]) => T;

Expand All @@ -15,7 +20,6 @@ export function PannerMixin<TBase extends Constructor>(Base: TBase) {
return this._panType;
}


setPanType(panType: PanType, audioContext: AudioContext) {
this._panType = panType;
if (panType === 'stereo') {
Expand All @@ -25,7 +29,6 @@ export function PannerMixin<TBase extends Constructor>(Base: TBase) {
}
}


setPannerNode(pannerNode: PannerNode) {
this.panner = pannerNode;
}
Expand Down Expand Up @@ -67,7 +70,7 @@ export function PannerMixin<TBase extends Constructor>(Base: TBase) {
* @throws {Error} Throws an error if the sound has been cleaned up or if HRTF panning is not used.
*/

get threeDOptions(): IPannerOptions {
get threeDOptions(): PannerOptions {
if (!this.panner) {
throw new Error('Cannot get 3D options of a sound that has been cleaned up');
}
Expand Down Expand Up @@ -101,7 +104,7 @@ export function PannerMixin<TBase extends Constructor>(Base: TBase) {
* @param {Partial<IPannerOptions>} options - The 3D audio options to set.
* @throws {Error} Throws an error if the sound has been cleaned up or if HRTF panning is not used.
*/
set threeDOptions(options: Partial<IPannerOptions>) {
set threeDOptions(options: Partial<PannerOptions>) {
if (!this.panner) {
throw new Error('Cannot set 3D options of a sound that has been cleaned up');
}
Expand Down
43 changes: 26 additions & 17 deletions src/playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,38 @@
*/


import type { BaseSound, FadeType, LoopCount, PanType, Position } from "./cacophony";
import type { AudioBuffer, AudioBufferSourceNode, AudioContext, BiquadFilterNode, GainNode, IPannerOptions, PannerNode, SourceNode, StereoPannerNode } from "./context";
import type { BaseSound, LoopCount, PanType } from "./cacophony";
import type { AudioBuffer, AudioBufferSourceNode, AudioContext, AudioNode, BiquadFilterNode, GainNode, SourceNode } from "./context";
import { FilterManager } from "./filters";
import { PannerMixin } from "./pannerMixin";
import { VolumeMixin } from "./volumeMixin";

export abstract class BasePlayback extends PannerMixin(VolumeMixin(FilterManager)) {
public source?: AudioNode
protected _playing: boolean = false;

export class Playback extends PannerMixin(VolumeMixin(FilterManager)) implements BaseSound {
private _playing: boolean = false;
abstract play(): [this];
abstract pause(): void;
abstract stop(): void;

/**
* Checks if the audio is currently playing.
* @returns {boolean} True if the audio is playing, false otherwise.
*/

get isPlaying(): boolean {
if (!this.source) {
return false;
}
return this._playing;
}


}

export class Playback extends BasePlayback implements BaseSound {
private context: AudioContext;
private source?: SourceNode;
public source?: SourceNode;
loopCount: LoopCount = 0;
currentLoop: number = 0;
private buffer?: AudioBuffer;
Expand Down Expand Up @@ -242,18 +263,6 @@ export class Playback extends PannerMixin(VolumeMixin(FilterManager)) implements
}
}

/**
* Checks if the audio is currently playing.
* @returns {boolean} True if the audio is playing, false otherwise.
*/

get isPlaying(): boolean {
if (!this.source) {
return false;
}
return this._playing;
}

/**
* Cleans up resources used by the Playback instance.
* This method should be called when the audio is no longer needed to free up resources.
Expand Down
Loading

0 comments on commit 32e8b65

Please sign in to comment.