// jeff smith, december '07 // // THIS PROGRAM WILL CRASH CHUCK UNLESS YOU CREATE THE FOLLOWING DIRECTORIES: // // channel/ // // upon completion, chuck will deposit 8 channel files in the channel/ directory // // multi-channel (8) layout // we have an -1 <= x <= 1,-2 <= y <= 2 rectangle at origin(0,0) which yields the following // corners // right,front 1, -2 // right,back 1, 2 // left,back -1, 2 // left,front -1, -2 8 => int num_channels; -1.0 => float room_left; -2.0 => float room_front; 1.0 => float room_right; 2.0 => float room_back; room_right - room_left => float room_width; room_back - room_front => float room_depth; // lay out position of each channel counter clockwise from right, front [ room_right, room_front, room_right, room_front + (room_depth / 3.0), room_right, room_front + (2.0 * room_depth / 3.0), room_right, room_back, room_left, room_back, room_left, room_front + (2.0 * room_depth / 3.0), room_left, room_front + (room_depth / 3.0), room_left, room_front ] @=> float channel_xy_locations[]; [ "channels/fr.wav", "channels/fmr.wav", "channels/bmr.wav", "channels/br.wav", "channels/bl.wav", "channels/bml.wav", "channels/fml.wav", "channels/fl.wav", "" ] @=> string channel_files[]; class AChannelWave { WvOut ch; public void init(string s) { if (s == "") { <<< "error: channel file names" >>>; me.exit(); } s => ch.wavFilename; } public void close(string s) { s => ch.closeFile; } } // this class, later a global variable, streams audio to wave files // representing our different channels class NChannelWave { true => int write_to_file; true => int debug_on_dac; new AChannelWave[num_channels] @=> AChannelWave chnls[]; Gain dl, dr; if (debug_on_dac) { 1.0 / (num_channels / 2) => dl.gain; 1.0 / (num_channels / 2) => dr.gain; dr => dac.right; dl => dac.left; } public void init(int write_to_wav_file) { if (!write_to_wav_file && dac.channels() != num_channels) { <<< "number of channels doesn't match dac:", num_channels, dac.channels() >>>; me.exit(); } write_to_wav_file => write_to_file; // == !write_to_dac if (!write_to_file) return; // this order is material and must correspond to ChannelSphere.c_connect loop for (0 => int i; i < num_channels; i++) { chnls[i].init(channel_files[i]); } } public void done() { if (!write_to_file) return; for (0 => int i; i < num_channels; i++) { chnls[i].close(channel_files[i]); } } public void connect(Gain g, float x, int i /* channel_index */) { if (i < 0 || i >= num_channels) { <<< "connect channel error", i >>>; me.exit(); } if (!write_to_file) { g => dac.chan(i); return; } g => chnls[i].ch; if (!debug_on_dac) { chnls[i].ch => blackhole; return; } // debug on two-channel dac while writing wav files if (x == 0) { chnls[i].ch => dl; chnls[i].ch => dr; } else if (x < 0) { chnls[i].ch => dl; } else { chnls[i].ch => dr; } } } fun float distance(float dx, float dy) { dx * dx => dx; dy * dy => dy; return Math.sqrt(dx + dy); } class ChannelPoint { float x,y; 0.0 => x => y; Gain g; 0.0 => g.gain; 0.0 => float set_gain; // To compute the gain of a given channel in panning, we'll compute the distance from a point // of the desired pan location to the channel. The gain for that channel will be the inverse // distance of the channel to that panning point. The channel radius, therefore, defines // the maximum distance from a panning point to the channel. A larger radius, therefore, // will expand the reach of the channel, and will result in more channels being utilized // for a given panning point. (room_width + room_depth) / 2 => float default_radius => float channel_radius; public float gain_at_point(float x2, float y2) { distance(x - x2, y - y2) => float d; // if d > channel_radius, return zero return Math.max(0.0, channel_radius - d); } public void connect(float x1, float y1, float gn, UGen in, NChannelWave out, int index) { x1 => x; y1 => y; Set(gn); in => g; out.connect(g, x, index); } // a channel radius of less than half (cr <= 0.50) room_width will mean that a // XYZ pan of (0,0,0) will yield no sound. public void SetChannelRadius(float cr) { if (cr < 0 || cr > 1.0) { <<< "set distance scale error", cr >>>; return; } // .7 < channel_radius <= 2 * default_radius Math.max(.7, cr * 2 * default_radius) => channel_radius; } public void Set(float gn) { gn => set_gain; Math.max(0.0, Math.min(1.0, gn)) => g.gain; } public void Amplify(float a) { Set(a * set_gain); } public void debug_gains() { <<< "x,y: g", x, y, ":", g.gain() >>>; } } class ChannelPlane { // this parameter will define the sum gain of all channels; set to zero if you don't want to normalize 1.0 => float normalize; num_channels => int num_points; new ChannelPoint[num_points] @=> ChannelPoint points[]; public void SetNormalizeValue(float n) { if (n < 0) { <<< "Normalize value negative", n >>>; return; } n => normalize; } public void SetChannelRadius(float cr) { for (0 => int i; i < num_points; i++) { points[i].SetChannelRadius(cr); } } public void SetChannelRadius(float cr, int index) { if (index < 0 || index > num_points) { <<< "SetChannelRadius error: index out of bounds", index >>>; return; } points[index].SetChannelRadius(cr); } // connect our sound source 'in' to our class and sound output 'out' public void c_connect(float gn, UGen in, NChannelWave out) { // build matrix of channels (see above) // matrix gives distances of any channel from origin (0, 0) 0 => int j; for (0 => int i; i < num_points; i++) { points[i].connect(channel_xy_locations[j], channel_xy_locations[j+1], gn, in, out, i); j + 2 => j; } } public void c_connect(UGen in, NChannelWave out) { // default to gain of .1 c_connect(0.1, in, out); } // set the sound source to a specific location in the room (plane) as defined above // the function will not allow a channel.gain to exceed 1.0 nor be less than 0.0 public void PanXY(float dx /* -1 = left, 1 = right */, float dy /* -1 = front, 1 = back */) { if (dx > room_right || dx < room_left || dy > room_back || dy < room_front) { <<< "panxy error: ", dx, dy >>>; return; } 0.0 => float sum; for (0 => int i; i < num_points; i++) { points[i].gain_at_point(dx, dy) => float d; sum + d => sum; points[i].Set(d); } // normalize all values, allowing sum of gain of all channels to equal 'normalize' if (normalize > 0.0 && sum > 0.0) { normalize / sum => float ratio; for (0 => int i; i < num_points; i++) { points[i].Amplify(ratio); } } } // set each channel to a specific value public void Set(float gn) { for (0 => int i; i < num_points; i++) { points[i].Set(gn); } } // set left/right (stereo); note that this would over-ride fb/bt public void PanLR(float pan) { PanXY(pan, 0); } // set front/back (fade); note that this would over-ride lr/bt public void PanFB(float pan) { PanXY(0, pan); } public void debug_gains() { for (0 => int i; i < num_points; i++) { points[i].debug_gains(); } } } // set up an example of multi-channel output class APitch extends ChannelPlane { SinOsc s; public void connect(float freq, NChannelWave out) { freq => s.freq; // using multiple sound sources, so keep this low .2 => s.gain; // connect our sound source to the channel output class c_connect(s, out); // use default radius and normalize values // SetChannelRadius // SetNormalizeValue } } class BPitch extends ChannelPlane { SawOsc s; public void connect(float freq, NChannelWave out) { freq => s.freq; // using multiple sound sources, so keep this low .2 => s.gain; // connect our sound source to the channel output class c_connect(s, out); // very small radius on channels for this sound source SetChannelRadius(.3); // turn off normalized values SetNormalizeValue(0.0); } } class CPitch extends ChannelPlane { SqrOsc s; public void connect(float freq, NChannelWave out) { freq => s.freq; // using multiple sound sources, so keep this low .2 => s.gain; // connect our sound source to the channel output class c_connect(s, out); // default radius on this sound source // yet small normalized values SetNormalizeValue(.3); } } NChannelWave ncw; APitch p1; BPitch p2; CPitch p3; // init our nchannelwave class true => int write_to_file; ncw.init(write_to_file); // set up three sound sources and attach to our ncw Std.mtof(41) => float freq; p1.connect(freq, ncw); p2.connect(freq * 2, ncw); p3.connect(freq * 4, ncw); // put all sounds in middle of room p1.PanXY(0, 0); p2.PanXY(0, 0); p3.PanXY(0, 0); // test p1 p2.Set(0.0); p3.Set(0.0); 1::second => now; // test p2 p1.Set(0.0); p2.Set(0.2); 1::second => now; // test p3 p2.Set(0.0); p3.Set(0.2); 1::second => now; // put 1 left, 2 middle, 3 right p1.PanLR(room_left); p2.PanLR(0); p3.PanLR(room_right); 1::second => now; // put 1 front, 3 middle, 2 back p1.PanFB(room_front); p3.PanFB(0); p2.PanFB(room_back); 1::second => now; // put 1 in left,front, 2 in right,back 3 in middle p1.PanXY(room_left, room_front); p2.PanXY(room_right, room_back); p3.PanXY(0, 0); 1::second => now; // move p2 from front,left to right,back p1.PanLR(room_left); p3.PanLR(room_right); 10 => int iterations; room_width / (iterations - 1) => float dx; room_depth / (iterations - 1) => float dy; room_left => float x; room_front => float y; for (0 => int i; i < iterations; i++) { p2.PanXY(x, y); 1::second/iterations => now; x + dx => x; y + dy => y; } // send sounds in a circle around the middle of the room // we'll go 1000 ms, and we'll iterate 1000/50 times. 1000 => float time_amount; (time_amount / 50) $ int => iterations; time_amount / iterations => float time_chunk; // set up circle origin and radius 0.0 => float origin_x; 0.0 => float origin_y; 1.0 => float radius; // we'll have each sound source follow by phase pi/2 pi => float phase_1; pi / 2.0 => float phase_2; 0.0 => float phase_3; for (0 => int i; i < iterations; i++) { ((i * 1.0) / iterations) * 2 * pi => float t; origin_x + radius * Math.cos(t + phase_1) => x; origin_y + radius * Math.sin(t + phase_1) => y; p1.PanXY(x, y); origin_x + radius * Math.cos(t + phase_2) => x; origin_y + radius * Math.sin(t + phase_2) => y; p2.PanXY(x, y); origin_x + radius * Math.cos(t + phase_3) => x; origin_y + radius * Math.sin(t + phase_3) => y; p3.PanXY(x, y); time_chunk::ms => now; time_amount - time_chunk => time_amount; } ncw.done(); <<< "done" >>>;