whistles

3d printable Irish whistles
git clone https://git.woozle.org/neale/whistles.git

Neale Pickett  ·  2025-12-13

whistle.scad

  1include <common.scad>
  2include <tonehole.scad>
  3
  4// Which section to render for printing
  5Section = -1; // [-1:Everything, 0:Bottom, 1:Middle, 2:Top]
  6
  7// Height of the instrument
  8Height = 469;
  9// Inner bore diameter
 10InnerDiameter = 20; // 0.1
 11// Outer diameter
 12OuterDiameter = 25.0; // 0.1
 13
 14// Diameter of the rubber O ring
 15ORingDiameter = 1.50; // 0.01
 16// How far beyond the outer diameter the O ring should protrude
 17ORingProtrusion = 0.80; // 0.01
 18
 19// Cuts to make for a sectioned print
 20Cuts = [190, 340];
 21
 22// Use Axianov tone holes?
 23AxianovHoles = true;
 24
 25// Text to display on the back
 26Text = "d";
 27
 28Hole1 = [90, 7.45];
 29Hole2 = [130, 11.0];
 30Hole3 = [161, 9.45];
 31Hole4 = [221, 7.45];
 32Hole5 = [254, 9.50];
 33Hole6 = [293, 7.45];
 34
 35/* [ Esoterics ] */
 36// Angle for tone holes. Anything other than 45 will create challenging overhangs!
 37AxianovHoleAngle = 45;
 38
 39// Depth of the tenon joint. Shallower will be less stable.
 40TenonDepth = 20;
 41
 42// Make the inner bore square, outer body hexagonal?
 43LowPoly = false;
 44
 45/* [ Hidden ] */
 46sortedCuts = [min(Cuts[0], Cuts[1]), max(Cuts[0], Cuts[1])];
 47id = LowPoly ? (sqrt(TAU) * InnerDiameter/2) : InnerDiameter;
 48od = LowPoly? (sqrt(TAU) * OuterDiameter/2) : OuterDiameter;
 49
 50module whistle() {
 51  difference() {
 52    if (LowPoly) {
 53      union() {
 54        cylinder(h=Height-70, d=od, $fn=6);
 55        translate([0, 0, Height-70]) cylinder(h=70, d=OuterDiameter);
 56      }
 57    } else {
 58      cylinder(h=Height, d=od);
 59    }
 60
 61    translate([0, 0, -1]) {
 62      if (LowPoly) {
 63        cylinder(h=Height+2, d=id, $fn=4);
 64      } else {
 65        cylinder(h=Height+2, d=InnerDiameter);
 66      }
 67    }
 68
 69    // O ring grooves
 70    for (z = [Height-7, Height-20]) {
 71      translate([0, 0, z]) tube(h=ORingDiameter*1.3, id=OuterDiameter - ORingDiameter*2 + ORingProtrusion, od=OuterDiameter+1);
 72    }
 73
 74    // Decoration
 75    for (i = [0, sortedCuts[0], sortedCuts[1]]) {
 76      translate([0, 0, i + 2]) {
 77        translate([0, 0, 0]) tube(h=1, id=OuterDiameter-0.5, od=OuterDiameter*2);
 78        translate([0, 0, 4]) tube(h=1, id=OuterDiameter-0.5, od=OuterDiameter*2);
 79      }
 80    }
 81
 82    for (hole = [Hole1, Hole2, Hole3, Hole4, Hole5, Hole6]) {
 83      if (AxianovHoles) {
 84        z = hole[0];
 85        top = (z>sortedCuts[0]) || (z>sortedCuts[1]);
 86        m = [0, top?0:1, 0];
 87        r = [90, top?90-AxianovHoleAngle:90+AxianovHoleAngle, 0];
 88        translate([0, 0, hole[0]]) rotate(r) mirror(m) tonehole(d=hole[1]);
 89      } else {
 90        translate([0, 0, hole[0]]) rotate([90, 0, 0]) cylinder(d=hole[1], h=50);
 91      }
 92    }
 93
 94    // Ridges to catch seams
 95    rotate([0, 0,  110]) translate([0, OuterDiameter/2, 0]) cylinder(h=sortedCuts[0], d=1);
 96    rotate([0, 0, -110]) translate([0, OuterDiameter/2, sortedCuts[0]]) cylinder(h=sortedCuts[1]-sortedCuts[0], d=1);
 97    rotate([0, 0,  110]) translate([0, OuterDiameter/2, sortedCuts[1]]) cylinder(h=Height, d=1);
 98
 99    // Inner channel, seems to cause problems with the tenon join being too thin
100    //rotate([0, 0, 140]) translate([0, InnerDiameter/2, 0]) cylinder(h=Height, d=1);
101
102    // Ruby head, and text on the back
103    // This makes previews slow, so we only do it in final renders.
104    if (!$preview) {
105      intersection() {
106        tube(h=50, id=OuterDiameter-0.5, od=OuterDiameter+10);
107        union() {
108          rotate([90, 0, 0]) {
109            linear_extrude(height=OuterDiameter) {
110              translate([-5, 0, 0]) import("ruby.svg");
111            }
112          }
113          translate([0, 0, 8]) {
114            rotate([90, 0, 180]) {
115              linear_extrude(height=OuterDiameter) {
116                text(Text, halign="center");
117              }
118            }
119          }
120        }
121      }
122    }
123  }
124}
125
126sectionedCuts = concat([-1000], sortedCuts, [1000]);
127if (Section == -1) {
128  whistle();
129} else {
130  tenon(h=sectionedCuts[Section], od=OuterDiameter, depth=TenonDepth, top=true) {
131    tenon(h=sectionedCuts[Section+1], od=OuterDiameter, depth=TenonDepth, top=false) {
132      whistle();
133    }
134  }
135}