Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
40 changes: 32 additions & 8 deletions Package.swift → Guest/Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import PackageDescription

let embeddedSwiftSettings: [SwiftSetting] = [
.enableExperimentalFeature("Embedded"),
.enableExperimentalFeature("Embedded"),
.enableExperimentalFeature("Extern"),
.interoperabilityMode(.Cxx),
.unsafeFlags(["-wmo", "-disable-cmo", "-Xfrontend", "-gnone"])
.unsafeFlags(["-wmo", "-disable-cmo", "-Xfrontend", "-gnone", "-disable-stack-protector"]),
]

let embeddedCSettings: [CSetting] = [
.unsafeFlags(["-fdeclspec"])
.unsafeFlags(["-fdeclspec"]),
]

let linkerSettings: [LinkerSetting] = [
.unsafeFlags([
"-Xclang-linker", "-nostdlib",
"-Xlinker", "--no-entry"
])
"-Xlinker", "--no-entry",
]),
]

let libcSettings: [CSetting] = [
Expand All @@ -33,13 +45,13 @@ let libcSettings: [CSetting] = [
]

let package = Package(
name: "swift-for-wasm-example",
name: "Guest",
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "swift-audio",
dependencies: ["VultDSP", "dlmalloc"],
name: "Plotter",
dependencies: ["dlmalloc"],
cSettings: embeddedCSettings,
swiftSettings: embeddedSwiftSettings,
linkerSettings: linkerSettings
Expand All @@ -51,3 +63,15 @@ let package = Package(
),
]
)

for module in ["Kick", "HiHat", "Bass", "Mix"] {
package.targets.append(
.executableTarget(
name: module,
dependencies: ["VultDSP", "dlmalloc"],
cSettings: embeddedCSettings,
swiftSettings: embeddedSwiftSettings,
linkerSettings: linkerSettings
)
)
}
1 change: 1 addition & 0 deletions Guest/Sources/Bass/Shared
33 changes: 33 additions & 0 deletions Guest/Sources/Bass/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

@_expose(wasm, "main")
@_cdecl("main")
func main(contextIndex: Int) {
var sequencedBass = Sequencer(
instrument: Bass(),
sequence: [
.noteOn(.c.octave(1)), .noteOff, .noteOn(.d.octave(1)), .noteOff, .noteOn(.e.octave(1)),
.noteOff, .noteOn(.f.octave(1)), .noteOff, .noteOn(.g.octave(1)), .noteOff, .noteOn(.a.octave(1)),
],
stepLengthInSeconds: 0.25
)

let totalLengthInSeconds = 6

let buffer = AudioBuffer(
capacity: sampleRate * totalLengthInSeconds,
source: &sequencedBass
)

Audio.encode(contextIndex: contextIndex, buffer)
}
1 change: 1 addition & 0 deletions Guest/Sources/HiHat/Shared
35 changes: 35 additions & 0 deletions Guest/Sources/HiHat/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

@_expose(wasm, "main")
@_cdecl("main")
func main(contextIndex: Int) {
var sequencedHiHat = Sequencer(
instrument: HiHat(),
sequence: [
.noteOff, .noteOff, .noteOff, .noteOff,
.noteOff, .noteOff, .noteOff, .noteOff,
.noteOn(.c.octave(1)), .noteOff, .noteOn(.c.octave(1)), .noteOff,
.noteOff, .noteOff, .noteOff, .noteOff,
],
stepLengthInSeconds: 0.125
)

let totalLengthInSeconds = 6

let buffer = AudioBuffer(
capacity: sampleRate * totalLengthInSeconds,
source: &sequencedHiHat
)

Audio.encode(contextIndex: contextIndex, buffer)
}
File renamed without changes.
116 changes: 116 additions & 0 deletions Guest/Sources/JavaScript/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import { Encoder } from './encoder.js';

const decoder = new TextDecoder();
const loggerElement = document.getElementById('wasm-logger');
const moduleInstances = [];

function wasmMemoryAsString(i, address, byteCount) {
return decoder.decode(moduleInstances[i].exports.memory.buffer.slice(address, address + byteCount));
}

function wasmMemoryAsFloat32Array(i, address, byteCount) {
return new Float32Array(moduleInstances[i].exports.memory.buffer.slice(address, address + byteCount));
}

const contexts = [];

const canvasImports = {
canvas: {
beginPath: (i) => contexts[i].beginPath(),
stroke: (i) => contexts[i].stroke(),
moveTo: (i, x, y) => contexts[i].moveTo(x, y),
lineTo: (i, x, y) => contexts[i].lineTo(x, y),
},
}

const plotterModule = await WebAssembly.instantiateStreaming(
fetch(".build/wasm32-unknown-none-wasm/release/plotter.wasm"),
{ ...canvasImports }
);

