// jeff smith, december '07 // // THIS PROGRAM WILL CRASH CHUCK UNLESS YOU CREATE THE FOLLOWING DIRECTORIES: // // channel/top/ // channel/middle/ // channel/bottom/ // // upon completion, chuck will deposit 4 channel files in top and bottom directories, 8 in middle // // multi-channel (16) layout // we have an x,y,z sphere at origin(0,0,0) and radius = 1 // at z = cos(pi/4) (top) we have four channels arrayed in a circle of radius 1.0 // at z = cos(pi/2)(middle) we have eight channels // at z = cos(3pi/4) (bottom) we have four channels // if you imposed the sphere w/i a cube, then the eight corners would be (y, x, z): // front,left,bottom: -1, -1, -1 // front,left,top: -1, -1, 1 // front,right,top: -1, 1, 1 // front,right,bottom: -1, 1, -1 // back, right,bottom: 1, 1, -1 // back, right,top: 1, 1, 1 // back, left, top: 1, -1, 1 // back, left, bottom: 1, -1, -1 16 => int num_channels; -1 => int room_left; -1 => int room_front; -1 => int room_bottom; 1 => int room_right; 1 => int room_back; 1 => int room_top; room_right - room_left => int room_width; room_back - room_front => int room_depth; room_top - room_bottom => int room_height; // radius of our room room_width / 2.0 => float room_radius; // angle from origins to top, mid, and bottom planes [pi / 4.0, pi / 2.0, 3.0 * pi / 4.0] @=> float z_angle[]; [4, 8, 4, 0] @=> int points_per_plane[]; // first x,y channel will be right,front, and then we proceed // counter-clockwise around the room -pi/4.0 => float xy_angle_phase; [ // top four "channels/top/fr.wav", "channels/top/br.wav", "channels/top/bl.wav", "channels/top/fl.wav", // middle eight "channels/middle/fr.wav", "channels/middle/r.wav", "channels/middle/br.wav", "channels/middle/b.wav", "channels/middle/bl.wav", "channels/middle/l.wav", "channels/middle/fl.wav", "channels/middle/f.wav", // bottom four "channels/bottom/fr.wav", "channels/bottom/br.wav", "channels/bottom/bl.wav", "channels/bottom/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, float dz) { dx * dx => dx; dy * dy => dy; dz * dz => dz; Math.sqrt(dx + dy + dz) => float d; return d; } class ChannelPoint { float x,y,z; 0.0 => x => y => z; 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. 1.1 => float channel_radius; public float gain_at_point(float x2, float y2, float z2) { distance(x - x2, y - y2, z - z2) => float d; // if d > channel_radius, return zero return Math.max(0.0, channel_radius - d); } public void connect(float x1, float y1, float z1, float gn, UGen in, NChannelWave out, int index) { x1 => x; y1 => y; z1 => z; 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; } // 0.6 < channel_radius <= room_width Math.max(0.6, cr * room_width) => 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,z: g", x, y, z, ":", g.gain() >>>; } } class ChannelSphere { if (room_height != room_width || room_width != room_depth) { <<< "error: room dimensions: variable radius" >>>; me.exit(); } // this parameter will define the sum gain of all channels; set to zero if you don't want to normalize 1.0 => float normalize; // our radius for the sphere room_radius => float sphere_radius; 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) // in the event you want a different origin, modify these values 0.0 => float o_x => float o_y => float o_z; 0 => int c; for (0 => int k; points_per_plane[k]; k++) { z_angle[k] => float phi; points_per_plane[k] => int n; for (0 => int i; i < n; i++) { ((i * 1.0) / n) * (2.0 * pi) + xy_angle_phase => float theta; o_x + (sphere_radius * Math.cos(theta) * Math.sin(phi)) => float x; o_y + (sphere_radius * Math.sin(theta) * Math.sin(phi)) => float y; o_z + (sphere_radius * Math.cos(phi)) => float z; points[c].connect(x, y, z, gn, in, out, c); ++c; } } } 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 public void PanXYZ(float dx /* -1 = left, 1 = right */, float dy /* -1 = front, 1 = back */, float dz /* -1 = bottom, 1 = top */) { if (dx > 1.0 || dx < -1.0 || dy > 1.0 || dy < -1.0 || dz > 1.0 || dz < -1.0) { <<< "panxyz error: ", dx, dy, dz >>>; return; } // update gain of all channels based on inverse distance from panning point (dx, dy, dz) 0.0 => float sum; for (0 => int i; i < num_points; i++) { points[i].gain_at_point(dx, dy, dz) => 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) { PanXYZ(pan, 0, 0); } // set front/back (fade); note that this would over-ride lr/bt public void PanFB(float pan) { PanXYZ(0, pan, 0); } // set bottom/top; note that this over-rides fb/lr, etc. public void PanBT(float pan) { PanXYZ(0, 0, pan); } public void PanXY(float x, float y) { PanXYZ(x, y, 0); } 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 ChannelSphere { 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 ChannelSphere { 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(0.55); // turn off normalized values SetNormalizeValue(0.0); } } class CPitch extends ChannelSphere { 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.PanXYZ(0, 0, 0); p2.PanXYZ(0, 0, 0); p3.PanXYZ(0, 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 bottom, 2 middle, 3 top p1.PanBT(-1); p2.PanBT(0); p3.PanBT(1); 1::second => now; // put 1 in left,front,bottom, 2 in right,front,middle, 3 in right,back,top p1.PanXYZ(-1, -1, -1); p2.PanXYZ(1, -1, 0); p3.PanXYZ(1, 1, 1); 1::second => now; // put 1 left, 2 on back, 3 on top p1.PanLR(-1); p2.PanFB(1); p3.PanBT(1); 1::second => now; // move p2 from left,front,bottom to right,back,top 10 => int iterations; (room_width) * 1.0 / (iterations - 1) => float dx; (room_depth) * 1.0 / (iterations - 1) => float dy; (room_height) * 1.0 / (iterations - 1) => float dz; room_left => float x; room_front => float y; room_bottom => float z; for (0 => int i; i < iterations; i++) { p2.PanXYZ(x, y, z); 1::second/iterations => now; x + dx => x; y + dy => y; z + dz => z; } // pan up a helix with each sound a phase (pi / 2) behind. // we'll go 1000 ms, and we'll iterate 1000/50 times. 1000 => float time_amount; (time_amount / 50) $ int => iterations; time_amount / (iterations + 1) => float time_chunk; // set up circle origin and radius 0.0 => float o_x; 0.0 => float o_y; 1.0 => float r; // 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; pi => float phi; phi / (((iterations + 1.0) * 3.0) - 1) => float phi_delta; for (0 => int i; i <= iterations; i++) { ((i * 1.0) / iterations) * 2 * pi => float t; o_x + r * Math.cos(t + phase_1) => x; o_y + r * Math.sin(t + phase_1) => y; r * Math.cos(phi) => z; phi - phi_delta => phi; p1.PanXYZ(x, y, z); o_x + r * Math.cos(t + phase_2) => x; o_y + r * Math.sin(t + phase_2) => y; r * Math.cos(phi) => z; phi - phi_delta => phi; p2.PanXYZ(x, y, z); o_x + r * Math.cos(t + phase_3) => x; o_y + r * Math.sin(t + phase_3) => y; r * Math.cos(phi) => z; phi - phi_delta => phi; p3.PanXYZ(x, y, z); time_amount - time_chunk => time_amount; } // tell our n channel wave class we are done ncw.done(); <<< "done" >>>;