/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / sources.ts
143 lines · 4.8 KBCodeBlameHistory
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 1/** Synthesized test sounds, and helpers for clips loaded from files or the microphone. */
2import { mulberry32 } from './phasemap'
3import type { Source } from './types'
5export const MAX_SECONDS = 3
7const raisedCosineFades = (x: Float32Array, nFade: number): void => {
8 const n = x.length
9 const m = Math.min(nFade, Math.floor(n / 2))
10 for (let i = 0; i < m; i++) {
11 const e = 0.5 - 0.5 * Math.cos((Math.PI * i) / m)
12 x[i] *= e
13 x[n - 1 - i] *= e
14 }
17/** Raised-cosine fades at both ends, in place. */
18export function applyFades(x: Float32Array, fs: number, fadeSec: number): Float32Array {
19 raisedCosineFades(x, Math.round(fadeSec * fs))
20 return x
23const add = (x: Float32Array, at: number, seg: Float32Array): void => {
24 for (let i = 0; i < seg.length && at + i < x.length; i++) if (at + i >= 0) x[at + i] += seg[i]
27function clickTrain(fs: number): Float32Array {
28 const x = new Float32Array(Math.round(1.1 * fs))
29 const w = Math.max(3, Math.round(0.0001 * fs)) // 0.1 ms pulse
30 const pulse = new Float32Array(w)
31 for (let i = 0; i < w; i++) pulse[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * (i + 1)) / (w + 1))
32 for (const t of [0.1, 0.35, 0.6, 0.85]) add(x, Math.round(t * fs), pulse)
33 return x
36function noiseBursts(fs: number): Float32Array {
37 const x = new Float32Array(Math.round(1.2 * fs))
38 const rnd = mulberry32(7)
39 const len = Math.round(0.06 * fs)
40 for (const t of [0.1, 0.5, 0.9]) {
41 const seg = new Float32Array(len)
42 for (let i = 0; i < len; i++) seg[i] = rnd() * 2 - 1
43 raisedCosineFades(seg, Math.round(0.002 * fs))
44 add(x, Math.round(t * fs), seg)
45 }
46 return x
49function pluck(fs: number, f0: number, seconds: number, seed: number): Float32Array {
50 // Karplus-Strong: a noise burst circulating through a delay line with a two-point average
51 const n = Math.round(seconds * fs)
52 const period = Math.max(2, Math.round(fs / f0))
53 const y = new Float32Array(n)
54 const rnd = mulberry32(seed)
55 for (let i = 0; i < period; i++) y[i] = rnd() * 2 - 1
56 for (let i = period; i < n; i++) y[i] = 0.996 * 0.5 * (y[i - period] + y[i - period - 1 < 0 ? 0 : i - period - 1])
57 raisedCosineFades(y, Math.round(0.002 * fs))
58 return y
61function plucks(fs: number): Float32Array {
62 const x = new Float32Array(Math.round(1.6 * fs))
63 add(x, Math.round(0.05 * fs), pluck(fs, 220, 0.9, 11))
64 add(x, Math.round(0.8 * fs), pluck(fs, 330, 0.8, 12))
65 return x
68function harmonicTone(fs: number, f0: number, seconds: number, nHarm: number, amp: number): Float32Array {
69 const n = Math.round(seconds * fs)
70 const y = new Float32Array(n)
71 for (let i = 0; i < n; i++) {
72 let s = 0
73 for (let k = 1; k <= nHarm; k++) s += Math.sin((2 * Math.PI * k * f0 * i) / fs) / k
74 y[i] = amp * s
75 }
76 raisedCosineFades(y, Math.round(0.01 * fs))
77 return y
80function tone(fs: number): Float32Array {
81 const x = new Float32Array(Math.round(1.0 * fs))
82 add(x, 0, harmonicTone(fs, 220, 1.0, 8, 0.5))
83 return x
86function clickThenTone(fs: number): Float32Array {
87 // the test signal used in the design document
88 const x = new Float32Array(Math.round(0.8 * fs))
89 const w = Math.max(3, Math.round(0.0002 * fs))
90 const pulse = new Float32Array(w)
91 for (let i = 0; i < w; i++) pulse[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * (i + 1)) / (w + 1))
92 add(x, Math.round(0.15 * fs), pulse)
93 add(x, Math.round(0.35 * fs), harmonicTone(fs, 220, 0.35, 8, 0.25))
94 return x
97export type Preset = { name: string; make: (fs: number) => Float32Array }
99export const presets: Preset[] = [
100 { name: 'Click train', make: clickTrain },
101 { name: 'Noise bursts', make: noiseBursts },
102 { name: 'Plucked string', make: plucks },
103 { name: 'Harmonic tone', make: tone },
104 { name: 'Click then tone', make: clickThenTone },
107export function makePreset(p: Preset, fs: number): Source {
108 return { name: p.name, fs, samples: p.make(fs) }
111/** Average the channels of a decoded buffer into one. */
112export function toMono(buffer: AudioBuffer): Float32Array {
113 const n = buffer.length
114 const out = new Float32Array(n)
115 const nc = buffer.numberOfChannels
116 for (let c = 0; c < nc; c++) {
117 const d = buffer.getChannelData(c)
118 for (let i = 0; i < n; i++) out[i] += d[i] / nc
119 }
120 return out
123/** A clip of at most MAX_SECONDS starting at startSec, with short fades. */
124export function excerpt(samples: Float32Array, fs: number, startSec: number): Float32Array {
125 const i0 = Math.max(0, Math.min(samples.length, Math.round(startSec * fs)))
126 const i1 = Math.min(samples.length, i0 + Math.round(MAX_SECONDS * fs))
127 return applyFades(samples.slice(i0, i1), fs, 0.005)
130export const rms = (x: Float32Array): number => {
131 let s = 0
132 for (let i = 0; i < x.length; i++) s += x[i] * x[i]
133 return Math.sqrt(s / Math.max(1, x.length))
136export const peak = (x: Float32Array): number => {
137 let m = 0
138 for (let i = 0; i < x.length; i++) {
139 const a = Math.abs(x[i])
140 if (a > m) m = a
141 }
142 return m
↑↓ move↵openescclose