samer@34
|
1 function ad=sonify(S,D,varargin)
|
samer@34
|
2 % sonify - Sonify sequences of durations and pitches
|
samer@34
|
3 %
|
samer@34
|
4 % sonify ::
|
samer@34
|
5 % ([[N]] | seq(real)) ~'sequence of pitches in semitones',
|
samer@34
|
6 % (natural | seq(real)) ~'sequence of durations',
|
samer@34
|
7 % options {
|
samer@34
|
8 % env :: ([[N]]->[[N]]) ~'envelope shaping function',
|
samer@34
|
9 % transpose :: real /0 ~'add this to all pitches first';
|
samer@34
|
10 % bpm :: nonneg /240 ~'number of time units per minute';
|
samer@34
|
11 %
|
samer@34
|
12 % gen :: { 'sine' ~'sine wave',
|
samer@34
|
13 % 'square' ~'frequency-quantised square wave',
|
samer@34
|
14 % 'blsquare' ~'bandlimited square wave',
|
samer@34
|
15 % 'blsquare2' ~'alternative method',
|
samer@34
|
16 % 'shepard' ~'Shepard tones'
|
samer@34
|
17 % } / 'sine' ~'determines waveform generated';
|
samer@34
|
18 % betap :: [[2]] /[2,8] ~'Beta pdf parameters for Shepard tones'
|
samer@34
|
19 %
|
samer@34
|
20 % fs :: real /11025 ~'intended sampling rate';
|
samer@34
|
21 % buffer :: noneg /0.125 ~'buffer size in seconds, [] means one per note'
|
samer@34
|
22 % }
|
samer@34
|
23 % -> seq([[1,T]]).
|
samer@34
|
24
|
samer@37
|
25 opts=options( ...
|
samer@34
|
26 'bpm',240,'transpose',0, ...
|
samer@34
|
27 'gen','sine','betap',[2,8], ...
|
samer@34
|
28 'fs',11025, 'buffer',0.125, ...
|
samer@34
|
29 'env',[],'bl_cutoff',[], ...
|
samer@34
|
30 varargin{:});
|
samer@34
|
31
|
samer@34
|
32 env=opts.env;
|
samer@34
|
33 fm=440/opts.fs;
|
samer@34
|
34 dm=60*opts.fs/opts.bpm;
|
samer@34
|
35 tr=opts.transpose;
|
samer@34
|
36 num2hz=@(n)nan2x(0,fm*2.^((n+tr)/12));
|
samer@34
|
37
|
samer@34
|
38 if isscalar(D), D=dm*D;
|
samer@36
|
39 else D=map(@(d)dm*d,dd(D)); % D=dm*D with optimisation
|
samer@34
|
40 end
|
samer@36
|
41 C=map(@(n)(n+tr)/12,dd(S)); % chroma values
|
samer@36
|
42 F=map(num2hz,dd(S)); % convert to normalised frequencies
|
samer@34
|
43
|
samer@34
|
44 % cutoff for band-limited waveform generation
|
samer@34
|
45 if isempty(opts.bl_cutoff)
|
samer@34
|
46 cutoff=0.21+0.2*repeat(normalise01(cumsum(sample(stable(0,1.4,0),[1,1024]),2),2));
|
samer@34
|
47 else
|
samer@34
|
48 cutoff=opts.bl_cutoff;
|
samer@34
|
49 end
|
samer@34
|
50
|
samer@34
|
51 switch opts.gen
|
samer@34
|
52 case 'square'
|
samer@34
|
53 % square wave with period quantized to sample period
|
samer@34
|
54 Y=blockdata(square,D,0,repeat(0.5),F);
|
samer@34
|
55 case 'blsquare'
|
samer@34
|
56 % cumsum within each buffer: doesn't drift but makes
|
samer@34
|
57 % audible clicks between buffers.
|
samer@36
|
58 Y=map(@(t)0.5*cumsum(t),blockdata(bpblit,D,0,cutoff,F));
|
samer@34
|
59 case 'blsquare2'
|
samer@34
|
60 % this version does cumsum across the sequence to remove
|
samer@34
|
61 % discontinuities but requires high-pass filter to stop drift.
|
samer@34
|
62 [fb,fa]=butter(2,0.001,'high');
|
samer@34
|
63 Y=filter(0.5*fb,fa,cumsum(blockdata(bpblit,D,0,cutoff,F),2));
|
samer@34
|
64 case 'shepard'
|
samer@34
|
65 Y=0.04*shepardtone(D,C,opts.betap);
|
samer@34
|
66 case 'sine2'
|
samer@34
|
67 Y=blockdata(sine,D,0,F)./(F/0.01);
|
samer@34
|
68 otherwise
|
samer@34
|
69 Y=blockdata(sine,D,0,F);
|
samer@34
|
70 end
|
samer@34
|
71
|
samer@34
|
72 % if supplied apply envelope to shape each note
|
samer@34
|
73 if nargin>2 && ~isempty(env),
|
samer@36
|
74 ad=map(env,Y);
|
samer@34
|
75 else
|
samer@34
|
76 ad=Y;
|
samer@34
|
77 end
|
samer@34
|
78
|
samer@34
|
79 % rebuffer to constant size
|
samer@34
|
80 if ~isempty(opts.buffer)
|
samer@36
|
81 ad=window(ad,ceil(opts.buffer*opts.fs));
|
samer@34
|
82 end
|
samer@34
|
83
|