function encodeAndPlot(audioBuffer, context) {
const wavEncoder = new Encoder(44100, 1);
wavEncoder.encode([audioBuffer]);
const blob = wavEncoder.finish();

const audioURL = URL.createObjectURL(blob);
document.getElementsByClassName('audio')[context].setAttribute('src', audioURL);

const byteCount = audioBuffer.length * 4;
const bufferPointer = plotterExports.allocateAudioBuffer(byteCount);
const memoryBytes = new Float32Array(plotterExports.memory.buffer);
memoryBytes.set(audioBuffer, bufferPointer / 4);
plotterExports.plot(context, 1000, 200, 10, bufferPointer, byteCount);
plotterExports.free(bufferPointer);
}

const plotterExports = plotterModule.instance.exports;

const audioImports = {
audio: {
encode: (i, address, byteCount) => {
const audioBuffer = wasmMemoryAsFloat32Array(i, address, byteCount);

encodeAndPlot(audioBuffer, i);
},
},
};

const consoleImports = {
console: {
log: (i, address, byteCount) => {
loggerElement.innerHTML = wasmMemoryAsString(i, address, byteCount);
},
logInt: (x) => console.log(x),
logFloat: (x) => console.log(x),
}
};

const pluginElements = document.getElementsByClassName("plugin");

for (let i = 0; i < pluginElements.length; ++i) {
const element = pluginElements[i];
const canvasElement = element.getElementsByClassName("plotter")[0];
const canvasContext = canvasElement.getContext('2d');
canvasContext.strokeStyle = 'white';

contexts.push(canvasContext);

const { instance } = await WebAssembly.instantiateStreaming(
fetch(element.dataset.modulePath),
{ ...audioImports, ...consoleImports }
);

moduleInstances.push(instance);

instance.exports.main(i);
}

const mixElement = document.getElementById("tracks-mix");

if (mixElement) {
const canvasElement = mixElement.getElementsByClassName("plotter")[0];
const canvasContext = canvasElement.getContext('2d');
canvasContext.strokeStyle = 'white';

contexts.push(canvasContext);

const response = await fetch("/mix");
const responseBlob = await response.blob();

const audioBuffer = new Float32Array(await responseBlob.arrayBuffer());

encodeAndPlot(audioBuffer, contexts.length - 1);
}
1 change: 1 addition & 0 deletions Guest/Sources/Kick/Shared
30 changes: 30 additions & 0 deletions Guest/Sources/Kick/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

@_expose(wasm, "main")
@_cdecl("main")
func main(contextIndex: Int) {
var sequencedKick = Sequencer(
instrument: Kick(),
sequence: [.noteOff, .noteOn(.c.octave(1)), .noteOff, .noteOn(.c.octave(1))],
stepLengthInSeconds: 0.25
)

let totalLengthInSeconds = 6

let buffer = AudioBuffer(
capacity: sampleRate * totalLengthInSeconds,
source: &sequencedKick
)

Audio.encode(contextIndex: contextIndex, buffer)
}
1 change: 1 addition & 0 deletions Guest/Sources/Mix/Shared
11 changes: 5 additions & 6 deletions Sources/swift-audio/main.swift → Guest/Sources/Mix/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
//===----------------------------------------------------------------------===//

@_expose(wasm, "main")
func main() {
@_cdecl("main")
func main(contextIndex: Int) {
let sequencedKick = Sequencer(
instrument: Kick(),
sequence: [.noteOff, .noteOn(.c.octave(1)), .noteOff, .noteOn(.c.octave(1))],
Expand All @@ -38,6 +39,8 @@ func main() {
stepLengthInSeconds: 0.25
)

let totalLengthInSeconds = 6

var mixer = Mixer(
source1: sequencedHiHat,
volume1: 0.05,
Expand All @@ -47,14 +50,10 @@ func main() {
volume3: 0.1
)

let totalLengthInSeconds = 6
let buffer = AudioBuffer(
capacity: sampleRate * totalLengthInSeconds,
source: &mixer
)
Audio.encode(buffer)

Plotter<HTMLCanvas>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in a separate dynamically-linked Plotter Wasm module.

width: 1000, height: 200, margin: 10
).plot(buffer)
Audio.encode(contextIndex: contextIndex, buffer)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,26 @@
/// implementation is required to provide these functions for drawing code in
/// this module to work.
protocol Canvas {
static func beginPath()
static func stroke()
static func moveTo(x: Int, y: Int)
static func lineTo(x: Int, y: Int)
static func beginPath(ctx: Int)
static func stroke(ctx: Int)
static func moveTo(ctx: Int, x: Int, y: Int)
static func lineTo(ctx: Int, x: Int, y: Int)
}

struct HTMLCanvas: Canvas {
@_extern(wasm, module: "canvas", name: "beginPath")
@_extern(c)
static func beginPath()
static func beginPath(ctx: Int)

@_extern(wasm, module: "canvas", name: "stroke")
@_extern(c)
static func stroke()
static func stroke(ctx: Int)

@_extern(wasm, module: "canvas", name: "moveTo")
@_extern(c)
static func moveTo(x: Int, y: Int)
static func moveTo(ctx: Int, x: Int, y: Int)

@_extern(wasm, module: "canvas", name: "lineTo")
@_extern(c)
static func lineTo(x: Int, y: Int)
static func lineTo(ctx: Int, x: Int, y: Int)
}
Loading