forked from esaruoho/paketti
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPakettiImageToSample.lua
More file actions
479 lines (404 loc) · 14.8 KB
/
PakettiImageToSample.lua
File metadata and controls
479 lines (404 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
local vb = renoise.ViewBuilder()
local DIALOG_TITLE = "Paketti Image to Sample Converter"
-- Global variables for the image to sample dialog
local image_to_sample_dialog = nil
local loaded_image_bitmap = nil
local current_sample_data = nil
local waveform_canvas_view = nil
local current_image_info = nil
-- Canvas dimensions
local WAVEFORM_CANVAS_WIDTH = 800
local WAVEFORM_CANVAS_HEIGHT = 200
-- Color constants for canvas drawing
local COLOR_BACKGROUND = {0, 0, 0, 255}
local COLOR_WAVEFORM = {0, 255, 0, 255}
local COLOR_GRID = {0, 64, 0, 128}
local COLOR_CENTER_LINE = {128, 128, 128, 255}
-- Convert image brightness to waveform amplitude
function PakettiImageToSampleConvertImageToWaveform(image_path)
-- Temporarily disable AutoSamplify monitoring to prevent interference
local AutoSamplifyMonitoringState = PakettiTemporarilyDisableNewSampleMonitoring()
if not image_path then
-- Create a default sine wave for demonstration
local samples = 512 -- Short single-cycle waveform
local waveform_data = {}
for i = 0, samples - 1 do
local phase = (i / samples) * 2 * math.pi
local amplitude = math.sin(phase)
table.insert(waveform_data, amplitude)
end
return waveform_data
end
-- For actual image processing, we'd need to:
-- 1. Load the image file and read pixel data
-- 2. Convert pixel brightness/color to amplitude values
-- 3. Return the waveform data array
-- Since Lua doesn't have built-in image processing, we'll analyze
-- the binary data of the image file to create a unique waveform
local samples = 1024
local waveform_data = {}
-- Read some bytes from the image file for analysis
local file = io.open(image_path, "rb")
if not file then
-- Fallback to default sine wave if file can't be read
for i = 0, samples - 1 do
local phase = (i / samples) * 2 * math.pi
local amplitude = math.sin(phase)
table.insert(waveform_data, amplitude)
end
return waveform_data
end
-- Read first 1024 bytes of the file for analysis
local file_data = file:read(1024)
file:close()
if not file_data then file_data = "" end
-- Convert file bytes to waveform amplitudes
for i = 0, samples - 1 do
local byte_index = (i % #file_data) + 1
local byte_value = 0
if i < #file_data then
byte_value = string.byte(file_data, byte_index) or 0
end
-- Normalize byte value (0-255) to amplitude (-1 to 1)
local amplitude = (byte_value / 127.5) - 1.0
-- Add some smoothing with neighboring values
if i > 0 and i < samples - 1 then
local prev_byte = string.byte(file_data, math.max(1, byte_index - 1)) or 0
local next_byte = string.byte(file_data, math.min(#file_data, byte_index + 1)) or 0
local prev_amp = (prev_byte / 127.5) - 1.0
local next_amp = (next_byte / 127.5) - 1.0
amplitude = (prev_amp * 0.25 + amplitude * 0.5 + next_amp * 0.25)
end
table.insert(waveform_data, amplitude)
end
return waveform_data
end
-- Canvas render function for waveform display
function PakettiImageToSampleRenderWaveform(ctx)
local width = WAVEFORM_CANVAS_WIDTH
local height = WAVEFORM_CANVAS_HEIGHT
-- Clear canvas
ctx:clear_rect(0, 0, width, height)
-- Draw grid lines
ctx.stroke_color = COLOR_GRID
ctx.line_width = 1
-- Vertical grid lines
for i = 0, 10 do
local x = (i / 10) * width
ctx:begin_path()
ctx:move_to(x, 0)
ctx:line_to(x, height)
ctx:stroke()
end
-- Horizontal grid lines
for i = 0, 4 do
local y = (i / 4) * height
ctx:begin_path()
ctx:move_to(0, y)
ctx:line_to(width, y)
ctx:stroke()
end
-- Draw center line
ctx.stroke_color = COLOR_CENTER_LINE
ctx.line_width = 2
local center_y = height / 2
ctx:begin_path()
ctx:move_to(0, center_y)
ctx:line_to(width, center_y)
ctx:stroke()
-- Draw waveform
if current_sample_data and #current_sample_data > 0 then
ctx.stroke_color = COLOR_WAVEFORM
ctx.line_width = 2
ctx:begin_path()
local sample_count = #current_sample_data
for x = 0, width - 1 do
local sample_index = math.floor((x / width) * sample_count) + 1
if sample_index <= sample_count then
local amplitude = current_sample_data[sample_index]
local y = center_y - (amplitude * center_y)
if x == 0 then
ctx:move_to(x, y)
else
ctx:line_to(x, y)
end
end
end
ctx:stroke()
end
end
-- Get image dimensions and file info
function PakettiImageToSampleGetImageInfo(file_path)
local file = io.open(file_path, "rb")
if not file then return nil end
-- Get file size
file:seek("end")
local file_size = file:seek()
file:seek("set", 0)
local width, height = 0, 0
local format = "Unknown"
-- Read first few bytes to determine format
local header = file:read(10) -- Read a bit more for GIF detection
file:seek("set", 0)
if header then
-- Check PNG signature
if header:sub(1, 8) == "\137\80\78\71\13\10\26\10" then
format = "PNG"
file:seek("set", 16) -- Skip to IHDR width/height
local width_bytes = file:read(4)
local height_bytes = file:read(4)
if width_bytes and height_bytes and #width_bytes == 4 and #height_bytes == 4 then
-- PNG uses big-endian 32-bit integers
local w1, w2, w3, w4 = string.byte(width_bytes, 1, 4)
local h1, h2, h3, h4 = string.byte(height_bytes, 1, 4)
width = w1 * 16777216 + w2 * 65536 + w3 * 256 + w4
height = h1 * 16777216 + h2 * 65536 + h3 * 256 + h4
end
-- Check BMP signature
elseif header:sub(1, 2) == "BM" then
format = "BMP"
file:seek("set", 18) -- Skip to width/height in BMP header
local width_bytes = file:read(4)
local height_bytes = file:read(4)
if width_bytes and height_bytes and #width_bytes == 4 and #height_bytes == 4 then
-- BMP uses little-endian 32-bit integers
local w1, w2, w3, w4 = string.byte(width_bytes, 1, 4)
local h1, h2, h3, h4 = string.byte(height_bytes, 1, 4)
width = w4 * 16777216 + w3 * 65536 + w2 * 256 + w1
height = h4 * 16777216 + h3 * 65536 + h2 * 256 + h1
end
-- Check JPEG signature
elseif header:sub(1, 3) == "\255\216\255" then
format = "JPEG"
-- Scan for SOF0 marker (0xFFC0) to get dimensions
file:seek("set", 0)
local data = file:read(1024) -- Read more data to find SOF marker
if data then
local sof_pos = data:find("\255\192") -- SOF0 marker
if sof_pos and sof_pos + 7 <= #data then
local h1, h2 = string.byte(data, sof_pos + 5), string.byte(data, sof_pos + 6)
local w1, w2 = string.byte(data, sof_pos + 7), string.byte(data, sof_pos + 8)
height = h1 * 256 + h2
width = w1 * 256 + w2
end
end
-- Check GIF signature
elseif header:sub(1, 6) == "GIF87a" or header:sub(1, 6) == "GIF89a" then
format = "GIF"
file:seek("set", 6) -- GIF dimensions are at bytes 6-9
local dim_bytes = file:read(4)
if dim_bytes and #dim_bytes == 4 then
local w1, w2, h1, h2 = string.byte(dim_bytes, 1, 4)
width = w1 + w2 * 256 -- GIF uses little-endian
height = h1 + h2 * 256
end
end
end
file:close()
-- Format file size
local size_str = ""
if file_size < 1024 then
size_str = file_size .. " bytes"
elseif file_size < 1024 * 1024 then
size_str = string.format("%.1f KB", file_size / 1024)
else
size_str = string.format("%.1f MB", file_size / (1024 * 1024))
end
return {
width = width,
height = height,
format = format,
file_size = file_size,
size_string = size_str
}
end
-- Load image file and convert to waveform (now called before dialog opens)
function PakettiImageToSampleLoadImage(file_path)
if not file_path then return false end
renoise.app():show_status("Loading image: " .. file_path)
-- Check if file exists and get image info
local file = io.open(file_path, "r")
if not file then
renoise.app():show_status("Error: Could not open image file")
return false
end
file:close()
-- Get image metadata
current_image_info = PakettiImageToSampleGetImageInfo(file_path)
-- Store the image path
loaded_image_bitmap = file_path
-- Convert image to waveform data
current_sample_data = PakettiImageToSampleConvertImageToWaveform(file_path)
local filename = file_path:match("([^/\\]+)$") or file_path
local info_str = ""
if current_image_info then
info_str = string.format(" (%dx%d, %s)", current_image_info.width, current_image_info.height, current_image_info.size_string)
end
renoise.app():show_status("Image loaded: " .. filename .. info_str .. " (" .. #current_sample_data .. " samples generated)")
return true
end
-- Export waveform as sample
function PakettiImageToSampleExportToSample()
if not current_sample_data then
renoise.app():show_status("No waveform data to export")
return
end
local song = renoise.song()
local instrument = song.selected_instrument
-- Ensure the instrument has at least one sample
if #instrument.samples == 0 then
-- Create a new sample if none exists
instrument:insert_sample_at(1)
end
local sample = instrument.samples[1]
local sample_buffer = sample.sample_buffer
-- Create new sample buffer
sample_buffer:create_sample_data(44100, 16, 1, #current_sample_data)
-- Copy waveform data to sample buffer
if sample_buffer.has_sample_data then
sample_buffer:prepare_sample_data_changes()
for i = 1, #current_sample_data do
sample_buffer:set_sample_data(1, i, current_sample_data[i])
end
sample_buffer:finalize_sample_data_changes()
end
-- Set a nice name for the sample
if loaded_image_bitmap then
local filename = loaded_image_bitmap:match("([^/\\]+)$") or "image"
local basename = filename:match("(.+)%..+$") or filename -- Remove extension
sample.name = "IMG_" .. basename
end
-- Set loop to Forward and On for continuous playback
sample.loop_mode = renoise.Sample.LOOP_MODE_FORWARD
sample.loop_start = 1
sample.loop_end = #current_sample_data
renoise.app():show_status("Waveform exported to sample: " .. (sample.name or "Sample 01") .. " (Loop: Forward)")
-- Restore AutoSamplify monitoring state
PakettiRestoreNewSampleMonitoring(AutoSamplifyMonitoringState)
end
-- Create the image to sample dialog
function PakettiImageToSampleShowDialog()
if image_to_sample_dialog and image_to_sample_dialog.visible then
image_to_sample_dialog:close()
end
-- Image data should already be loaded before dialog opens
-- Create dialog content
local dialog_content = vb:column {
-- Image info section
vb:horizontal_aligner {
mode = "center",
vb:text {
id = "image_info_display",
text = loaded_image_bitmap and current_image_info and
string.format("%s - %dx%d pixels, %s (%s)",
loaded_image_bitmap:match("([^/\\]+)$") or loaded_image_bitmap,
current_image_info.width, current_image_info.height,
current_image_info.format, current_image_info.size_string) or "No image loaded",
style = "strong"
}
},
-- Waveform display section
vb:column {
vb:canvas {
id = "waveform_canvas",
width = WAVEFORM_CANVAS_WIDTH,
height = WAVEFORM_CANVAS_HEIGHT,
mode = "plain",
render = PakettiImageToSampleRenderWaveform
}
},
-- Control buttons
vb:horizontal_aligner {
mode = "center",
vb:button {
text = "Load Different Image",
width = 140,
notifier = function()
local file_path = renoise.app():prompt_for_filename_to_read({"*.png", "*.bmp", "*.jpg", "*.jpeg", "*.gif"}, "Select different image file")
if file_path and PakettiImageToSampleLoadImage(file_path) then
-- Update combined image info display
local info_view = vb.views.image_info_display
if info_view and current_image_info then
local filename = file_path:match("([^/\\]+)$") or file_path
info_view.text = string.format("%s - %dx%d pixels, %s (%s)",
filename, current_image_info.width, current_image_info.height,
current_image_info.format, current_image_info.size_string)
end
-- Refresh canvas
if waveform_canvas_view then
waveform_canvas_view:update()
end
end
end
},
vb:button {
text = "Export to Sample",
width = 120,
notifier = function()
PakettiImageToSampleExportToSample()
end
},
vb:button {
text = "Close",
width = 80,
notifier = function()
if image_to_sample_dialog then
image_to_sample_dialog:close()
end
end
}
}
}
-- Create and show dialog
image_to_sample_dialog = renoise.app():show_custom_dialog(DIALOG_TITLE, dialog_content, my_keyhandler_func)
-- Store canvas reference for updates
waveform_canvas_view = vb.views.waveform_canvas
-- Ensure Renoise gets keyboard focus
renoise.app().window.active_middle_frame = renoise.app().window.active_middle_frame
end
-- Menu entry function - now prompts for image first
function PakettiImageToSampleStart()
-- Prompt for image file first
local file_path = renoise.app():prompt_for_filename_to_read({"*.png", "*.bmp", "*.jpg", "*.jpeg", "*.gif"}, "Select image file to convert to waveform")
if not file_path then
renoise.app():show_status("Image to Sample: Cancelled")
return
end
-- Load and convert the image
if PakettiImageToSampleLoadImage(file_path) then
-- Open dialog with waveform ready
PakettiImageToSampleShowDialog()
end
end
-- Add menu entries
renoise.tool():add_menu_entry {
name = "Main Menu:Tools:Paketti Image to Sample Converter",
invoke = function()
PakettiImageToSampleStart()
end
}
renoise.tool():add_keybinding {
name = "Global:Paketti:Paketti Image to Sample Converter",
invoke = function()
PakettiImageToSampleStart()
end
}
-- File import hooks for drag & drop and file loading
local function PakettiImageToSampleImportHook(file_path)
-- Load the image and show dialog directly
if PakettiImageToSampleLoadImage(file_path) then
PakettiImageToSampleShowDialog()
return true
end
return false
end
-- Create integration for image formats
local image_integration = {
category = "sample",
extensions = { "png", "bmp", "jpg", "jpeg", "gif" },
invoke = PakettiImageToSampleImportHook
}
-- Add file import hook if not already present
if not renoise.tool():has_file_import_hook("sample", { "png", "bmp", "jpg", "jpeg", "gif" }) then
renoise.tool():add_file_import_hook(image_integration)
end