import React , { useEffect , useRef } from "react" ; const random = ( min , max ) : number => { return Math . random ( ) * ( max - min ) + min ; } ; export const getAudioBuffer = async ( audioContext , url ) : Promise < AudioBuffer > => { const response = await fetch ( url ) ; const audioData = await response . arrayBuffer ( ) ; const buffer = await audioContext . decodeAudioData ( audioData ) ; return buffer ; } ; export const createGain = ( audioContext , velocity = 0.001 ) : GainNode => { const gain = audioContext . createGain ( ) ; gain . gain . value = velocity ; return gain ; } ; export const createPan = ( audioContext ) : StereoPannerNode => { const panner = audioContext . createStereoPanner ( ) ; return panner ; } ; interface Grain { isPlaying : ( ) => boolean ; play : ( props : GranulaProps ) => void ; } interface ADSR { attack : number ; sustain : number ; release : number ; } export const createGrain = ( audioContext , output ) : Grain => { const state = { busy : false , } ; const gain = audioContext . createGain ( ) ; const panner = audioContext . createStereoPanner ( ) ; panner . connect ( gain ) ; gain . connect ( output ) ; const play = ( props ) => { state . busy = true ; const source = props . audioContext . createBufferSource ( ) ; source . buffer = props . buffer ; source . connect ( panner ) ; panner . pan . setValueAtTime ( random ( - props . controls . pan , props . controls . pan ) , props . audioContext . currentTime ) ; source . playbackRate . value = multiplyTranspose ( props . controls . transpose ) ; const position = getPosition ( props . buffer . duration , props . controls . position , props . controls . spread ) ; const now = props . audioContext . currentTime ; const { attack , sustain , release } = props . controls . adsr ; const attackDuration = attack / 1000 ; const sustainDuration = sustain / 1000 ; const releaseDuration = release / 1000 ; const duration = attackDuration + sustainDuration + releaseDuration ; const durationMs = attack + sustain + release ; const attackAt = now ; const sustainAt = attackAt + attackDuration ; const releaseAt = sustainAt + sustainDuration ; const endAt = releaseAt + releaseDuration ; source . start ( attackAt , position , duration ) ; source . stop ( endAt ) ; gain . gain . setValueAtTime ( 0 , attackAt ) ; gain . gain . exponentialRampToValueAtTime ( 1 , sustainAt ) ; gain . gain . setValueAtTime ( 1 , releaseAt ) ; gain . gain . exponentialRampToValueAtTime ( 0.00001 , endAt ) ; setTimeout ( ( ) => { source . disconnect ( ) ; state . busy = false ; } , durationMs ) ; } ; return { isPlaying : ( ) => state . busy , play , } ; } ; const getPosition = ( duration , position , spread ) : number => { const spreadOffset = Math . random ( ) * spread - spread / 2 ; const adjustedPos = position * duration ; return Math . max ( 0 , adjustedPos + spreadOffset ) ; } ; const multiplyTranspose = ( value ) : number => Math . pow ( 2 , value / 12 ) ; export interface GranulaProps { audioContext : AudioContext ; buffer : AudioBuffer ; output : AudioNode ; controls : { transpose : number ; adsr : ADSR ; density : number ; spread : number ; position : number ; pan : number ; playbackRate : number ; reverb : number ; gain : number ; running : boolean ; } ; } interface Granula { start : ( ) => void ; stop : ( ) => void ; } const createGranula = ( props ) : Granula => { const { audioContext , buffer , controls , output } = props ; const state = { interval : undefined , grains : [ ] , } ; const master = createGain ( audioContext ) ; master . gain . value = Math . max ( 0 , controls . gain / 4 ) ; master . connect ( output ) ; const start = ( ) => { if ( state . interval ) { return ; } state . interval = setInterval ( ( ) => tick ( ) , 1000 / controls . density ) ; } ; const stop = ( ) => { if ( state . interval ) { clearInterval ( state . interval ) ; } state . interval = undefined ; } ; const tick = ( ) => { const grain = state . grains . find ( ( candidate ) => ! candidate . isPlaying ( ) ) ; try { if ( grain ) { grain . play ( props ) ; return ; } const newGrain = createGrain ( audioContext , master ) ; newGrain . play ( props ) ; state . grains = [ ... state . grains , newGrain ] . slice ( 0 , 100 ) ; console . log ( state . grains ) ; } catch ( error ) { console . error ( error ) ; stop ( ) ; } } ; return { start , stop , } ; } ; export const Granular = ( props ) => { const granula = useRef < Granula > ( ) ; useEffect ( ( ) => { granula . current = createGranula ( props ) ; granula . current . start ( ) ; return ( ) => { if ( granula . current ) { granula . current . stop ( ) ; } } ; } , [ props ] ) ; return null ; } ; export interface Reverb { input : AudioNode ; output : AudioNode ; setWet : ( wet : number ) => void ; } export const createReverb = async ( audioContext ) : Promise < Reverb > => { const convolver = audioContext . createConvolver ( ) ; const response = await fetch ( "https://oskareriksson.se/tuna/impulses/impulse_rev.wav" ) ; const buffer = await response . arrayBuffer ( ) ; convolver . buffer = await audioContext . decodeAudioData ( buffer ) ; const master = audioContext . createGain ( ) ; const input = audioContext . createGain ( ) ; const dry = audioContext . createGain ( ) ; const wet = audioContext . createGain ( ) ; input . connect ( dry ) ; input . connect ( wet ) ; dry . connect ( master ) ; wet . connect ( convolver ) ; convolver . connect ( master ) ; return { input : input , output : master , setWet : ( wetGain ) => { wet . gain . value = wetGain ; dry . gain . value = 1 - wetGain ; } , } ; } ;