samer@34: function ad=sonify(S,D,varargin) samer@34: % sonify - Sonify sequences of durations and pitches samer@34: % samer@34: % sonify :: samer@34: % ([[N]] | seq(real)) ~'sequence of pitches in semitones', samer@34: % (natural | seq(real)) ~'sequence of durations', samer@34: % options { samer@34: % env :: ([[N]]->[[N]]) ~'envelope shaping function', samer@34: % transpose :: real /0 ~'add this to all pitches first'; samer@34: % bpm :: nonneg /240 ~'number of time units per minute'; samer@34: % samer@34: % gen :: { 'sine' ~'sine wave', samer@34: % 'square' ~'frequency-quantised square wave', samer@34: % 'blsquare' ~'bandlimited square wave', samer@34: % 'blsquare2' ~'alternative method', samer@34: % 'shepard' ~'Shepard tones' samer@34: % } / 'sine' ~'determines waveform generated'; samer@34: % betap :: [[2]] /[2,8] ~'Beta pdf parameters for Shepard tones' samer@34: % samer@34: % fs :: real /11025 ~'intended sampling rate'; samer@34: % buffer :: noneg /0.125 ~'buffer size in seconds, [] means one per note' samer@34: % } samer@34: % -> seq([[1,T]]). samer@34: samer@37: opts=options( ... samer@34: 'bpm',240,'transpose',0, ... samer@34: 'gen','sine','betap',[2,8], ... samer@34: 'fs',11025, 'buffer',0.125, ... samer@34: 'env',[],'bl_cutoff',[], ... samer@34: varargin{:}); samer@34: samer@34: env=opts.env; samer@34: fm=440/opts.fs; samer@34: dm=60*opts.fs/opts.bpm; samer@34: tr=opts.transpose; samer@34: num2hz=@(n)nan2x(0,fm*2.^((n+tr)/12)); samer@34: samer@34: if isscalar(D), D=dm*D; samer@36: else D=map(@(d)dm*d,dd(D)); % D=dm*D with optimisation samer@34: end samer@36: C=map(@(n)(n+tr)/12,dd(S)); % chroma values samer@36: F=map(num2hz,dd(S)); % convert to normalised frequencies samer@34: samer@34: % cutoff for band-limited waveform generation samer@34: if isempty(opts.bl_cutoff) samer@34: cutoff=0.21+0.2*repeat(normalise01(cumsum(sample(stable(0,1.4,0),[1,1024]),2),2)); samer@34: else samer@34: cutoff=opts.bl_cutoff; samer@34: end samer@34: samer@34: switch opts.gen samer@34: case 'square' samer@34: % square wave with period quantized to sample period samer@34: Y=blockdata(square,D,0,repeat(0.5),F); samer@34: case 'blsquare' samer@34: % cumsum within each buffer: doesn't drift but makes samer@34: % audible clicks between buffers. samer@36: Y=map(@(t)0.5*cumsum(t),blockdata(bpblit,D,0,cutoff,F)); samer@34: case 'blsquare2' samer@34: % this version does cumsum across the sequence to remove samer@34: % discontinuities but requires high-pass filter to stop drift. samer@34: [fb,fa]=butter(2,0.001,'high'); samer@34: Y=filter(0.5*fb,fa,cumsum(blockdata(bpblit,D,0,cutoff,F),2)); samer@34: case 'shepard' samer@34: Y=0.04*shepardtone(D,C,opts.betap); samer@34: case 'sine2' samer@34: Y=blockdata(sine,D,0,F)./(F/0.01); samer@34: otherwise samer@34: Y=blockdata(sine,D,0,F); samer@34: end samer@34: samer@34: % if supplied apply envelope to shape each note samer@34: if nargin>2 && ~isempty(env), samer@36: ad=map(env,Y); samer@34: else samer@34: ad=Y; samer@34: end samer@34: samer@34: % rebuffer to constant size samer@34: if ~isempty(opts.buffer) samer@36: ad=window(ad,ceil(opts.buffer*opts.fs)); samer@34: end samer@34: